Skip to main content
Glama

Peekaboo MCP

by steipete
ai-sdk-full.md8.71 MB
Directory Structure: └── ./ └── ai ├── .github │ └── workflows │ └── actions │ └── verify-changesets │ ├── index.js │ └── test.js ├── content │ ├── cookbook │ │ ├── 00-guides │ │ │ ├── 01-rag-chatbot.mdx │ │ │ ├── 02-multi-modal-chatbot.mdx │ │ │ ├── 03-slackbot.mdx │ │ │ ├── 04-natural-language-postgres.mdx │ │ │ ├── 05-computer-use.mdx │ │ │ ├── 17-gemini-2-5.mdx │ │ │ ├── 18-claude-4.mdx │ │ │ ├── 19-openai-responses.mdx │ │ │ ├── 20-sonnet-3-7.mdx │ │ │ ├── 21-llama-3_1.mdx │ │ │ ├── 22-gpt-4-5.mdx │ │ │ ├── 23-o1.mdx │ │ │ ├── 24-o3.mdx │ │ │ ├── 25-r1.mdx │ │ │ └── index.mdx │ │ ├── 01-next │ │ │ ├── 10-generate-text.mdx │ │ │ ├── 11-generate-text-with-chat-prompt.mdx │ │ │ ├── 12-generate-image-with-chat-prompt.mdx │ │ │ ├── 122-caching-middleware.mdx │ │ │ ├── 20-stream-text.mdx │ │ │ ├── 21-stream-text-with-chat-prompt.mdx │ │ │ ├── 22-stream-text-with-image-prompt.mdx │ │ │ ├── 23-chat-with-pdf.mdx │ │ │ ├── 24-stream-text-multistep.mdx │ │ │ ├── 25-markdown-chatbot-with-memoization.mdx │ │ │ ├── 30-generate-object.mdx │ │ │ ├── 31-generate-object-with-file-prompt.mdx │ │ │ ├── 40-stream-object.mdx │ │ │ ├── 70-call-tools.mdx │ │ │ ├── 72-call-tools-multiple-steps.mdx │ │ │ ├── 73-mcp-tools.mdx │ │ │ ├── 75-human-in-the-loop.mdx │ │ │ ├── 80-send-custom-body-from-use-chat.mdx │ │ │ ├── 90-render-visual-interface-in-chat.mdx │ │ │ └── index.mdx │ │ ├── 05-node │ │ │ ├── 10-generate-text.mdx │ │ │ ├── 100-retrieval-augmented-generation.mdx │ │ │ ├── 11-generate-text-with-chat-prompt.mdx │ │ │ ├── 12-generate-text-with-image-prompt.mdx │ │ │ ├── 20-stream-text.mdx │ │ │ ├── 21-stream-text-with-chat-prompt.mdx │ │ │ ├── 22-stream-text-with-image-prompt.mdx │ │ │ ├── 23-stream-text-with-file-prompt.mdx │ │ │ ├── 30-generate-object-reasoning.mdx │ │ │ ├── 30-generate-object.mdx │ │ │ ├── 40-stream-object.mdx │ │ │ ├── 41-stream-object-with-image-prompt.mdx │ │ │ ├── 45-stream-object-record-token-usage.mdx │ │ │ ├── 46-stream-object-record-final-object.mdx │ │ │ ├── 50-call-tools.mdx │ │ │ ├── 52-call-tools-with-image-prompt.mdx │ │ │ ├── 53-call-tools-multiple-steps.mdx │ │ │ ├── 54-mcp-tools.mdx │ │ │ ├── 55-manual-agent-loop.mdx │ │ │ ├── 56-web-search-agent.mdx │ │ │ ├── 60-embed-text.mdx │ │ │ ├── 61-embed-text-batch.mdx │ │ │ ├── 70-intercept-fetch-requests.mdx │ │ │ ├── 80-local-caching-middleware.mdx │ │ │ └── index.mdx │ │ ├── 15-api-servers │ │ │ ├── 10-node-http-server.mdx │ │ │ ├── 20-express.mdx │ │ │ ├── 30-hono.mdx │ │ │ ├── 40-fastify.mdx │ │ │ ├── 50-nest.mdx │ │ │ └── index.mdx │ │ └── 20-rsc │ │ ├── 10-generate-text.mdx │ │ ├── 11-generate-text-with-chat-prompt.mdx │ │ ├── 20-stream-text.mdx │ │ ├── 21-stream-text-with-chat-prompt.mdx │ │ ├── 30-generate-object.mdx │ │ ├── 40-stream-object.mdx │ │ ├── 50-call-tools.mdx │ │ ├── 51-call-tools-in-parallel.mdx │ │ ├── 60-save-messages-to-database.mdx │ │ ├── 61-restore-messages-from-database.mdx │ │ ├── 90-render-visual-interface-in-chat.mdx │ │ ├── 91-stream-updates-to-visual-interfaces.mdx │ │ ├── 92-stream-ui-record-token-usage.mdx │ │ └── index.mdx │ ├── docs │ │ ├── 00-introduction │ │ │ └── index.mdx │ │ ├── 02-foundations │ │ │ ├── 01-overview.mdx │ │ │ ├── 02-providers-and-models.mdx │ │ │ ├── 03-prompts.mdx │ │ │ ├── 04-tools.mdx │ │ │ ├── 05-streaming.mdx │ │ │ ├── 06-agents.mdx │ │ │ └── index.mdx │ │ ├── 02-getting-started │ │ │ ├── 01-navigating-the-library.mdx │ │ │ ├── 02-nextjs-app-router.mdx │ │ │ ├── 03-nextjs-pages-router.mdx │ │ │ ├── 04-svelte.mdx │ │ │ ├── 05-nuxt.mdx │ │ │ ├── 06-nodejs.mdx │ │ │ ├── 07-expo.mdx │ │ │ └── index.mdx │ │ ├── 03-ai-sdk-core │ │ │ ├── 01-overview.mdx │ │ │ ├── 05-generating-text.mdx │ │ │ ├── 10-generating-structured-data.mdx │ │ │ ├── 15-tools-and-tool-calling.mdx │ │ │ ├── 20-prompt-engineering.mdx │ │ │ ├── 25-settings.mdx │ │ │ ├── 30-embeddings.mdx │ │ │ ├── 35-image-generation.mdx │ │ │ ├── 36-transcription.mdx │ │ │ ├── 37-speech.mdx │ │ │ ├── 40-middleware.mdx │ │ │ ├── 45-provider-management.mdx │ │ │ ├── 50-error-handling.mdx │ │ │ ├── 55-testing.mdx │ │ │ ├── 60-telemetry.mdx │ │ │ └── index.mdx │ │ ├── 04-ai-sdk-ui │ │ │ ├── 01-overview.mdx │ │ │ ├── 02-chatbot.mdx │ │ │ ├── 03-chatbot-message-persistence.mdx │ │ │ ├── 03-chatbot-tool-usage.mdx │ │ │ ├── 04-generative-user-interfaces.mdx │ │ │ ├── 05-completion.mdx │ │ │ ├── 08-object-generation.mdx │ │ │ ├── 20-streaming-data.mdx │ │ │ ├── 21-error-handling.mdx │ │ │ ├── 21-transport.mdx │ │ │ ├── 24-reading-ui-message-streams.mdx │ │ │ ├── 25-message-metadata.mdx │ │ │ ├── 50-stream-protocol.mdx │ │ │ └── index.mdx │ │ ├── 05-ai-sdk-rsc │ │ │ ├── 01-overview.mdx │ │ │ ├── 02-streaming-react-components.mdx │ │ │ ├── 03-generative-ui-state.mdx │ │ │ ├── 03-saving-and-restoring-states.mdx │ │ │ ├── 04-multistep-interfaces.mdx │ │ │ ├── 05-streaming-values.mdx │ │ │ ├── 06-loading-state.mdx │ │ │ ├── 08-error-handling.mdx │ │ │ ├── 09-authentication.mdx │ │ │ ├── 10-migrating-to-ui.mdx │ │ │ └── index.mdx │ │ ├── 06-advanced │ │ │ ├── 01-prompt-engineering.mdx │ │ │ ├── 02-stopping-streams.mdx │ │ │ ├── 03-backpressure.mdx │ │ │ ├── 04-caching.mdx │ │ │ ├── 05-multiple-streamables.mdx │ │ │ ├── 06-rate-limiting.mdx │ │ │ ├── 07-rendering-ui-with-language-models.mdx │ │ │ ├── 08-model-as-router.mdx │ │ │ ├── 09-multistep-interfaces.mdx │ │ │ ├── 09-sequential-generations.mdx │ │ │ ├── 10-vercel-deployment-guide.mdx │ │ │ └── index.mdx │ │ ├── 07-reference │ │ │ ├── 01-ai-sdk-core │ │ │ │ ├── 01-generate-text.mdx │ │ │ │ ├── 02-stream-text.mdx │ │ │ │ ├── 03-generate-object.mdx │ │ │ │ ├── 04-stream-object.mdx │ │ │ │ ├── 05-embed.mdx │ │ │ │ ├── 06-embed-many.mdx │ │ │ │ ├── 10-generate-image.mdx │ │ │ │ ├── 11-transcribe.mdx │ │ │ │ ├── 12-generate-speech.mdx │ │ │ │ ├── 20-tool.mdx │ │ │ │ ├── 22-dynamic-tool.mdx │ │ │ │ ├── 23-create-mcp-client.mdx │ │ │ │ ├── 24-mcp-stdio-transport.mdx │ │ │ │ ├── 25-json-schema.mdx │ │ │ │ ├── 26-zod-schema.mdx │ │ │ │ ├── 27-valibot-schema.mdx │ │ │ │ ├── 30-model-message.mdx │ │ │ │ ├── 31-ui-message.mdx │ │ │ │ ├── 40-provider-registry.mdx │ │ │ │ ├── 42-custom-provider.mdx │ │ │ │ ├── 50-cosine-similarity.mdx │ │ │ │ ├── 60-wrap-language-model.mdx │ │ │ │ ├── 65-language-model-v2-middleware.mdx │ │ │ │ ├── 66-extract-reasoning-middleware.mdx │ │ │ │ ├── 67-simulate-streaming-middleware.mdx │ │ │ │ ├── 68-default-settings-middleware.mdx │ │ │ │ ├── 75-simulate-readable-stream.mdx │ │ │ │ ├── 80-smooth-stream.mdx │ │ │ │ ├── 90-generate-id.mdx │ │ │ │ ├── 91-create-id-generator.mdx │ │ │ │ └── index.mdx │ │ │ ├── 02-ai-sdk-ui │ │ │ │ ├── 01-use-chat.mdx │ │ │ │ ├── 02-use-completion.mdx │ │ │ │ ├── 03-use-object.mdx │ │ │ │ ├── 31-convert-to-model-messages.mdx │ │ │ │ ├── 40-create-ui-message-stream.mdx │ │ │ │ ├── 41-create-ui-message-stream-response.mdx │ │ │ │ ├── 42-pipe-ui-message-stream-to-response.mdx │ │ │ │ ├── 43-read-ui-message-stream.mdx │ │ │ │ ├── 46-infer-ui-tools.mdx │ │ │ │ ├── 47-infer-ui-tool.mdx │ │ │ │ └── index.mdx │ │ │ ├── 03-ai-sdk-rsc │ │ │ │ ├── 01-stream-ui.mdx │ │ │ │ ├── 02-create-ai.mdx │ │ │ │ ├── 03-create-streamable-ui.mdx │ │ │ │ ├── 04-create-streamable-value.mdx │ │ │ │ ├── 05-read-streamable-value.mdx │ │ │ │ ├── 06-get-ai-state.mdx │ │ │ │ ├── 07-get-mutable-ai-state.mdx │ │ │ │ ├── 08-use-ai-state.mdx │ │ │ │ ├── 09-use-actions.mdx │ │ │ │ ├── 10-use-ui-state.mdx │ │ │ │ ├── 11-use-streamable-value.mdx │ │ │ │ ├── 20-render.mdx │ │ │ │ └── index.mdx │ │ │ ├── 04-stream-helpers │ │ │ │ ├── 01-ai-stream.mdx │ │ │ │ ├── 02-streaming-text-response.mdx │ │ │ │ ├── 05-stream-to-response.mdx │ │ │ │ ├── 07-openai-stream.mdx │ │ │ │ ├── 08-anthropic-stream.mdx │ │ │ │ ├── 09-aws-bedrock-stream.mdx │ │ │ │ ├── 10-aws-bedrock-anthropic-stream.mdx │ │ │ │ ├── 10-aws-bedrock-messages-stream.mdx │ │ │ │ ├── 11-aws-bedrock-cohere-stream.mdx │ │ │ │ ├── 12-aws-bedrock-llama-2-stream.mdx │ │ │ │ ├── 13-cohere-stream.mdx │ │ │ │ ├── 14-google-generative-ai-stream.mdx │ │ │ │ ├── 15-hugging-face-stream.mdx │ │ │ │ ├── 16-langchain-adapter.mdx │ │ │ │ ├── 16-llamaindex-adapter.mdx │ │ │ │ ├── 17-mistral-stream.mdx │ │ │ │ ├── 18-replicate-stream.mdx │ │ │ │ ├── 19-inkeep-stream.mdx │ │ │ │ └── index.mdx │ │ │ ├── 05-ai-sdk-errors │ │ │ │ ├── ai-api-call-error.mdx │ │ │ │ ├── ai-download-error.mdx │ │ │ │ ├── ai-empty-response-body-error.mdx │ │ │ │ ├── ai-invalid-argument-error.mdx │ │ │ │ ├── ai-invalid-data-content-error.mdx │ │ │ │ ├── ai-invalid-data-content.mdx │ │ │ │ ├── ai-invalid-message-role-error.mdx │ │ │ │ ├── ai-invalid-prompt-error.mdx │ │ │ │ ├── ai-invalid-response-data-error.mdx │ │ │ │ ├── ai-invalid-tool-arguments-error.mdx │ │ │ │ ├── ai-json-parse-error.mdx │ │ │ │ ├── ai-load-api-key-error.mdx │ │ │ │ ├── ai-load-setting-error.mdx │ │ │ │ ├── ai-message-conversion-error.mdx │ │ │ │ ├── ai-no-audio-generated-error.mdx │ │ │ │ ├── ai-no-content-generated-error.mdx │ │ │ │ ├── ai-no-image-generated-error.mdx │ │ │ │ ├── ai-no-object-generated-error.mdx │ │ │ │ ├── ai-no-output-specified-error.mdx │ │ │ │ ├── ai-no-such-model-error.mdx │ │ │ │ ├── ai-no-such-provider-error.mdx │ │ │ │ ├── ai-no-such-tool-error.mdx │ │ │ │ ├── ai-no-transcript-generated-error.mdx │ │ │ │ ├── ai-retry-error.mdx │ │ │ │ ├── ai-too-many-embedding-values-for-call-error.mdx │ │ │ │ ├── ai-tool-call-repair-error.mdx │ │ │ │ ├── ai-tool-execution-error.mdx │ │ │ │ ├── ai-type-validation-error.mdx │ │ │ │ ├── ai-unsupported-functionality-error.mdx │ │ │ │ └── index.mdx │ │ │ └── index.mdx │ │ ├── 08-migration-guides │ │ │ ├── 00-versioning.mdx │ │ │ ├── 26-migration-guide-5-0.mdx │ │ │ ├── 27-migration-guide-4-2.mdx │ │ │ ├── 28-migration-guide-4-1.mdx │ │ │ ├── 29-migration-guide-4-0.mdx │ │ │ ├── 36-migration-guide-3-4.mdx │ │ │ ├── 37-migration-guide-3-3.mdx │ │ │ ├── 38-migration-guide-3-2.mdx │ │ │ ├── 39-migration-guide-3-1.mdx │ │ │ └── index.mdx │ │ └── 09-troubleshooting │ │ ├── 01-azure-stream-slow.mdx │ │ ├── 02-client-side-function-calls-not-invoked.mdx │ │ ├── 03-server-actions-in-client-components.mdx │ │ ├── 04-strange-stream-output.mdx │ │ ├── 05-streamable-ui-errors.mdx │ │ ├── 05-tool-invocation-missing-result.mdx │ │ ├── 06-streaming-not-working-when-deployed.mdx │ │ ├── 06-streaming-not-working-when-proxied.mdx │ │ ├── 06-timeout-on-vercel.mdx │ │ ├── 07-unclosed-streams.mdx │ │ ├── 08-use-chat-failed-to-parse-stream.mdx │ │ ├── 09-client-stream-error.mdx │ │ ├── 10-use-chat-tools-no-response.mdx │ │ ├── 11-use-chat-custom-request-options.mdx │ │ ├── 12-use-chat-an-error-occurred.mdx │ │ ├── 13-repeated-assistant-messages.mdx │ │ ├── 14-stream-abort-handling.mdx │ │ ├── 15-stream-text-not-working.mdx │ │ ├── 16-streaming-status-delay.mdx │ │ ├── 30-model-is-not-assignable-to-type.mdx │ │ ├── 40-typescript-cannot-find-namespace-jsx.mdx │ │ ├── 50-react-maximum-update-depth-exceeded.mdx │ │ ├── 60-jest-cannot-find-module-ai-rsc.mdx │ │ └── index.mdx │ └── providers │ ├── 01-ai-sdk-providers │ │ ├── 00-ai-gateway.mdx │ │ ├── 01-xai.mdx │ │ ├── 02-vercel.mdx │ │ ├── 03-openai.mdx │ │ ├── 04-azure.mdx │ │ ├── 05-anthropic.mdx │ │ ├── 08-amazon-bedrock.mdx │ │ ├── 09-groq.mdx │ │ ├── 10-fal.mdx │ │ ├── 100-assemblyai.mdx │ │ ├── 11-deepinfra.mdx │ │ ├── 110-deepgram.mdx │ │ ├── 120-gladia.mdx │ │ ├── 140-lmnt.mdx │ │ ├── 15-google-generative-ai.mdx │ │ ├── 150-hume.mdx │ │ ├── 16-google-vertex.mdx │ │ ├── 160-revai.mdx │ │ ├── 20-mistral.mdx │ │ ├── 24-togetherai.mdx │ │ ├── 25-cohere.mdx │ │ ├── 26-fireworks.mdx │ │ ├── 30-deepseek.mdx │ │ ├── 40-cerebras.mdx │ │ ├── 60-replicate.mdx │ │ ├── 70-perplexity.mdx │ │ ├── 80-luma.mdx │ │ ├── 90-elevenlabs.mdx │ │ └── index.mdx │ ├── 02-openai-compatible-providers │ │ ├── 01-custom-providers.mdx │ │ ├── 30-lmstudio.mdx │ │ ├── 35-nim.mdx │ │ ├── 40-baseten.mdx │ │ └── index.mdx │ ├── 03-community-providers │ │ ├── 01-custom-providers.mdx │ │ ├── 02-qwen.mdx │ │ ├── 03-ollama.mdx │ │ ├── 05-a2a.mdx │ │ ├── 08-friendliai.mdx │ │ ├── 10-portkey.mdx │ │ ├── 100-built-in-ai.mdx │ │ ├── 100-gemini-cli.mdx │ │ ├── 11-cloudflare-workers-ai.mdx │ │ ├── 12-cloudflare-ai-gateway.mdx │ │ ├── 13-openrouter.mdx │ │ ├── 14-azure-ai.mdx │ │ ├── 20-sap-ai.mdx │ │ ├── 21-crosshatch.mdx │ │ ├── 5-requesty.mdx │ │ ├── 60-mixedbread.mdx │ │ ├── 61-voyage-ai.mdx │ │ ├── 70-mem0.mdx │ │ ├── 71-letta.mdx │ │ ├── 91-anthropic-vertex-ai.mdx │ │ ├── 92-spark.mdx │ │ ├── 93-inflection-ai.mdx │ │ ├── 94-langdb.mdx │ │ ├── 95-zhipu.mdx │ │ ├── 96-sambanova.mdx │ │ ├── 97-dify.mdx │ │ ├── 97-sarvam.mdx │ │ ├── 99-claude-code.mdx │ │ └── index.mdx │ ├── 04-adapters │ │ ├── 01-langchain.mdx │ │ ├── 02-llamaindex.mdx │ │ └── index.mdx │ └── 05-observability │ ├── braintrust.mdx │ ├── helicone.mdx │ ├── index.mdx │ ├── laminar.mdx │ ├── langfuse.mdx │ ├── langsmith.mdx │ ├── langwatch.mdx │ ├── maxim.mdx │ ├── patronus.mdx │ ├── signoz.mdx │ ├── traceloop.mdx │ └── weave.mdx ├── examples │ ├── ai-core │ │ ├── src │ │ │ ├── agent │ │ │ │ ├── openai-generate.ts │ │ │ │ ├── openai-stream-tools.ts │ │ │ │ └── openai-stream.ts │ │ │ ├── complex │ │ │ │ ├── math-agent │ │ │ │ │ ├── agent-required-tool-choice.ts │ │ │ │ │ └── agent.ts │ │ │ │ └── semantic-router │ │ │ │ ├── main.ts │ │ │ │ └── semantic-router.ts │ │ │ ├── e2e │ │ │ │ ├── cerebras.test.ts │ │ │ │ ├── cohere.test.ts │ │ │ │ ├── deepinfra.test.ts │ │ │ │ ├── deepseek.test.ts │ │ │ │ ├── feature-test-suite.ts │ │ │ │ ├── fireworks.test.ts │ │ │ │ ├── gateway.test.ts │ │ │ │ ├── google-vertex-anthropic.test.ts │ │ │ │ ├── google-vertex.test.ts │ │ │ │ ├── google.test.ts │ │ │ │ ├── groq.test.ts │ │ │ │ ├── luma.test.ts │ │ │ │ ├── openai.test.ts │ │ │ │ ├── perplexity.test.ts │ │ │ │ ├── raw-chunks.test.ts │ │ │ │ ├── togetherai.test.ts │ │ │ │ └── xai.test.ts │ │ │ ├── embed │ │ │ │ ├── amazon-bedrock.ts │ │ │ │ ├── azure.ts │ │ │ │ ├── cohere.ts │ │ │ │ ├── gateway.ts │ │ │ │ ├── google-vertex.ts │ │ │ │ ├── google.ts │ │ │ │ ├── mistral.ts │ │ │ │ ├── openai-compatible-togetherai.ts │ │ │ │ ├── openai.ts │ │ │ │ └── togetherai.ts │ │ │ ├── embed-many │ │ │ │ ├── amazon-bedrock.ts │ │ │ │ ├── azure.ts │ │ │ │ ├── cohere.ts │ │ │ │ ├── gateway.ts │ │ │ │ ├── google-vertex.ts │ │ │ │ ├── google.ts │ │ │ │ ├── mistral.ts │ │ │ │ ├── openai-compatible-togetherai.ts │ │ │ │ ├── openai-cosine-similarity.ts │ │ │ │ └── openai.ts │ │ │ ├── generate-image │ │ │ │ ├── amazon-bedrock.ts │ │ │ │ ├── azure.ts │ │ │ │ ├── deepinfra.ts │ │ │ │ ├── fal-kontext.ts │ │ │ │ ├── fal-photon.ts │ │ │ │ ├── fal-recraft.ts │ │ │ │ ├── fal.ts │ │ │ │ ├── fireworks.ts │ │ │ │ ├── google-vertex.ts │ │ │ │ ├── google.ts │ │ │ │ ├── luma-character-reference.ts │ │ │ │ ├── luma-image-reference.ts │ │ │ │ ├── luma-modify-image.ts │ │ │ │ ├── luma-style-reference.ts │ │ │ │ ├── luma.ts │ │ │ │ ├── openai-gpt-image.ts │ │ │ │ ├── openai-many.ts │ │ │ │ ├── openai.ts │ │ │ │ ├── replicate-1.ts │ │ │ │ ├── replicate-2.ts │ │ │ │ ├── replicate-3.ts │ │ │ │ ├── replicate-versioned.ts │ │ │ │ ├── togetherai.ts │ │ │ │ ├── xai-many.ts │ │ │ │ └── xai.ts │ │ │ ├── generate-object │ │ │ │ ├── amazon-bedrock.ts │ │ │ │ ├── anthropic.ts │ │ │ │ ├── azure.ts │ │ │ │ ├── cohere.ts │ │ │ │ ├── fireworks.ts │ │ │ │ ├── gateway.ts │ │ │ │ ├── google-caching.ts │ │ │ │ ├── google-complex-1.ts │ │ │ │ ├── google-complex-2.ts │ │ │ │ ├── google-enum.ts │ │ │ │ ├── google-gemini-files.ts │ │ │ │ ├── google-no-structured-output.ts │ │ │ │ ├── google-pdf-url.ts │ │ │ │ ├── google-vertex-anthropic.ts │ │ │ │ ├── google-vertex.ts │ │ │ │ ├── google.ts │ │ │ │ ├── groq-kimi-k2-structured-outputs.ts │ │ │ │ ├── groq.ts │ │ │ │ ├── mistral.ts │ │ │ │ ├── mock-error.ts │ │ │ │ ├── mock-repair-add-close.ts │ │ │ │ ├── mock.ts │ │ │ │ ├── nim.ts │ │ │ │ ├── openai-array.ts │ │ │ │ ├── openai-compatible-togetherai.ts │ │ │ │ ├── openai-date-parsing.ts │ │ │ │ ├── openai-enum.ts │ │ │ │ ├── openai-full-result.ts │ │ │ │ ├── openai-multimodal.ts │ │ │ │ ├── openai-no-schema.ts │ │ │ │ ├── openai-raw-json-schema.ts │ │ │ │ ├── openai-reasoning.ts │ │ │ │ ├── openai-request-body.ts │ │ │ │ ├── openai-responses.ts │ │ │ │ ├── openai-store-generation.ts │ │ │ │ ├── openai-structured-outputs-name-description.ts │ │ │ │ ├── openai-valibot.ts │ │ │ │ ├── openai.ts │ │ │ │ ├── perplexity.ts │ │ │ │ ├── togetherai.ts │ │ │ │ ├── vercel.ts │ │ │ │ ├── xai-structured-outputs-name-description.ts │ │ │ │ └── xai.ts │ │ │ ├── generate-speech │ │ │ │ ├── azure.ts │ │ │ │ ├── hume-instructions.ts │ │ │ │ ├── hume-language.ts │ │ │ │ ├── hume-speed.ts │ │ │ │ ├── hume-voice.ts │ │ │ │ ├── hume.ts │ │ │ │ ├── lmnt-language.ts │ │ │ │ ├── lmnt-speed.ts │ │ │ │ ├── lmnt-voice.ts │ │ │ │ ├── lmnt.ts │ │ │ │ ├── openai-instructions.ts │ │ │ │ ├── openai-language.ts │ │ │ │ ├── openai-speed.ts │ │ │ │ ├── openai-voice.ts │ │ │ │ └── openai.ts │ │ │ ├── generate-text │ │ │ │ ├── amazon-bedrock-api-key.ts │ │ │ │ ├── amazon-bedrock-cache-point-assistant.ts │ │ │ │ ├── amazon-bedrock-cache-point-system.ts │ │ │ │ ├── amazon-bedrock-cache-point-tool-call.ts │ │ │ │ ├── amazon-bedrock-cache-point-user-image.ts │ │ │ │ ├── amazon-bedrock-cache-point-user.ts │ │ │ │ ├── amazon-bedrock-chatbot.ts │ │ │ │ ├── amazon-bedrock-guardrails.ts │ │ │ │ ├── amazon-bedrock-image-url.ts │ │ │ │ ├── amazon-bedrock-image.ts │ │ │ │ ├── amazon-bedrock-nova-tool-call.ts │ │ │ │ ├── amazon-bedrock-prefilled-assistant-message.ts │ │ │ │ ├── amazon-bedrock-reasoning-chatbot.ts │ │ │ │ ├── amazon-bedrock-reasoning.ts │ │ │ │ ├── amazon-bedrock-tool-call-image-result.ts │ │ │ │ ├── amazon-bedrock-tool-call.ts │ │ │ │ ├── amazon-bedrock-tool-choice.ts │ │ │ │ ├── amazon-bedrock.ts │ │ │ │ ├── anthropic-cache-control-beta-1h-streaming.ts │ │ │ │ ├── anthropic-cache-control-beta-1h.ts │ │ │ │ ├── anthropic-cache-control.ts │ │ │ │ ├── anthropic-chatbot-websearch.ts │ │ │ │ ├── anthropic-chatbot.ts │ │ │ │ ├── anthropic-computer-use-bash.ts │ │ │ │ ├── anthropic-computer-use-computer.ts │ │ │ │ ├── anthropic-computer-use-editor-cache-control.ts │ │ │ │ ├── anthropic-computer-use-editor.ts │ │ │ │ ├── anthropic-custom-fetch.ts │ │ │ │ ├── anthropic-file-part-citations.ts │ │ │ │ ├── anthropic-full-result.ts │ │ │ │ ├── anthropic-image-url.ts │ │ │ │ ├── anthropic-image.ts │ │ │ │ ├── anthropic-pdf-url.ts │ │ │ │ ├── anthropic-pdf.ts │ │ │ │ ├── anthropic-provider-defined-tools.ts │ │ │ │ ├── anthropic-reasoning-chatbot.ts │ │ │ │ ├── anthropic-reasoning.ts │ │ │ │ ├── anthropic-search.ts │ │ │ │ ├── anthropic-text-citations.ts │ │ │ │ ├── anthropic-tool-call-cache.ts │ │ │ │ ├── anthropic-tool-call.ts │ │ │ │ ├── anthropic-tool-choice.ts │ │ │ │ ├── anthropic-web-search.ts │ │ │ │ ├── anthropic.ts │ │ │ │ ├── azure-custom-fetch.ts │ │ │ │ ├── azure-image.ts │ │ │ │ ├── azure-responses.ts │ │ │ │ ├── azure.ts │ │ │ │ ├── bedrock-consistent-file-names.ts │ │ │ │ ├── bedrock-document-support.ts │ │ │ │ ├── cerebras-tool-call.ts │ │ │ │ ├── cerebras.ts │ │ │ │ ├── cohere-chatbot.ts │ │ │ │ ├── cohere-citations.ts │ │ │ │ ├── cohere-tool-call-empty-params.ts │ │ │ │ ├── cohere-tool-call.ts │ │ │ │ ├── cohere.ts │ │ │ │ ├── deepinfra-tool-call.ts │ │ │ │ ├── deepinfra.ts │ │ │ │ ├── deepseek-cache-token.ts │ │ │ │ ├── deepseek-reasoning.ts │ │ │ │ ├── deepseek.ts │ │ │ │ ├── fireworks-deepseek.ts │ │ │ │ ├── fireworks-reasoning.ts │ │ │ │ ├── gateway-image-base64.ts │ │ │ │ ├── gateway-image-data-url.ts │ │ │ │ ├── gateway-image-url.ts │ │ │ │ ├── gateway-pdf.ts │ │ │ │ ├── gateway-tool-call.ts │ │ │ │ ├── gateway.ts │ │ │ │ ├── google-audio.ts │ │ │ │ ├── google-caching.ts │ │ │ │ ├── google-chatbot-image-output.ts │ │ │ │ ├── google-custom-fetch.ts │ │ │ │ ├── google-gemma-system-instructions.ts │ │ │ │ ├── google-image-output.ts │ │ │ │ ├── google-image-url.ts │ │ │ │ ├── google-image.ts │ │ │ │ ├── google-multi-step.ts │ │ │ │ ├── google-output-object.ts │ │ │ │ ├── google-pdf.ts │ │ │ │ ├── google-reasoning.ts │ │ │ │ ├── google-sources.ts │ │ │ │ ├── google-tool-call.ts │ │ │ │ ├── google-tool-choice.ts │ │ │ │ ├── google-url-context-wtih-google-search.ts │ │ │ │ ├── google-url-context.ts │ │ │ │ ├── google-vertex-anthropic-cache-control.ts │ │ │ │ ├── google-vertex-anthropic-chatbot.ts │ │ │ │ ├── google-vertex-anthropic-computer-use-bash.ts │ │ │ │ ├── google-vertex-anthropic-computer-use-computer.ts │ │ │ │ ├── google-vertex-anthropic-computer-use-editor-cache-control.ts │ │ │ │ ├── google-vertex-anthropic-computer-use-editor.ts │ │ │ │ ├── google-vertex-anthropic-custom-fetch.ts │ │ │ │ ├── google-vertex-anthropic-full-result.ts │ │ │ │ ├── google-vertex-anthropic-image-url.ts │ │ │ │ ├── google-vertex-anthropic-image.ts │ │ │ │ ├── google-vertex-anthropic-pdf.ts │ │ │ │ ├── google-vertex-anthropic-tool-call.ts │ │ │ │ ├── google-vertex-anthropic-tool-choice.ts │ │ │ │ ├── google-vertex-anthropic.ts │ │ │ │ ├── google-vertex-audio.ts │ │ │ │ ├── google-vertex-code-execution.ts │ │ │ │ ├── google-vertex-grounding.ts │ │ │ │ ├── google-vertex-image-base64.ts │ │ │ │ ├── google-vertex-image-url.ts │ │ │ │ ├── google-vertex-multi-step.ts │ │ │ │ ├── google-vertex-output-object.ts │ │ │ │ ├── google-vertex-pdf-url.ts │ │ │ │ ├── google-vertex-pdf.ts │ │ │ │ ├── google-vertex-reasoning-generate-text.ts │ │ │ │ ├── google-vertex-safety.ts │ │ │ │ ├── google-vertex-tool-call.ts │ │ │ │ ├── google-vertex.ts │ │ │ │ ├── google-youtube-url.ts │ │ │ │ ├── google.ts │ │ │ │ ├── groq-kimi-k2.ts │ │ │ │ ├── groq-reasoning.ts │ │ │ │ ├── groq.ts │ │ │ │ ├── mistral-chatbot.ts │ │ │ │ ├── mistral-custom-fetch.ts │ │ │ │ ├── mistral-full-result.ts │ │ │ │ ├── mistral-image-base64.ts │ │ │ │ ├── mistral-image-url.ts │ │ │ │ ├── mistral-medium.ts │ │ │ │ ├── mistral-pdf-url.ts │ │ │ │ ├── mistral-reasoning-raw.ts │ │ │ │ ├── mistral-tool-call.ts │ │ │ │ ├── mistral-tool-choice.ts │ │ │ │ ├── mistral.ts │ │ │ │ ├── mock-invalid-tool-call.ts │ │ │ │ ├── mock-tool-call-repair-reask.ts │ │ │ │ ├── mock-tool-call-repair-structured-model.ts │ │ │ │ ├── mock.ts │ │ │ │ ├── nim.ts │ │ │ │ ├── openai-active-tools.ts │ │ │ │ ├── openai-audio.ts │ │ │ │ ├── openai-cached-prompt-tokens.ts │ │ │ │ ├── openai-compatible-deepseek.ts │ │ │ │ ├── openai-compatible-litellm-anthropic-cache-control.ts │ │ │ │ ├── openai-compatible-openai-image.ts │ │ │ │ ├── openai-compatible-togetherai-tool-call.ts │ │ │ │ ├── openai-compatible-togetherai.ts │ │ │ │ ├── openai-completion-chat.ts │ │ │ │ ├── openai-completion.ts │ │ │ │ ├── openai-custom-fetch.ts │ │ │ │ ├── openai-custom-headers.ts │ │ │ │ ├── openai-dynamic-tool-call.ts │ │ │ │ ├── openai-full-result.ts │ │ │ │ ├── openai-image-base64.ts │ │ │ │ ├── openai-image-url.ts │ │ │ │ ├── openai-image.ts │ │ │ │ ├── openai-log-metadata-middleware.ts │ │ │ │ ├── openai-logprobs.ts │ │ │ │ ├── openai-multi-step.ts │ │ │ │ ├── openai-nullable.ts │ │ │ │ ├── openai-output-object.ts │ │ │ │ ├── openai-pdf-url.ts │ │ │ │ ├── openai-pdf.ts │ │ │ │ ├── openai-provider-defined-tools.ts │ │ │ │ ├── openai-reasoning.ts │ │ │ │ ├── openai-request-body.ts │ │ │ │ ├── openai-responses-chatbot.ts │ │ │ │ ├── openai-responses-image-url.ts │ │ │ │ ├── openai-responses-image.ts │ │ │ │ ├── openai-responses-output-object.ts │ │ │ │ ├── openai-responses-pdf-url.ts │ │ │ │ ├── openai-responses-pdf.ts │ │ │ │ ├── openai-responses-previous-response-id.ts │ │ │ │ ├── openai-responses-reasoning-summary.ts │ │ │ │ ├── openai-responses-reasoning-zero-data-retention.ts │ │ │ │ ├── openai-responses-reasoning.ts │ │ │ │ ├── openai-responses-roundtrip-server-side-tools.ts │ │ │ │ ├── openai-responses-tool-call.ts │ │ │ │ ├── openai-responses-websearch.ts │ │ │ │ ├── openai-responses.ts │ │ │ │ ├── openai-store-generation.ts │ │ │ │ ├── openai-system-message-a.ts │ │ │ │ ├── openai-system-message-b.ts │ │ │ │ ├── openai-timeout.ts │ │ │ │ ├── openai-tool-call-raw-json-schema.ts │ │ │ │ ├── openai-tool-call-with-context.ts │ │ │ │ ├── openai-tool-call.ts │ │ │ │ ├── openai-tool-choice.ts │ │ │ │ ├── openai-tool-execution-error.ts │ │ │ │ ├── openai.ts │ │ │ │ ├── perplexity-images.ts │ │ │ │ ├── perplexity.ts │ │ │ │ ├── provider-defined-tools-working.ts │ │ │ │ ├── togetherai-tool-call.ts │ │ │ │ ├── togetherai.ts │ │ │ │ ├── vercel-image.ts │ │ │ │ ├── vercel.ts │ │ │ │ ├── xai-search.ts │ │ │ │ ├── xai-structured-output.ts │ │ │ │ ├── xai-tool-call.ts │ │ │ │ └── xai.ts │ │ │ ├── lib │ │ │ │ ├── present-image.ts │ │ │ │ └── save-audio.ts │ │ │ ├── middleware │ │ │ │ ├── add-to-last-user-message.ts │ │ │ │ ├── default-settings-example.ts │ │ │ │ ├── generate-text-cache-middleware-example.ts │ │ │ │ ├── generate-text-log-middleware-example.ts │ │ │ │ ├── get-last-user-message-text.ts │ │ │ │ ├── simulate-streaming-example.ts │ │ │ │ ├── stream-text-log-middleware.ts │ │ │ │ ├── stream-text-rag-middleware.ts │ │ │ │ ├── your-cache-middleware.ts │ │ │ │ ├── your-guardrail-middleware.ts │ │ │ │ ├── your-log-middleware.ts │ │ │ │ └── your-rag-middleware.ts │ │ │ ├── registry │ │ │ │ ├── embed-openai.ts │ │ │ │ ├── generate-image.ts │ │ │ │ ├── generate-speech-openai.ts │ │ │ │ ├── setup-registry.ts │ │ │ │ ├── stream-text-anthropic.ts │ │ │ │ ├── stream-text-groq.ts │ │ │ │ ├── stream-text-openai.ts │ │ │ │ ├── stream-text-xai.ts │ │ │ │ └── transcribe-openai.ts │ │ │ ├── stream-object │ │ │ │ ├── amazon-bedrock.ts │ │ │ │ ├── anthropic.ts │ │ │ │ ├── azure.ts │ │ │ │ ├── fireworks.ts │ │ │ │ ├── gateway.ts │ │ │ │ ├── google-caching.ts │ │ │ │ ├── google-vertex-anthropic.ts │ │ │ │ ├── google-vertex.ts │ │ │ │ ├── google.ts │ │ │ │ ├── groq.ts │ │ │ │ ├── mistral.ts │ │ │ │ ├── mock.ts │ │ │ │ ├── nim.ts │ │ │ │ ├── openai-array.ts │ │ │ │ ├── openai-compatible-togetherai.ts │ │ │ │ ├── openai-fullstream.ts │ │ │ │ ├── openai-no-schema.ts │ │ │ │ ├── openai-object.ts │ │ │ │ ├── openai-on-finish.ts │ │ │ │ ├── openai-raw-json-schema.ts │ │ │ │ ├── openai-reasoning.ts │ │ │ │ ├── openai-request-body.ts │ │ │ │ ├── openai-responses.ts │ │ │ │ ├── openai-store-generation.ts │ │ │ │ ├── openai-stream-object-name-description.ts │ │ │ │ ├── openai-stream-object.ts │ │ │ │ ├── openai-token-usage.ts │ │ │ │ ├── openai-unstructured-output.ts │ │ │ │ ├── openai.ts │ │ │ │ ├── togetherai.ts │ │ │ │ ├── vercel.ts │ │ │ │ ├── xai-structured-outputs-name-description.ts │ │ │ │ └── xai.ts │ │ │ ├── stream-text │ │ │ │ ├── amazon-bedrock-activetools.ts │ │ │ │ ├── amazon-bedrock-anthropic-bash.ts │ │ │ │ ├── amazon-bedrock-anthropic-websearch.ts │ │ │ │ ├── amazon-bedrock-cache-point-assistant.ts │ │ │ │ ├── amazon-bedrock-cache-point-image.ts │ │ │ │ ├── amazon-bedrock-cache-point-system.ts │ │ │ │ ├── amazon-bedrock-cache-point-tool-call.ts │ │ │ │ ├── amazon-bedrock-cache-point-user.ts │ │ │ │ ├── amazon-bedrock-chatbot.ts │ │ │ │ ├── amazon-bedrock-fullstream.ts │ │ │ │ ├── amazon-bedrock-image.ts │ │ │ │ ├── amazon-bedrock-pdf.ts │ │ │ │ ├── amazon-bedrock-reasoning-chatbot.ts │ │ │ │ ├── amazon-bedrock-reasoning-fullstream.ts │ │ │ │ ├── amazon-bedrock-reasoning.ts │ │ │ │ ├── amazon-bedrock-tool-call.ts │ │ │ │ ├── amazon-bedrock.ts │ │ │ │ ├── anthropic-cache-control.ts │ │ │ │ ├── anthropic-chatbot.ts │ │ │ │ ├── anthropic-disable-parallel-tools.ts │ │ │ │ ├── anthropic-fullstream.ts │ │ │ │ ├── anthropic-image.ts │ │ │ │ ├── anthropic-on-chunk-raw.ts │ │ │ │ ├── anthropic-pdf-sources.ts │ │ │ │ ├── anthropic-pdf.ts │ │ │ │ ├── anthropic-reasoning-chatbot.ts │ │ │ │ ├── anthropic-reasoning-fullstream.ts │ │ │ │ ├── anthropic-reasoning.ts │ │ │ │ ├── anthropic-search.ts │ │ │ │ ├── anthropic-smooth.ts │ │ │ │ ├── anthropic-text-citations.ts │ │ │ │ ├── anthropic-web-search.ts │ │ │ │ ├── anthropic.ts │ │ │ │ ├── azure-completion.ts │ │ │ │ ├── azure-fullstream-logprobs.ts │ │ │ │ ├── azure-fullstream.ts │ │ │ │ ├── azure-smooth-line.ts │ │ │ │ ├── azure-smooth.ts │ │ │ │ ├── azure.ts │ │ │ │ ├── baseten.ts │ │ │ │ ├── cerebras-tool-call.ts │ │ │ │ ├── cerebras.ts │ │ │ │ ├── cohere-chatbot.ts │ │ │ │ ├── cohere-raw-chunks.ts │ │ │ │ ├── cohere-response.ts │ │ │ │ ├── cohere-tool-call-empty-params.ts │ │ │ │ ├── cohere-tool-call.ts │ │ │ │ ├── cohere.ts │ │ │ │ ├── deepseek-cache-token.ts │ │ │ │ ├── deepseek-reasoning.ts │ │ │ │ ├── deepseek-tool-call.ts │ │ │ │ ├── deepseek.ts │ │ │ │ ├── fireworks-deepseek.ts │ │ │ │ ├── fireworks-kimi-k2-tool-call.ts │ │ │ │ ├── fireworks-kimi-k2.ts │ │ │ │ ├── fireworks-reasoning.ts │ │ │ │ ├── fireworks.ts │ │ │ │ ├── gateway-auth.ts │ │ │ │ ├── gateway-pdf.ts │ │ │ │ ├── gateway-provider-options-order.ts │ │ │ │ ├── gateway.ts │ │ │ │ ├── google-caching.ts │ │ │ │ ├── google-chatbot-image-output.ts │ │ │ │ ├── google-chatbot.ts │ │ │ │ ├── google-fullstream.ts │ │ │ │ ├── google-gemma-system-instructions.ts │ │ │ │ ├── google-grounding.ts │ │ │ │ ├── google-image-output.ts │ │ │ │ ├── google-raw-chunks.ts │ │ │ │ ├── google-reasoning-with-tools.ts │ │ │ │ ├── google-reasoning.ts │ │ │ │ ├── google-url-context.ts │ │ │ │ ├── google-vertex-anthropic-cache-control.ts │ │ │ │ ├── google-vertex-anthropic-chatbot.ts │ │ │ │ ├── google-vertex-anthropic-fullstream.ts │ │ │ │ ├── google-vertex-anthropic-image-url.ts │ │ │ │ ├── google-vertex-anthropic-image.ts │ │ │ │ ├── google-vertex-anthropic-pdf.ts │ │ │ │ ├── google-vertex-anthropic-tool-call.ts │ │ │ │ ├── google-vertex-anthropic.ts │ │ │ │ ├── google-vertex-code-execution.ts │ │ │ │ ├── google-vertex-fullstream.ts │ │ │ │ ├── google-vertex-grounding.ts │ │ │ │ ├── google-vertex-pdf-url.ts │ │ │ │ ├── google-vertex-reasoning.ts │ │ │ │ ├── google-vertex.ts │ │ │ │ ├── google-youtube-url.ts │ │ │ │ ├── google.ts │ │ │ │ ├── groq-kimi-k2-tool-call.ts │ │ │ │ ├── groq-kimi-k2.ts │ │ │ │ ├── groq-openai-oss.ts │ │ │ │ ├── groq-raw-chunks.ts │ │ │ │ ├── groq-reasoning-fullstream.ts │ │ │ │ ├── groq.ts │ │ │ │ ├── lmstudio.ts │ │ │ │ ├── mistral-chatbot.ts │ │ │ │ ├── mistral-fullstream.ts │ │ │ │ ├── mistral-raw-chunks.ts │ │ │ │ ├── mistral-reasoning-raw.ts │ │ │ │ ├── mistral.ts │ │ │ │ ├── mock.ts │ │ │ │ ├── nim.ts │ │ │ │ ├── openai-abort.ts │ │ │ │ ├── openai-audio.ts │ │ │ │ ├── openai-cached-prompt-tokens.ts │ │ │ │ ├── openai-chatbot.ts │ │ │ │ ├── openai-compatible-deepseek.ts │ │ │ │ ├── openai-compatible-litellm-anthropic-cache-control.ts │ │ │ │ ├── openai-compatible-raw-chunks.ts │ │ │ │ ├── openai-compatible-togetherai-tool-call.ts │ │ │ │ ├── openai-compatible-togetherai.ts │ │ │ │ ├── openai-completion-chat.ts │ │ │ │ ├── openai-completion.ts │ │ │ │ ├── openai-custom-fetch-inject-error.ts │ │ │ │ ├── openai-dynamic-tool-call.ts │ │ │ │ ├── openai-flex-processing.ts │ │ │ │ ├── openai-fullstream-logprobs.ts │ │ │ │ ├── openai-fullstream-raw.ts │ │ │ │ ├── openai-fullstream.ts │ │ │ │ ├── openai-global-provider.ts │ │ │ │ ├── openai-multi-step.ts │ │ │ │ ├── openai-on-chunk-tool-call-streaming.ts │ │ │ │ ├── openai-on-chunk.ts │ │ │ │ ├── openai-on-finish-response-messages.ts │ │ │ │ ├── openai-on-finish-steps.ts │ │ │ │ ├── openai-on-finish.ts │ │ │ │ ├── openai-on-step-finish.ts │ │ │ │ ├── openai-output-object.ts │ │ │ │ ├── openai-predicted-output.ts │ │ │ │ ├── openai-prepare-step.ts │ │ │ │ ├── openai-read-ui-message-stream.ts │ │ │ │ ├── openai-reader.ts │ │ │ │ ├── openai-reasoning.ts │ │ │ │ ├── openai-request-body.ts │ │ │ │ ├── openai-responses-chatbot.ts │ │ │ │ ├── openai-responses-raw-chunks.ts │ │ │ │ ├── openai-responses-reasoning-chatbot.ts │ │ │ │ ├── openai-responses-reasoning-summary.ts │ │ │ │ ├── openai-responses-reasoning-tool-call.ts │ │ │ │ ├── openai-responses-reasoning-zero-data-retention.ts │ │ │ │ ├── openai-responses-reasoning.ts │ │ │ │ ├── openai-responses-tool-call.ts │ │ │ │ ├── openai-responses-websearch.ts │ │ │ │ ├── openai-responses.ts │ │ │ │ ├── openai-store-generation.ts │ │ │ │ ├── openai-swarm.ts │ │ │ │ ├── openai-tool-abort.ts │ │ │ │ ├── openai-tool-call-raw-json-schema.ts │ │ │ │ ├── openai-tool-call.ts │ │ │ │ ├── openai-web-search-tool.ts │ │ │ │ ├── openai.ts │ │ │ │ ├── perplexity-images.ts │ │ │ │ ├── perplexity-raw-chunks.ts │ │ │ │ ├── perplexity.ts │ │ │ │ ├── raw-chunks.ts │ │ │ │ ├── smooth-stream-chinese.ts │ │ │ │ ├── smooth-stream-japanese.ts │ │ │ │ ├── togetherai-tool-call.ts │ │ │ │ ├── togetherai.ts │ │ │ │ ├── vercel-image.ts │ │ │ │ ├── vercel-tool-call.ts │ │ │ │ ├── vercel.ts │ │ │ │ ├── xai-chatbot.ts │ │ │ │ ├── xai-image.ts │ │ │ │ ├── xai-raw-chunks.ts │ │ │ │ ├── xai-search.ts │ │ │ │ ├── xai-tool-call.ts │ │ │ │ └── xai.ts │ │ │ ├── telemetry │ │ │ │ ├── generate-object.ts │ │ │ │ ├── generate-text-tool-call.ts │ │ │ │ ├── generate-text.ts │ │ │ │ ├── stream-object.ts │ │ │ │ └── stream-text.ts │ │ │ ├── test │ │ │ │ └── response-format.ts │ │ │ ├── tools │ │ │ │ └── weather-tool.ts │ │ │ ├── transcribe │ │ │ │ ├── assemblyai-string.ts │ │ │ │ ├── assemblyai-url.ts │ │ │ │ ├── assemblyai.ts │ │ │ │ ├── azure-string.ts │ │ │ │ ├── azure-url.ts │ │ │ │ ├── azure.ts │ │ │ │ ├── deepgram-string.ts │ │ │ │ ├── deepgram-url.ts │ │ │ │ ├── deepgram.ts │ │ │ │ ├── elevenlabs-string.ts │ │ │ │ ├── elevenlabs-url.ts │ │ │ │ ├── elevenlabs.ts │ │ │ │ ├── fal-string.ts │ │ │ │ ├── fal-url.ts │ │ │ │ ├── fal.ts │ │ │ │ ├── gladia-string.ts │ │ │ │ ├── gladia-url.ts │ │ │ │ ├── gladia.ts │ │ │ │ ├── groq-string.ts │ │ │ │ ├── groq-url.ts │ │ │ │ ├── groq.ts │ │ │ │ ├── openai-string.ts │ │ │ │ ├── openai-url.ts │ │ │ │ ├── openai.ts │ │ │ │ ├── revai-string.ts │ │ │ │ ├── revai-url.ts │ │ │ │ └── revai.ts │ │ │ └── types │ │ │ └── tool-set.ts │ │ └── vitest.config.js │ ├── angular │ │ └── src │ │ ├── app │ │ │ ├── chat │ │ │ │ └── chat.component.ts │ │ │ ├── completion │ │ │ │ └── completion.component.ts │ │ │ ├── structured-object │ │ │ │ └── structured-object.component.ts │ │ │ ├── app.component.ts │ │ │ ├── app.config.ts │ │ │ └── app.routes.ts │ │ ├── main.ts │ │ └── server.ts │ ├── express │ │ └── src │ │ └── server.ts │ ├── fastify │ │ └── src │ │ └── server.ts │ ├── hono │ │ └── src │ │ ├── hono-streaming.ts │ │ └── server.ts │ ├── mcp │ │ └── src │ │ ├── http │ │ │ ├── client.ts │ │ │ └── server.ts │ │ ├── sse │ │ │ ├── client.ts │ │ │ └── server.ts │ │ └── stdio │ │ ├── client.ts │ │ └── server.ts │ ├── nest │ │ ├── src │ │ │ ├── app.controller.ts │ │ │ ├── app.module.ts │ │ │ └── main.ts │ │ └── .eslintrc.js │ ├── next │ │ ├── app │ │ │ ├── api │ │ │ │ └── chat │ │ │ │ ├── [id] │ │ │ │ │ └── stream │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── [chatId] │ │ │ │ ├── chat-input.tsx │ │ │ │ ├── chat.tsx │ │ │ │ ├── message.tsx │ │ │ │ └── page.tsx │ │ │ ├── actions.ts │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── util │ │ │ ├── chat-schema.ts │ │ │ └── chat-store.ts │ │ ├── next.config.js │ │ ├── postcss.config.js │ │ └── tailwind.config.js │ ├── next-fastapi │ │ ├── app │ │ │ ├── (examples) │ │ │ │ ├── 01-chat-text │ │ │ │ │ ├── layout.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── 02-chat-data │ │ │ │ │ ├── layout.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── 03-chat-attachments │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ │ ├── components.tsx │ │ │ ├── icons.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── next.config.js │ │ ├── postcss.config.js │ │ └── tailwind.config.js │ ├── next-google-vertex │ │ ├── app │ │ │ ├── api │ │ │ │ ├── generate-edge │ │ │ │ │ └── route.ts │ │ │ │ └── generate-node │ │ │ │ └── route.ts │ │ │ ├── edge │ │ │ │ └── page.tsx │ │ │ ├── node │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── next.config.js │ │ └── tailwind.config.ts │ ├── next-langchain │ │ ├── app │ │ │ ├── api │ │ │ │ ├── chat │ │ │ │ │ └── route.ts │ │ │ │ ├── completion │ │ │ │ │ └── route.ts │ │ │ │ └── completion-string-output-parser │ │ │ │ └── route.ts │ │ │ ├── completion │ │ │ │ └── page.tsx │ │ │ ├── completion-string-output-parser │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── next.config.js │ │ ├── postcss.config.js │ │ └── tailwind.config.js │ ├── next-openai │ │ ├── app │ │ │ ├── api │ │ │ │ ├── bedrock │ │ │ │ │ └── route.ts │ │ │ │ ├── chat │ │ │ │ │ └── route.ts │ │ │ │ ├── chat-cohere │ │ │ │ │ └── route.ts │ │ │ │ ├── chat-google │ │ │ │ │ └── route.ts │ │ │ │ ├── chat-groq │ │ │ │ │ └── route.ts │ │ │ │ ├── chat-mistral │ │ │ │ │ └── route.ts │ │ │ │ ├── chat-openai-file-search │ │ │ │ │ └── route.ts │ │ │ │ ├── chat-openai-responses │ │ │ │ │ └── route.ts │ │ │ │ ├── chat-openai-web-search │ │ │ │ │ └── route.ts │ │ │ │ ├── chat-perplexity │ │ │ │ │ └── route.ts │ │ │ │ ├── chat-xai │ │ │ │ │ └── route.ts │ │ │ │ ├── completion │ │ │ │ │ └── route.ts │ │ │ │ ├── dynamic-tools │ │ │ │ │ └── route.ts │ │ │ │ ├── files │ │ │ │ │ └── route.ts │ │ │ │ ├── generate-image │ │ │ │ │ └── route.ts │ │ │ │ ├── mcp-zapier │ │ │ │ │ └── route.ts │ │ │ │ ├── test-invalid-tool-call │ │ │ │ │ └── route.ts │ │ │ │ ├── use-chat-cache │ │ │ │ │ └── route.ts │ │ │ │ ├── use-chat-custom-sources │ │ │ │ │ └── route.ts │ │ │ │ ├── use-chat-data-ui-parts │ │ │ │ │ └── route.ts │ │ │ │ ├── use-chat-human-in-the-loop │ │ │ │ │ ├── route.ts │ │ │ │ │ ├── tools.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── use-chat-image-output │ │ │ │ │ └── route.ts │ │ │ │ ├── use-chat-message-metadata │ │ │ │ │ ├── example-metadata-schema.ts │ │ │ │ │ └── route.ts │ │ │ │ ├── use-chat-persistence │ │ │ │ │ └── route.ts │ │ │ │ ├── use-chat-persistence-metadata │ │ │ │ │ └── route.ts │ │ │ │ ├── use-chat-persistence-single-message │ │ │ │ │ └── route.ts │ │ │ │ ├── use-chat-reasoning │ │ │ │ │ └── route.ts │ │ │ │ ├── use-chat-reasoning-tools │ │ │ │ │ └── route.ts │ │ │ │ ├── use-chat-resilient-persistence │ │ │ │ │ └── route.ts │ │ │ │ ├── use-chat-resume │ │ │ │ │ ├── [id] │ │ │ │ │ │ └── stream │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ ├── use-chat-sources │ │ │ │ │ └── route.ts │ │ │ │ ├── use-chat-streaming-tool-calls │ │ │ │ │ └── route.ts │ │ │ │ ├── use-chat-throttle │ │ │ │ │ └── route.ts │ │ │ │ ├── use-chat-tools │ │ │ │ │ └── route.ts │ │ │ │ ├── use-completion-server-side-multi-step │ │ │ │ │ └── route.ts │ │ │ │ ├── use-completion-throttle │ │ │ │ │ └── route.ts │ │ │ │ ├── use-object │ │ │ │ │ ├── route.ts │ │ │ │ │ └── schema.ts │ │ │ │ └── use-object-expense-tracker │ │ │ │ ├── route.ts │ │ │ │ └── schema.ts │ │ │ ├── bedrock │ │ │ │ └── page.tsx │ │ │ ├── completion │ │ │ │ └── page.tsx │ │ │ ├── completion-rsc │ │ │ │ ├── generate-completion.ts │ │ │ │ └── page.tsx │ │ │ ├── dynamic-tools │ │ │ │ └── page.tsx │ │ │ ├── generate-image │ │ │ │ └── page.tsx │ │ │ ├── mcp │ │ │ │ ├── chat │ │ │ │ │ └── route.ts │ │ │ │ ├── server │ │ │ │ │ └── route.ts │ │ │ │ └── page.tsx │ │ │ ├── mcp-zapier │ │ │ │ └── page.tsx │ │ │ ├── stream-object │ │ │ │ ├── actions.ts │ │ │ │ ├── page.tsx │ │ │ │ └── schema.ts │ │ │ ├── stream-ui │ │ │ │ ├── actions.tsx │ │ │ │ ├── ai.ts │ │ │ │ ├── layout.tsx │ │ │ │ ├── message.tsx │ │ │ │ └── page.tsx │ │ │ ├── test-cohere │ │ │ │ └── page.tsx │ │ │ ├── test-google │ │ │ │ └── page.tsx │ │ │ ├── test-groq │ │ │ │ └── page.tsx │ │ │ ├── test-invalid-tool-call │ │ │ │ └── page.tsx │ │ │ ├── test-mistral │ │ │ │ └── page.tsx │ │ │ ├── test-openai-file-search │ │ │ │ └── page.tsx │ │ │ ├── test-openai-responses │ │ │ │ └── page.tsx │ │ │ ├── test-openai-web-search │ │ │ │ └── page.tsx │ │ │ ├── test-perplexity │ │ │ │ └── page.tsx │ │ │ ├── test-xai │ │ │ │ └── page.tsx │ │ │ ├── use-chat-attachments │ │ │ │ └── page.tsx │ │ │ ├── use-chat-attachments-append │ │ │ │ └── page.tsx │ │ │ ├── use-chat-attachments-url │ │ │ │ └── page.tsx │ │ │ ├── use-chat-custom-sources │ │ │ │ └── page.tsx │ │ │ ├── use-chat-data-ui-parts │ │ │ │ └── page.tsx │ │ │ ├── use-chat-human-in-the-loop │ │ │ │ └── page.tsx │ │ │ ├── use-chat-image-output │ │ │ │ └── page.tsx │ │ │ ├── use-chat-message-metadata │ │ │ │ └── page.tsx │ │ │ ├── use-chat-persistence │ │ │ │ ├── [id] │ │ │ │ │ ├── chat.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── use-chat-persistence-metadata │ │ │ │ ├── [chatId] │ │ │ │ │ ├── chat.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── use-chat-persistence-single-message │ │ │ │ ├── [id] │ │ │ │ │ ├── chat.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── use-chat-reasoning │ │ │ │ └── page.tsx │ │ │ ├── use-chat-reasoning-tools │ │ │ │ └── page.tsx │ │ │ ├── use-chat-resilient-persistence │ │ │ │ ├── [id] │ │ │ │ │ ├── chat.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── use-chat-resume │ │ │ │ ├── [id] │ │ │ │ │ └── page.tsx │ │ │ │ ├── chat.tsx │ │ │ │ └── page.tsx │ │ │ ├── use-chat-sources │ │ │ │ └── page.tsx │ │ │ ├── use-chat-streaming-tool-calls │ │ │ │ └── page.tsx │ │ │ ├── use-chat-throttle │ │ │ │ └── page.tsx │ │ │ ├── use-chat-tools │ │ │ │ └── page.tsx │ │ │ ├── use-completion-server-side-multi-step │ │ │ │ └── page.tsx │ │ │ ├── use-completion-throttle │ │ │ │ └── page.tsx │ │ │ ├── use-object │ │ │ │ └── page.tsx │ │ │ ├── use-object-expense-tracker │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── component │ │ │ └── chat-input.tsx │ │ ├── util │ │ │ ├── mcp │ │ │ │ ├── handler.ts │ │ │ │ ├── incoming-message.ts │ │ │ │ └── server-response.ts │ │ │ └── chat-store.ts │ │ ├── next.config.js │ │ ├── postcss.config.js │ │ └── tailwind.config.js │ ├── next-openai-kasada-bot-protection │ │ ├── app │ │ │ ├── 149e9513-01fa-4fb0-aad4-566afd725d1b │ │ │ │ └── 2d206a39-8ed7-437e-a3be-862e0f06eea3 │ │ │ │ └── [[...restpath]] │ │ │ │ └── route.ts │ │ │ ├── api │ │ │ │ └── chat │ │ │ │ └── route.ts │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── toaster.tsx │ │ ├── kasada │ │ │ ├── kasada-client.tsx │ │ │ └── kasada-server.tsx │ │ ├── middleware.ts │ │ ├── next.config.js │ │ ├── postcss.config.js │ │ └── tailwind.config.js │ ├── next-openai-pages │ │ ├── app │ │ │ └── api │ │ │ ├── call-tool │ │ │ │ └── route.ts │ │ │ ├── generate-chat │ │ │ │ └── route.ts │ │ │ ├── generate-object │ │ │ │ └── route.ts │ │ │ ├── generate-text │ │ │ │ └── route.ts │ │ │ ├── stream-chat │ │ │ │ └── route.ts │ │ │ ├── stream-object │ │ │ │ └── route.ts │ │ │ └── stream-text │ │ │ └── route.ts │ │ ├── pages │ │ │ ├── api │ │ │ │ ├── chat-api-route.ts │ │ │ │ └── chat-edge.ts │ │ │ ├── basics │ │ │ │ ├── generate-object │ │ │ │ │ └── index.tsx │ │ │ │ ├── generate-text │ │ │ │ │ └── index.tsx │ │ │ │ ├── stream-object │ │ │ │ │ └── index.tsx │ │ │ │ └── stream-text │ │ │ │ └── index.tsx │ │ │ ├── chat │ │ │ │ ├── generate-chat │ │ │ │ │ └── index.tsx │ │ │ │ ├── stream-chat │ │ │ │ │ └── index.tsx │ │ │ │ ├── stream-chat-api-route │ │ │ │ │ └── index.tsx │ │ │ │ └── stream-chat-edge │ │ │ │ └── index.tsx │ │ │ ├── tools │ │ │ │ └── call-tool │ │ │ │ └── index.tsx │ │ │ ├── _app.tsx │ │ │ ├── _document.tsx │ │ │ └── index.tsx │ │ ├── next.config.js │ │ ├── postcss.config.js │ │ └── tailwind.config.js │ ├── next-openai-telemetry │ │ ├── app │ │ │ ├── api │ │ │ │ └── text │ │ │ │ └── route.ts │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── instrumentation.ts │ │ ├── next.config.js │ │ ├── postcss.config.js │ │ └── tailwind.config.js │ ├── next-openai-telemetry-sentry │ │ ├── app │ │ │ ├── api │ │ │ │ └── text │ │ │ │ └── route.ts │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── instrumentation.ts │ │ ├── next.config.js │ │ ├── postcss.config.js │ │ ├── sentry.client.config.js │ │ ├── sentry.edge.config.js │ │ ├── sentry.server.config.js │ │ └── tailwind.config.js │ ├── next-openai-upstash-rate-limits │ │ ├── app │ │ │ ├── api │ │ │ │ └── chat │ │ │ │ └── route.ts │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── toaster.tsx │ │ ├── next.config.js │ │ ├── postcss.config.js │ │ └── tailwind.config.js │ ├── node-http-server │ │ └── src │ │ └── server.ts │ ├── nuxt-openai │ │ ├── server │ │ │ └── api │ │ │ ├── chat-with-vision.ts │ │ │ ├── chat.ts │ │ │ ├── completion.ts │ │ │ ├── use-chat-request.ts │ │ │ └── use-chat-tools.ts │ │ └── nuxt.config.ts │ └── sveltekit-openai │ ├── src │ │ ├── lib │ │ │ ├── components │ │ │ │ └── ui │ │ │ │ ├── button │ │ │ │ │ └── index.ts │ │ │ │ └── textarea │ │ │ │ └── index.ts │ │ │ └── utils.ts │ │ ├── routes │ │ │ ├── api │ │ │ │ ├── chat │ │ │ │ │ └── +server.ts │ │ │ │ ├── completion │ │ │ │ │ └── +server.ts │ │ │ │ └── structured-object │ │ │ │ └── +server.ts │ │ │ └── structured-object │ │ │ └── schema.ts │ │ └── app.d.ts │ ├── eslint.config.js │ ├── postcss.config.js │ ├── svelte.config.js │ ├── tailwind.config.ts │ └── vite.config.ts ├── packages │ ├── ai │ │ ├── internal │ │ │ └── index.ts │ │ ├── mcp-stdio │ │ │ ├── create-child-process.test.ts │ │ │ ├── create-child-process.ts │ │ │ ├── get-environment.test.ts │ │ │ ├── get-environment.ts │ │ │ ├── index.ts │ │ │ ├── mcp-stdio-transport.test.ts │ │ │ └── mcp-stdio-transport.ts │ │ ├── src │ │ │ ├── agent │ │ │ │ ├── agent.ts │ │ │ │ └── index.ts │ │ │ ├── embed │ │ │ │ ├── embed-many-result.ts │ │ │ │ ├── embed-many.test.ts │ │ │ │ ├── embed-many.ts │ │ │ │ ├── embed-result.ts │ │ │ │ ├── embed.test.ts │ │ │ │ ├── embed.ts │ │ │ │ └── index.ts │ │ │ ├── error │ │ │ │ ├── index.ts │ │ │ │ ├── invalid-argument-error.ts │ │ │ │ ├── invalid-stream-part-error.ts │ │ │ │ ├── invalid-tool-input-error.ts │ │ │ │ ├── mcp-client-error.ts │ │ │ │ ├── no-image-generated-error.ts │ │ │ │ ├── no-object-generated-error.ts │ │ │ │ ├── no-output-specified-error.ts │ │ │ │ ├── no-speech-generated-error.ts │ │ │ │ ├── no-such-tool-error.ts │ │ │ │ ├── no-transcript-generated-error.ts │ │ │ │ ├── tool-call-repair-error.ts │ │ │ │ └── unsupported-model-version-error.ts │ │ │ ├── generate-image │ │ │ │ ├── generate-image-result.ts │ │ │ │ ├── generate-image.test.ts │ │ │ │ ├── generate-image.ts │ │ │ │ └── index.ts │ │ │ ├── generate-object │ │ │ │ ├── generate-object-result.ts │ │ │ │ ├── generate-object.test-d.ts │ │ │ │ ├── generate-object.test.ts │ │ │ │ ├── generate-object.ts │ │ │ │ ├── index.ts │ │ │ │ ├── inject-json-instruction.test.ts │ │ │ │ ├── inject-json-instruction.ts │ │ │ │ ├── output-strategy.ts │ │ │ │ ├── parse-and-validate-object-result.ts │ │ │ │ ├── repair-text.ts │ │ │ │ ├── stream-object-result.ts │ │ │ │ ├── stream-object.test-d.ts │ │ │ │ ├── stream-object.test.ts │ │ │ │ ├── stream-object.ts │ │ │ │ └── validate-object-generation-input.ts │ │ │ ├── generate-speech │ │ │ │ ├── generate-speech-result.ts │ │ │ │ ├── generate-speech.test.ts │ │ │ │ ├── generate-speech.ts │ │ │ │ ├── generated-audio-file.ts │ │ │ │ └── index.ts │ │ │ ├── generate-text │ │ │ │ ├── content-part.ts │ │ │ │ ├── extract-content-text.ts │ │ │ │ ├── generate-text-result.ts │ │ │ │ ├── generate-text.test.ts │ │ │ │ ├── generate-text.ts │ │ │ │ ├── generated-file.ts │ │ │ │ ├── index.ts │ │ │ │ ├── output.test.ts │ │ │ │ ├── output.ts │ │ │ │ ├── parse-tool-call.test.ts │ │ │ │ ├── parse-tool-call.ts │ │ │ │ ├── prepare-step.ts │ │ │ │ ├── reasoning.ts │ │ │ │ ├── response-message.ts │ │ │ │ ├── run-tools-transformation.test.ts │ │ │ │ ├── run-tools-transformation.ts │ │ │ │ ├── smooth-stream.test.ts │ │ │ │ ├── smooth-stream.ts │ │ │ │ ├── step-result.ts │ │ │ │ ├── stop-condition.ts │ │ │ │ ├── stream-text-result.ts │ │ │ │ ├── stream-text.test.ts │ │ │ │ ├── stream-text.ts │ │ │ │ ├── to-response-messages.test.ts │ │ │ │ ├── to-response-messages.ts │ │ │ │ ├── tool-call-repair-function.ts │ │ │ │ ├── tool-call.ts │ │ │ │ ├── tool-error.ts │ │ │ │ ├── tool-output.ts │ │ │ │ ├── tool-result.ts │ │ │ │ └── tool-set.ts │ │ │ ├── middleware │ │ │ │ ├── default-settings-middleware.test.ts │ │ │ │ ├── default-settings-middleware.ts │ │ │ │ ├── extract-reasoning-middleware.test.ts │ │ │ │ ├── extract-reasoning-middleware.ts │ │ │ │ ├── index.ts │ │ │ │ ├── simulate-streaming-middleware.test.ts │ │ │ │ ├── simulate-streaming-middleware.ts │ │ │ │ ├── wrap-language-model.test.ts │ │ │ │ ├── wrap-language-model.ts │ │ │ │ ├── wrap-provider.test.ts │ │ │ │ └── wrap-provider.ts │ │ │ ├── model │ │ │ │ ├── resolve-model.test.ts │ │ │ │ └── resolve-model.ts │ │ │ ├── prompt │ │ │ │ ├── call-settings.ts │ │ │ │ ├── content-part.ts │ │ │ │ ├── convert-to-language-model-prompt.test.ts │ │ │ │ ├── convert-to-language-model-prompt.ts │ │ │ │ ├── create-tool-model-output.test.ts │ │ │ │ ├── create-tool-model-output.ts │ │ │ │ ├── data-content.test.ts │ │ │ │ ├── data-content.ts │ │ │ │ ├── index.ts │ │ │ │ ├── invalid-data-content-error.ts │ │ │ │ ├── invalid-message-role-error.ts │ │ │ │ ├── message-conversion-error.ts │ │ │ │ ├── message.ts │ │ │ │ ├── prepare-call-settings.test.ts │ │ │ │ ├── prepare-call-settings.ts │ │ │ │ ├── prepare-tools-and-tool-choice.test.ts │ │ │ │ ├── prepare-tools-and-tool-choice.ts │ │ │ │ ├── prompt.ts │ │ │ │ ├── split-data-url.ts │ │ │ │ ├── standardize-prompt.test.ts │ │ │ │ ├── standardize-prompt.ts │ │ │ │ └── wrap-gateway-error.ts │ │ │ ├── registry │ │ │ │ ├── custom-provider.test.ts │ │ │ │ ├── custom-provider.ts │ │ │ │ ├── index.ts │ │ │ │ ├── no-such-provider-error.ts │ │ │ │ ├── provider-registry.test.ts │ │ │ │ └── provider-registry.ts │ │ │ ├── telemetry │ │ │ │ ├── assemble-operation-name.ts │ │ │ │ ├── get-base-telemetry-attributes.ts │ │ │ │ ├── get-tracer.ts │ │ │ │ ├── noop-tracer.ts │ │ │ │ ├── record-span.ts │ │ │ │ ├── select-telemetry-attributes.ts │ │ │ │ ├── select-temetry-attributes.test.ts │ │ │ │ ├── stringify-for-telemetry.test.ts │ │ │ │ ├── stringify-for-telemetry.ts │ │ │ │ └── telemetry-settings.ts │ │ │ ├── test │ │ │ │ ├── mock-embedding-model-v2.ts │ │ │ │ ├── mock-image-model-v2.ts │ │ │ │ ├── mock-language-model-v2.ts │ │ │ │ ├── mock-provider-v2.ts │ │ │ │ ├── mock-server-response.ts │ │ │ │ ├── mock-speech-model-v2.ts │ │ │ │ ├── mock-tracer.ts │ │ │ │ ├── mock-transcription-model-v2.ts │ │ │ │ ├── mock-values.ts │ │ │ │ └── not-implemented.ts │ │ │ ├── text-stream │ │ │ │ ├── create-text-stream-response.test.ts │ │ │ │ ├── create-text-stream-response.ts │ │ │ │ ├── index.ts │ │ │ │ ├── pipe-text-stream-to-response.test.ts │ │ │ │ └── pipe-text-stream-to-response.ts │ │ │ ├── tool │ │ │ │ ├── mcp │ │ │ │ │ ├── json-rpc-message.ts │ │ │ │ │ ├── mcp-client.test.ts │ │ │ │ │ ├── mcp-client.ts │ │ │ │ │ ├── mcp-sse-transport.test.ts │ │ │ │ │ ├── mcp-sse-transport.ts │ │ │ │ │ ├── mcp-transport.ts │ │ │ │ │ ├── mock-mcp-transport.ts │ │ │ │ │ └── types.ts │ │ │ │ └── index.ts │ │ │ ├── transcribe │ │ │ │ ├── index.ts │ │ │ │ ├── transcribe-result.ts │ │ │ │ ├── transcribe.test.ts │ │ │ │ └── transcribe.ts │ │ │ ├── types │ │ │ │ ├── embedding-model.ts │ │ │ │ ├── image-model-response-metadata.ts │ │ │ │ ├── image-model.ts │ │ │ │ ├── index.ts │ │ │ │ ├── json-value.ts │ │ │ │ ├── language-model-request-metadata.ts │ │ │ │ ├── language-model-response-metadata.ts │ │ │ │ ├── language-model.ts │ │ │ │ ├── provider-metadata.ts │ │ │ │ ├── provider.ts │ │ │ │ ├── speech-model-response-metadata.ts │ │ │ │ ├── speech-model.ts │ │ │ │ ├── transcription-model-response-metadata.ts │ │ │ │ ├── transcription-model.ts │ │ │ │ └── usage.ts │ │ │ ├── ui │ │ │ │ ├── call-completion-api.ts │ │ │ │ ├── chat-transport.ts │ │ │ │ ├── chat.test-d.ts │ │ │ │ ├── chat.test.ts │ │ │ │ ├── chat.ts │ │ │ │ ├── convert-file-list-to-file-ui-parts.ts │ │ │ │ ├── convert-to-model-messages.test.ts │ │ │ │ ├── convert-to-model-messages.ts │ │ │ │ ├── default-chat-transport.ts │ │ │ │ ├── http-chat-transport.test.ts │ │ │ │ ├── http-chat-transport.ts │ │ │ │ ├── index.ts │ │ │ │ ├── last-assistant-message-is-complete-with-tool-calls.test.ts │ │ │ │ ├── last-assistant-message-is-complete-with-tool-calls.ts │ │ │ │ ├── process-text-stream.test.ts │ │ │ │ ├── process-text-stream.ts │ │ │ │ ├── process-ui-message-stream.test.ts │ │ │ │ ├── process-ui-message-stream.ts │ │ │ │ ├── text-stream-chat-transport.ts │ │ │ │ ├── transform-text-to-ui-message-stream.test.ts │ │ │ │ ├── transform-text-to-ui-message-stream.ts │ │ │ │ ├── ui-messages.test.ts │ │ │ │ ├── ui-messages.ts │ │ │ │ └── use-completion.ts │ │ │ ├── ui-message-stream │ │ │ │ ├── create-ui-message-stream-response.test.ts │ │ │ │ ├── create-ui-message-stream-response.ts │ │ │ │ ├── create-ui-message-stream.test.ts │ │ │ │ ├── create-ui-message-stream.ts │ │ │ │ ├── get-response-ui-message-id.test.ts │ │ │ │ ├── get-response-ui-message-id.ts │ │ │ │ ├── handle-ui-message-stream-finish.test.ts │ │ │ │ ├── handle-ui-message-stream-finish.ts │ │ │ │ ├── index.ts │ │ │ │ ├── json-to-sse-transform-stream.ts │ │ │ │ ├── pipe-ui-message-stream-to-response.test.ts │ │ │ │ ├── pipe-ui-message-stream-to-response.ts │ │ │ │ ├── read-ui-message-stream.test.ts │ │ │ │ ├── read-ui-message-stream.ts │ │ │ │ ├── ui-message-chunks.ts │ │ │ │ ├── ui-message-stream-headers.ts │ │ │ │ ├── ui-message-stream-on-finish-callback.ts │ │ │ │ ├── ui-message-stream-response-init.ts │ │ │ │ └── ui-message-stream-writer.ts │ │ │ ├── util │ │ │ │ ├── as-array.ts │ │ │ │ ├── async-iterable-stream.test.ts │ │ │ │ ├── async-iterable-stream.ts │ │ │ │ ├── consume-stream.ts │ │ │ │ ├── cosine-similarity.test.ts │ │ │ │ ├── cosine-similarity.ts │ │ │ │ ├── create-resolvable-promise.ts │ │ │ │ ├── create-stitchable-stream.test.ts │ │ │ │ ├── create-stitchable-stream.ts │ │ │ │ ├── data-url.ts │ │ │ │ ├── deep-partial.ts │ │ │ │ ├── delayed-promise.test.ts │ │ │ │ ├── delayed-promise.ts │ │ │ │ ├── detect-media-type.test.ts │ │ │ │ ├── detect-media-type.ts │ │ │ │ ├── download-error.ts │ │ │ │ ├── download.test.ts │ │ │ │ ├── download.ts │ │ │ │ ├── error-handler.ts │ │ │ │ ├── filter-stream-errors.ts │ │ │ │ ├── fix-json.test.ts │ │ │ │ ├── fix-json.ts │ │ │ │ ├── get-potential-start-index.test.ts │ │ │ │ ├── get-potential-start-index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── is-deep-equal-data.test.ts │ │ │ │ ├── is-deep-equal-data.ts │ │ │ │ ├── is-non-empty-object.ts │ │ │ │ ├── job.ts │ │ │ │ ├── merge-objects.test.ts │ │ │ │ ├── merge-objects.ts │ │ │ │ ├── now.ts │ │ │ │ ├── parse-partial-json.test.ts │ │ │ │ ├── parse-partial-json.ts │ │ │ │ ├── prepare-headers.test.ts │ │ │ │ ├── prepare-headers.ts │ │ │ │ ├── prepare-retries.test.ts │ │ │ │ ├── prepare-retries.ts │ │ │ │ ├── retry-error.ts │ │ │ │ ├── retry-with-exponential-backoff.test.ts │ │ │ │ ├── retry-with-exponential-backoff.ts │ │ │ │ ├── serial-job-executor.test.ts │ │ │ │ ├── serial-job-executor.ts │ │ │ │ ├── simulate-readable-stream.test.ts │ │ │ │ ├── simulate-readable-stream.ts │ │ │ │ ├── split-array.test.ts │ │ │ │ ├── split-array.ts │ │ │ │ ├── value-of.ts │ │ │ │ └── write-to-server-response.ts │ │ │ ├── global.ts │ │ │ └── index.ts │ │ ├── test │ │ │ └── index.ts │ │ ├── .eslintrc.js │ │ ├── index.ts │ │ ├── internal.d.ts │ │ ├── mcp-stdio.d.ts │ │ ├── test.d.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── amazon-bedrock │ │ ├── src │ │ │ ├── bedrock-api-types.ts │ │ │ ├── bedrock-chat-language-model.test.ts │ │ │ ├── bedrock-chat-language-model.ts │ │ │ ├── bedrock-chat-options.ts │ │ │ ├── bedrock-embedding-model.test.ts │ │ │ ├── bedrock-embedding-model.ts │ │ │ ├── bedrock-embedding-options.ts │ │ │ ├── bedrock-error.ts │ │ │ ├── bedrock-event-stream-response-handler.test.ts │ │ │ ├── bedrock-event-stream-response-handler.ts │ │ │ ├── bedrock-image-model.test.ts │ │ │ ├── bedrock-image-model.ts │ │ │ ├── bedrock-image-settings.ts │ │ │ ├── bedrock-prepare-tools.ts │ │ │ ├── bedrock-provider.test.ts │ │ │ ├── bedrock-provider.ts │ │ │ ├── bedrock-sigv4-fetch.test.ts │ │ │ ├── bedrock-sigv4-fetch.ts │ │ │ ├── convert-to-bedrock-chat-messages.test.ts │ │ │ ├── convert-to-bedrock-chat-messages.ts │ │ │ ├── headers-utils.test.ts │ │ │ ├── headers-utils.ts │ │ │ ├── index.ts │ │ │ ├── inject-fetch-headers.test.ts │ │ │ ├── inject-fetch-headers.ts │ │ │ └── map-bedrock-finish-reason.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── angular │ │ ├── src │ │ │ ├── lib │ │ │ │ ├── chat.ng.test.ts │ │ │ │ ├── chat.ng.ts │ │ │ │ ├── completion.ng.test.ts │ │ │ │ ├── completion.ng.ts │ │ │ │ ├── structured-object.ng.test.ts │ │ │ │ └── structured-object.ng.ts │ │ │ └── index.ts │ │ ├── tsup.config.ts │ │ └── vitest.config.ts │ ├── anthropic │ │ ├── src │ │ │ ├── internal │ │ │ │ └── index.ts │ │ │ ├── tool │ │ │ │ ├── bash_20241022.ts │ │ │ │ ├── bash_20250124.ts │ │ │ │ ├── computer_20241022.ts │ │ │ │ ├── computer_20250124.ts │ │ │ │ ├── text-editor_20241022.ts │ │ │ │ ├── text-editor_20250124.ts │ │ │ │ ├── text-editor_20250429.ts │ │ │ │ └── web-search_20250305.ts │ │ │ ├── anthropic-api-types.ts │ │ │ ├── anthropic-error.test.ts │ │ │ ├── anthropic-error.ts │ │ │ ├── anthropic-messages-language-model.test.ts │ │ │ ├── anthropic-messages-language-model.ts │ │ │ ├── anthropic-messages-options.ts │ │ │ ├── anthropic-prepare-tools.test.ts │ │ │ ├── anthropic-prepare-tools.ts │ │ │ ├── anthropic-provider.ts │ │ │ ├── anthropic-tools.ts │ │ │ ├── convert-to-anthropic-messages-prompt.test.ts │ │ │ ├── convert-to-anthropic-messages-prompt.ts │ │ │ ├── get-cache-control.ts │ │ │ ├── index.ts │ │ │ └── map-anthropic-stop-reason.ts │ │ ├── internal.d.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── assemblyai │ │ ├── src │ │ │ ├── assemblyai-api-types.ts │ │ │ ├── assemblyai-config.ts │ │ │ ├── assemblyai-error.test.ts │ │ │ ├── assemblyai-error.ts │ │ │ ├── assemblyai-provider.ts │ │ │ ├── assemblyai-transcription-model.test.ts │ │ │ ├── assemblyai-transcription-model.ts │ │ │ ├── assemblyai-transcription-settings.ts │ │ │ └── index.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── azure │ │ ├── src │ │ │ ├── azure-openai-provider.test.ts │ │ │ ├── azure-openai-provider.ts │ │ │ └── index.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── cerebras │ │ ├── src │ │ │ ├── cerebras-chat-options.ts │ │ │ ├── cerebras-provider.test.ts │ │ │ ├── cerebras-provider.ts │ │ │ └── index.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── codemod │ │ ├── scripts │ │ │ ├── generate-readme.ts │ │ │ └── scaffold-codemod.ts │ │ ├── src │ │ │ ├── bin │ │ │ │ └── codemod.ts │ │ │ ├── codemods │ │ │ │ ├── lib │ │ │ │ │ ├── create-transformer.ts │ │ │ │ │ ├── remove-await-fn.ts │ │ │ │ │ └── remove-facade.ts │ │ │ │ ├── v4 │ │ │ │ │ ├── remove-ai-stream-methods-from-stream-text-result.ts │ │ │ │ │ ├── remove-anthropic-facade.ts │ │ │ │ │ ├── remove-await-streamobject.ts │ │ │ │ │ ├── remove-await-streamtext.ts │ │ │ │ │ ├── remove-deprecated-provider-registry-exports.ts │ │ │ │ │ ├── remove-experimental-ai-fn-exports.ts │ │ │ │ │ ├── remove-experimental-message-types.ts │ │ │ │ │ ├── remove-experimental-streamdata.ts │ │ │ │ │ ├── remove-experimental-tool.ts │ │ │ │ │ ├── remove-experimental-useassistant.ts │ │ │ │ │ ├── remove-google-facade.ts │ │ │ │ │ ├── remove-isxxxerror.ts │ │ │ │ │ ├── remove-metadata-with-headers.ts │ │ │ │ │ ├── remove-mistral-facade.ts │ │ │ │ │ ├── remove-openai-facade.ts │ │ │ │ │ ├── rename-format-stream-part.ts │ │ │ │ │ ├── rename-parse-stream-part.ts │ │ │ │ │ ├── replace-baseurl.ts │ │ │ │ │ ├── replace-continuation-steps.ts │ │ │ │ │ ├── replace-langchain-toaistream.ts │ │ │ │ │ ├── replace-nanoid.ts │ │ │ │ │ ├── replace-roundtrips-with-maxsteps.ts │ │ │ │ │ ├── replace-token-usage-types.ts │ │ │ │ │ └── rewrite-framework-imports.ts │ │ │ │ └── v5 │ │ │ │ ├── flatten-streamtext-file-properties.ts │ │ │ │ ├── import-LanguageModelV2-from-provider-package.ts │ │ │ │ ├── migrate-to-data-stream-protocol-v2.ts │ │ │ │ ├── move-image-model-maxImagesPerCall.ts │ │ │ │ ├── move-langchain-adapter.ts │ │ │ │ ├── move-provider-options.ts │ │ │ │ ├── move-react-to-ai-sdk.ts │ │ │ │ ├── move-ui-utils-to-ai.ts │ │ │ │ ├── remove-experimental-wrap-language-model.ts │ │ │ │ ├── remove-get-ui-text.ts │ │ │ │ ├── remove-openai-compatibility.ts │ │ │ │ ├── remove-sendExtraMessageFields.ts │ │ │ │ ├── rename-converttocoremessages-to-converttomodelmessages.ts │ │ │ │ ├── rename-core-message-to-model-message.ts │ │ │ │ ├── rename-datastream-transform-stream.ts │ │ │ │ ├── rename-IDGenerator-to-IdGenerator.ts │ │ │ │ ├── rename-languagemodelv1providermetadata.ts │ │ │ │ ├── rename-max-tokens-to-max-output-tokens.ts │ │ │ │ ├── rename-message-to-ui-message.ts │ │ │ │ ├── rename-mime-type-to-media-type.ts │ │ │ │ ├── rename-pipedatastreamtoresponse-to-pipeuimessagestreamtoresponse.ts │ │ │ │ ├── rename-reasoning-properties.ts │ │ │ │ ├── rename-reasoning-to-reasoningText.ts │ │ │ │ ├── rename-request-options.ts │ │ │ │ ├── rename-todatastreamresponse-to-touimessagestreamresponse.ts │ │ │ │ ├── rename-tool-parameters-to-inputschema.ts │ │ │ │ ├── replace-bedrock-snake-case.ts │ │ │ │ ├── replace-content-with-parts.ts │ │ │ │ ├── replace-experimental-provider-metadata.ts │ │ │ │ ├── replace-generatetext-text-property.ts │ │ │ │ ├── replace-image-type-with-file-type.ts │ │ │ │ ├── replace-llamaindex-adapter.ts │ │ │ │ ├── replace-oncompletion-with-onfinal.ts │ │ │ │ ├── replace-provider-metadata-with-provider-options.ts │ │ │ │ ├── replace-rawresponse-with-response.ts │ │ │ │ ├── replace-redacted-reasoning-type.ts │ │ │ │ ├── replace-simulate-streaming.ts │ │ │ │ ├── replace-textdelta-with-text.ts │ │ │ │ ├── replace-usage-token-properties.ts │ │ │ │ ├── require-createIdGenerator-size-argument.ts │ │ │ │ ├── restructure-file-stream-parts.ts │ │ │ │ ├── restructure-source-stream-parts.ts │ │ │ │ └── rsc-package.ts │ │ │ ├── lib │ │ │ │ ├── transform-options.ts │ │ │ │ ├── transform.ts │ │ │ │ └── upgrade.ts │ │ │ └── test │ │ │ ├── __testfixtures__ │ │ │ │ ├── flatten-streamtext-file-properties.input.ts │ │ │ │ ├── flatten-streamtext-file-properties.output.ts │ │ │ │ ├── remove-ai-stream-methods-from-stream-text-result.input.ts │ │ │ │ ├── remove-ai-stream-methods-from-stream-text-result.output.ts │ │ │ │ ├── remove-anthropic-facade.input.ts │ │ │ │ ├── remove-anthropic-facade.output.ts │ │ │ │ ├── remove-await-fn-alias.input.ts │ │ │ │ ├── remove-await-fn-alias.output.ts │ │ │ │ ├── remove-await-fn-other-fn.input.ts │ │ │ │ ├── remove-await-fn-other-fn.output.ts │ │ │ │ ├── remove-await-fn-other.input.ts │ │ │ │ ├── remove-await-fn-other.output.ts │ │ │ │ ├── remove-await-fn.input.ts │ │ │ │ ├── remove-await-fn.output.ts │ │ │ │ ├── remove-deprecated-provider-registry-exports.input.ts │ │ │ │ ├── remove-deprecated-provider-registry-exports.output.ts │ │ │ │ ├── remove-experimental-ai-fn-exports.input.ts │ │ │ │ ├── remove-experimental-ai-fn-exports.output.ts │ │ │ │ ├── remove-experimental-message-types.input.ts │ │ │ │ ├── remove-experimental-message-types.output.ts │ │ │ │ ├── remove-experimental-streamdata.input.ts │ │ │ │ ├── remove-experimental-streamdata.output.ts │ │ │ │ ├── remove-experimental-tool-not-ai.input.ts │ │ │ │ ├── remove-experimental-tool-not-ai.output.ts │ │ │ │ ├── remove-experimental-tool.input.ts │ │ │ │ ├── remove-experimental-tool.output.ts │ │ │ │ ├── remove-experimental-useassistant.input.tsx │ │ │ │ ├── remove-experimental-useassistant.output.tsx │ │ │ │ ├── remove-google-facade.input.ts │ │ │ │ ├── remove-google-facade.output.ts │ │ │ │ ├── remove-isxxxerror.input.ts │ │ │ │ ├── remove-isxxxerror.output.ts │ │ │ │ ├── remove-metadata-with-headers.input.ts │ │ │ │ ├── remove-metadata-with-headers.output.ts │ │ │ │ ├── remove-mistral-facade.input.ts │ │ │ │ ├── remove-mistral-facade.output.ts │ │ │ │ ├── remove-openai-facade-as.input.ts │ │ │ │ ├── remove-openai-facade-as.output.ts │ │ │ │ ├── remove-openai-facade-corp.input.ts │ │ │ │ ├── remove-openai-facade-corp.output.ts │ │ │ │ ├── remove-openai-facade.input.ts │ │ │ │ ├── remove-openai-facade.output.ts │ │ │ │ ├── rename-converttocoremessages-to-converttomodelmessages.input.ts │ │ │ │ ├── rename-converttocoremessages-to-converttomodelmessages.output.ts │ │ │ │ ├── rename-format-stream-part-not-ai.input.ts │ │ │ │ ├── rename-format-stream-part-not-ai.output.ts │ │ │ │ ├── rename-format-stream-part.input.ts │ │ │ │ ├── rename-format-stream-part.output.ts │ │ │ │ ├── rename-IDGenerator-to-IdGenerator.input.ts │ │ │ │ ├── rename-IDGenerator-to-IdGenerator.output.ts │ │ │ │ ├── rename-message-to-ui-message.input.ts │ │ │ │ ├── rename-message-to-ui-message.output.ts │ │ │ │ ├── rename-parse-stream-part-not-ai.input.ts │ │ │ │ ├── rename-parse-stream-part-not-ai.output.ts │ │ │ │ ├── rename-parse-stream-part.input.ts │ │ │ │ ├── rename-parse-stream-part.output.ts │ │ │ │ ├── rename-pipedatastreamtoresponse-to-pipeuimessagestreamtoresponse.input.ts │ │ │ │ ├── rename-pipedatastreamtoresponse-to-pipeuimessagestreamtoresponse.output.ts │ │ │ │ ├── rename-todatastreamresponse-to-touimessagestreamresponse.input.ts │ │ │ │ ├── rename-todatastreamresponse-to-touimessagestreamresponse.output.ts │ │ │ │ ├── rename-tool-parameters-to-inputschema.input.ts │ │ │ │ ├── rename-tool-parameters-to-inputschema.output.ts │ │ │ │ ├── replace-baseurl.input.ts │ │ │ │ ├── replace-baseurl.output.ts │ │ │ │ ├── replace-continuation-steps.input.ts │ │ │ │ ├── replace-continuation-steps.output.ts │ │ │ │ ├── replace-langchain-toaistream.input.ts │ │ │ │ ├── replace-langchain-toaistream.output.ts │ │ │ │ ├── replace-nanoid-not-ai.input.ts │ │ │ │ ├── replace-nanoid-not-ai.output.ts │ │ │ │ ├── replace-nanoid.input.ts │ │ │ │ ├── replace-nanoid.output.ts │ │ │ │ ├── replace-roundtrips-with-maxsteps.input.ts │ │ │ │ ├── replace-roundtrips-with-maxsteps.output.ts │ │ │ │ ├── replace-token-usage-types.input.ts │ │ │ │ ├── replace-token-usage-types.output.ts │ │ │ │ ├── require-createIdGenerator-size-argument.input.ts │ │ │ │ ├── require-createIdGenerator-size-argument.output.ts │ │ │ │ ├── rewrite-framework-imports-solid.input.ts │ │ │ │ ├── rewrite-framework-imports-solid.output.ts │ │ │ │ ├── rewrite-framework-imports-svelte.input.ts │ │ │ │ ├── rewrite-framework-imports-svelte.output.ts │ │ │ │ ├── rewrite-framework-imports-vue.input.ts │ │ │ │ └── rewrite-framework-imports-vue.output.ts │ │ │ ├── create-transformer.test.ts │ │ │ ├── flatten-streamtext-file-properties.test.ts │ │ │ ├── jscodeshift-testUtils.d.ts │ │ │ ├── remove-ai-stream-methods-from-stream-text-result.test.ts │ │ │ ├── remove-anthropic-facade.test.ts │ │ │ ├── remove-await-fn.test.ts │ │ │ ├── remove-deprecated-provider-registry-exports.test.ts │ │ │ ├── remove-experimental-ai-fn-exports.test.ts │ │ │ ├── remove-experimental-message-types.test.ts │ │ │ ├── remove-experimental-streamdata.test.ts │ │ │ ├── remove-experimental-tool.test.ts │ │ │ ├── remove-experimental-useassistant.test.ts │ │ │ ├── remove-google-facade.test.ts │ │ │ ├── remove-isxxxerror.test.ts │ │ │ ├── remove-metadata-with-headers.test.ts │ │ │ ├── remove-mistral-facade.test.ts │ │ │ ├── remove-openai-facade.test.ts │ │ │ ├── rename-converttocoremessages-to-converttomodelmessages.test.ts │ │ │ ├── rename-format-stream-part.test.ts │ │ │ ├── rename-IDGenerator-to-IdGenerator.test.ts │ │ │ ├── rename-message-to-ui-message.test.ts │ │ │ ├── rename-parse-stream-part.test.ts │ │ │ ├── rename-pipedatastreamtoresponse-to-pipeuimessagestreamtoresponse.test.ts │ │ │ ├── rename-todatastreamresponse-to-touimessagestreamresponse.test.ts │ │ │ ├── rename-tool-parameters-to-inputschema.test.ts │ │ │ ├── replace-baseurl.test.ts │ │ │ ├── replace-continuation-steps.test.ts │ │ │ ├── replace-langchain-toaistream.test.ts │ │ │ ├── replace-nanoid.test.ts │ │ │ ├── replace-roundtrips-with-maxsteps.test.ts │ │ │ ├── replace-token-usage-types.test.ts │ │ │ ├── require-createIdGenerator-size-argument.test.ts │ │ │ ├── rewrite-framework-imports.test.ts │ │ │ ├── test-utils.test.ts │ │ │ └── test-utils.ts │ │ ├── tsup.config.ts │ │ └── vitest.config.ts │ ├── cohere │ │ ├── src │ │ │ ├── cohere-chat-language-model.test.ts │ │ │ ├── cohere-chat-language-model.ts │ │ │ ├── cohere-chat-options.ts │ │ │ ├── cohere-chat-prompt.ts │ │ │ ├── cohere-embedding-model.test.ts │ │ │ ├── cohere-embedding-model.ts │ │ │ ├── cohere-embedding-options.ts │ │ │ ├── cohere-error.ts │ │ │ ├── cohere-prepare-tools.test.ts │ │ │ ├── cohere-prepare-tools.ts │ │ │ ├── cohere-provider.ts │ │ │ ├── convert-to-cohere-chat-prompt.test.ts │ │ │ ├── convert-to-cohere-chat-prompt.ts │ │ │ ├── index.ts │ │ │ └── map-cohere-finish-reason.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── deepgram │ │ ├── src │ │ │ ├── deepgram-api-types.ts │ │ │ ├── deepgram-config.ts │ │ │ ├── deepgram-error.test.ts │ │ │ ├── deepgram-error.ts │ │ │ ├── deepgram-provider.ts │ │ │ ├── deepgram-transcription-model.test.ts │ │ │ ├── deepgram-transcription-model.ts │ │ │ ├── deepgram-transcription-options.ts │ │ │ └── index.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── deepinfra │ │ ├── src │ │ │ ├── deepinfra-chat-options.ts │ │ │ ├── deepinfra-completion-options.ts │ │ │ ├── deepinfra-embedding-options.ts │ │ │ ├── deepinfra-image-model.test.ts │ │ │ ├── deepinfra-image-model.ts │ │ │ ├── deepinfra-image-settings.ts │ │ │ ├── deepinfra-provider.test.ts │ │ │ ├── deepinfra-provider.ts │ │ │ └── index.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── deepseek │ │ ├── src │ │ │ ├── deepseek-chat-options.ts │ │ │ ├── deepseek-metadata-extractor.test.ts │ │ │ ├── deepseek-metadata-extractor.ts │ │ │ ├── deepseek-provider.test.ts │ │ │ ├── deepseek-provider.ts │ │ │ └── index.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── elevenlabs │ │ ├── src │ │ │ ├── elevenlabs-api-types.ts │ │ │ ├── elevenlabs-config.ts │ │ │ ├── elevenlabs-error.test.ts │ │ │ ├── elevenlabs-error.ts │ │ │ ├── elevenlabs-provider.ts │ │ │ ├── elevenlabs-transcription-model.test.ts │ │ │ ├── elevenlabs-transcription-model.ts │ │ │ ├── elevenlabs-transcription-options.ts │ │ │ └── index.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── fal │ │ ├── src │ │ │ ├── fal-api-types.ts │ │ │ ├── fal-config.ts │ │ │ ├── fal-error.test.ts │ │ │ ├── fal-error.ts │ │ │ ├── fal-image-model.test.ts │ │ │ ├── fal-image-model.ts │ │ │ ├── fal-image-settings.ts │ │ │ ├── fal-provider.test.ts │ │ │ ├── fal-provider.ts │ │ │ ├── fal-transcription-model.test.ts │ │ │ ├── fal-transcription-model.ts │ │ │ ├── fal-transcription-options.ts │ │ │ └── index.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── fireworks │ │ ├── src │ │ │ ├── fireworks-chat-options.ts │ │ │ ├── fireworks-completion-options.ts │ │ │ ├── fireworks-embedding-options.ts │ │ │ ├── fireworks-image-model.test.ts │ │ │ ├── fireworks-image-model.ts │ │ │ ├── fireworks-image-options.ts │ │ │ ├── fireworks-provider.test.ts │ │ │ ├── fireworks-provider.ts │ │ │ └── index.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── gateway │ │ ├── src │ │ │ ├── errors │ │ │ │ ├── as-gateway-error.ts │ │ │ │ ├── create-gateway-error.test.ts │ │ │ │ ├── create-gateway-error.ts │ │ │ │ ├── extract-api-call-response.test.ts │ │ │ │ ├── extract-api-call-response.ts │ │ │ │ ├── gateway-authentication-error.ts │ │ │ │ ├── gateway-error-types.test.ts │ │ │ │ ├── gateway-error.ts │ │ │ │ ├── gateway-internal-server-error.ts │ │ │ │ ├── gateway-invalid-request-error.ts │ │ │ │ ├── gateway-model-not-found-error.ts │ │ │ │ ├── gateway-rate-limit-error.ts │ │ │ │ ├── gateway-response-error.ts │ │ │ │ ├── index.ts │ │ │ │ ├── parse-auth-method.test.ts │ │ │ │ └── parse-auth-method.ts │ │ │ ├── gateway-config.ts │ │ │ ├── gateway-embedding-model-settings.ts │ │ │ ├── gateway-embedding-model.test.ts │ │ │ ├── gateway-embedding-model.ts │ │ │ ├── gateway-fetch-metadata.test.ts │ │ │ ├── gateway-fetch-metadata.ts │ │ │ ├── gateway-language-model-settings.ts │ │ │ ├── gateway-language-model.test.ts │ │ │ ├── gateway-language-model.ts │ │ │ ├── gateway-model-entry.ts │ │ │ ├── gateway-provider-options.ts │ │ │ ├── gateway-provider.test.ts │ │ │ ├── gateway-provider.ts │ │ │ ├── index.ts │ │ │ ├── vercel-environment.test.ts │ │ │ └── vercel-environment.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── gladia │ │ ├── src │ │ │ ├── gladia-api-types.ts │ │ │ ├── gladia-config.ts │ │ │ ├── gladia-error.test.ts │ │ │ ├── gladia-error.ts │ │ │ ├── gladia-provider.ts │ │ │ ├── gladia-transcription-model.test.ts │ │ │ ├── gladia-transcription-model.ts │ │ │ └── index.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── google │ │ ├── src │ │ │ ├── internal │ │ │ │ └── index.ts │ │ │ ├── tool │ │ │ │ ├── code-execution.ts │ │ │ │ ├── google-search.ts │ │ │ │ └── url-context.ts │ │ │ ├── convert-json-schema-to-openapi-schema.test.ts │ │ │ ├── convert-json-schema-to-openapi-schema.ts │ │ │ ├── convert-to-google-generative-ai-messages.test.ts │ │ │ ├── convert-to-google-generative-ai-messages.ts │ │ │ ├── get-model-path.test.ts │ │ │ ├── get-model-path.ts │ │ │ ├── google-error.ts │ │ │ ├── google-generative-ai-embedding-model.test.ts │ │ │ ├── google-generative-ai-embedding-model.ts │ │ │ ├── google-generative-ai-embedding-options.ts │ │ │ ├── google-generative-ai-image-model.test.ts │ │ │ ├── google-generative-ai-image-model.ts │ │ │ ├── google-generative-ai-image-settings.ts │ │ │ ├── google-generative-ai-language-model.test.ts │ │ │ ├── google-generative-ai-language-model.ts │ │ │ ├── google-generative-ai-options.ts │ │ │ ├── google-generative-ai-prompt.ts │ │ │ ├── google-prepare-tools.test.ts │ │ │ ├── google-prepare-tools.ts │ │ │ ├── google-provider.test.ts │ │ │ ├── google-provider.ts │ │ │ ├── google-supported-file-url.test.ts │ │ │ ├── google-supported-file-url.ts │ │ │ ├── google-tools.ts │ │ │ ├── index.ts │ │ │ └── map-google-generative-ai-finish-reason.ts │ │ ├── internal.d.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── google-vertex │ │ ├── anthropic │ │ │ ├── edge.d.ts │ │ │ └── index.d.ts │ │ ├── src │ │ │ ├── anthropic │ │ │ │ ├── edge │ │ │ │ │ ├── google-vertex-anthropic-provider-edge.test.ts │ │ │ │ │ ├── google-vertex-anthropic-provider-edge.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── google-vertex-anthropic-messages-options.ts │ │ │ │ ├── google-vertex-anthropic-provider-node.test.ts │ │ │ │ ├── google-vertex-anthropic-provider-node.ts │ │ │ │ ├── google-vertex-anthropic-provider.test.ts │ │ │ │ ├── google-vertex-anthropic-provider.ts │ │ │ │ └── index.ts │ │ │ ├── edge │ │ │ │ ├── google-vertex-auth-edge.test.ts │ │ │ │ ├── google-vertex-auth-edge.ts │ │ │ │ ├── google-vertex-provider-edge.test.ts │ │ │ │ ├── google-vertex-provider-edge.ts │ │ │ │ └── index.ts │ │ │ ├── google-vertex-auth-google-auth-library.test.ts │ │ │ ├── google-vertex-auth-google-auth-library.ts │ │ │ ├── google-vertex-config.ts │ │ │ ├── google-vertex-embedding-model.test.ts │ │ │ ├── google-vertex-embedding-model.ts │ │ │ ├── google-vertex-embedding-options.ts │ │ │ ├── google-vertex-error.ts │ │ │ ├── google-vertex-image-model.test.ts │ │ │ ├── google-vertex-image-model.ts │ │ │ ├── google-vertex-image-settings.ts │ │ │ ├── google-vertex-options.ts │ │ │ ├── google-vertex-provider-node.test.ts │ │ │ ├── google-vertex-provider-node.ts │ │ │ ├── google-vertex-provider.test.ts │ │ │ ├── google-vertex-provider.ts │ │ │ └── index.ts │ │ ├── edge.d.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── groq │ │ ├── src │ │ │ ├── convert-to-groq-chat-messages.test.ts │ │ │ ├── convert-to-groq-chat-messages.ts │ │ │ ├── get-response-metadata.ts │ │ │ ├── groq-api-types.ts │ │ │ ├── groq-chat-language-model.test.ts │ │ │ ├── groq-chat-language-model.ts │ │ │ ├── groq-chat-options.ts │ │ │ ├── groq-config.ts │ │ │ ├── groq-error.ts │ │ │ ├── groq-prepare-tools.ts │ │ │ ├── groq-provider.ts │ │ │ ├── groq-transcription-model.test.ts │ │ │ ├── groq-transcription-model.ts │ │ │ ├── groq-transcription-options.ts │ │ │ ├── index.ts │ │ │ └── map-groq-finish-reason.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── hume │ │ ├── src │ │ │ ├── hume-api-types.ts │ │ │ ├── hume-config.ts │ │ │ ├── hume-error.test.ts │ │ │ ├── hume-error.ts │ │ │ ├── hume-provider.ts │ │ │ ├── hume-speech-model.test.ts │ │ │ ├── hume-speech-model.ts │ │ │ └── index.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── langchain │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── langchain-adapter.test.ts │ │ │ ├── langchain-adapter.ts │ │ │ └── stream-callbacks.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── llamaindex │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── llamaindex-adapter.test.ts │ │ │ ├── llamaindex-adapter.ts │ │ │ └── stream-callbacks.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── lmnt │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── lmnt-api-types.ts │ │ │ ├── lmnt-config.ts │ │ │ ├── lmnt-error.test.ts │ │ │ ├── lmnt-error.ts │ │ │ ├── lmnt-provider.ts │ │ │ ├── lmnt-speech-model.test.ts │ │ │ ├── lmnt-speech-model.ts │ │ │ └── lmnt-speech-options.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── luma │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── luma-image-model.test.ts │ │ │ ├── luma-image-model.ts │ │ │ ├── luma-image-settings.ts │ │ │ ├── luma-provider.test.ts │ │ │ └── luma-provider.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── mistral │ │ ├── src │ │ │ ├── convert-to-mistral-chat-messages.test.ts │ │ │ ├── convert-to-mistral-chat-messages.ts │ │ │ ├── get-response-metadata.ts │ │ │ ├── index.ts │ │ │ ├── map-mistral-finish-reason.ts │ │ │ ├── mistral-chat-language-model.test.ts │ │ │ ├── mistral-chat-language-model.ts │ │ │ ├── mistral-chat-options.ts │ │ │ ├── mistral-chat-prompt.ts │ │ │ ├── mistral-embedding-model.test.ts │ │ │ ├── mistral-embedding-model.ts │ │ │ ├── mistral-embedding-options.ts │ │ │ ├── mistral-error.ts │ │ │ ├── mistral-prepare-tools.ts │ │ │ └── mistral-provider.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── openai │ │ ├── src │ │ │ ├── internal │ │ │ │ └── index.ts │ │ │ ├── responses │ │ │ │ ├── convert-to-openai-responses-messages.test.ts │ │ │ │ ├── convert-to-openai-responses-messages.ts │ │ │ │ ├── map-openai-responses-finish-reason.ts │ │ │ │ ├── openai-responses-api-types.ts │ │ │ │ ├── openai-responses-language-model.test.ts │ │ │ │ ├── openai-responses-language-model.ts │ │ │ │ ├── openai-responses-prepare-tools.ts │ │ │ │ └── openai-responses-settings.ts │ │ │ ├── tool │ │ │ │ ├── file-search.ts │ │ │ │ └── web-search-preview.ts │ │ │ ├── convert-to-openai-chat-messages.test.ts │ │ │ ├── convert-to-openai-chat-messages.ts │ │ │ ├── convert-to-openai-completion-prompt.ts │ │ │ ├── get-response-metadata.ts │ │ │ ├── index.ts │ │ │ ├── map-openai-finish-reason.ts │ │ │ ├── openai-api-types.ts │ │ │ ├── openai-chat-language-model.test.ts │ │ │ ├── openai-chat-language-model.ts │ │ │ ├── openai-chat-options.ts │ │ │ ├── openai-chat-prompt.ts │ │ │ ├── openai-completion-language-model.test.ts │ │ │ ├── openai-completion-language-model.ts │ │ │ ├── openai-completion-options.ts │ │ │ ├── openai-config.ts │ │ │ ├── openai-embedding-model.test.ts │ │ │ ├── openai-embedding-model.ts │ │ │ ├── openai-embedding-options.ts │ │ │ ├── openai-error.test.ts │ │ │ ├── openai-error.ts │ │ │ ├── openai-image-model.test.ts │ │ │ ├── openai-image-model.ts │ │ │ ├── openai-image-settings.ts │ │ │ ├── openai-prepare-tools.test.ts │ │ │ ├── openai-prepare-tools.ts │ │ │ ├── openai-provider.ts │ │ │ ├── openai-speech-model.test.ts │ │ │ ├── openai-speech-model.ts │ │ │ ├── openai-speech-options.ts │ │ │ ├── openai-tools.ts │ │ │ ├── openai-transcription-model.test.ts │ │ │ ├── openai-transcription-model.ts │ │ │ ├── openai-transcription-options.ts │ │ │ └── openai-types.ts │ │ ├── internal.d.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── openai-compatible │ │ ├── src │ │ │ ├── internal │ │ │ │ └── index.ts │ │ │ ├── convert-to-openai-compatible-chat-messages.test.ts │ │ │ ├── convert-to-openai-compatible-chat-messages.ts │ │ │ ├── convert-to-openai-compatible-completion-prompt.ts │ │ │ ├── get-response-metadata.ts │ │ │ ├── index.ts │ │ │ ├── map-openai-compatible-finish-reason.ts │ │ │ ├── openai-compatible-api-types.ts │ │ │ ├── openai-compatible-chat-language-model.test.ts │ │ │ ├── openai-compatible-chat-language-model.ts │ │ │ ├── openai-compatible-chat-options.ts │ │ │ ├── openai-compatible-completion-language-model.test.ts │ │ │ ├── openai-compatible-completion-language-model.ts │ │ │ ├── openai-compatible-completion-options.ts │ │ │ ├── openai-compatible-embedding-model.test.ts │ │ │ ├── openai-compatible-embedding-model.ts │ │ │ ├── openai-compatible-embedding-options.ts │ │ │ ├── openai-compatible-error.ts │ │ │ ├── openai-compatible-image-model.test.ts │ │ │ ├── openai-compatible-image-model.ts │ │ │ ├── openai-compatible-image-settings.ts │ │ │ ├── openai-compatible-metadata-extractor.ts │ │ │ ├── openai-compatible-prepare-tools.ts │ │ │ ├── openai-compatible-provider.test.ts │ │ │ └── openai-compatible-provider.ts │ │ ├── internal.d.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── perplexity │ │ ├── src │ │ │ ├── convert-to-perplexity-messages.test.ts │ │ │ ├── convert-to-perplexity-messages.ts │ │ │ ├── index.ts │ │ │ ├── map-perplexity-finish-reason.ts │ │ │ ├── perplexity-language-model-options.ts │ │ │ ├── perplexity-language-model-prompt.ts │ │ │ ├── perplexity-language-model.test.ts │ │ │ ├── perplexity-language-model.ts │ │ │ └── perplexity-provider.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── provider │ │ ├── src │ │ │ ├── embedding-model │ │ │ │ ├── v2 │ │ │ │ │ ├── embedding-model-v2-embedding.ts │ │ │ │ │ ├── embedding-model-v2.ts │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── errors │ │ │ │ ├── ai-sdk-error.ts │ │ │ │ ├── api-call-error.ts │ │ │ │ ├── empty-response-body-error.ts │ │ │ │ ├── get-error-message.ts │ │ │ │ ├── index.ts │ │ │ │ ├── invalid-argument-error.ts │ │ │ │ ├── invalid-prompt-error.ts │ │ │ │ ├── invalid-response-data-error.ts │ │ │ │ ├── json-parse-error.ts │ │ │ │ ├── load-api-key-error.ts │ │ │ │ ├── load-setting-error.ts │ │ │ │ ├── no-content-generated-error.ts │ │ │ │ ├── no-such-model-error.ts │ │ │ │ ├── too-many-embedding-values-for-call-error.ts │ │ │ │ ├── type-validation-error.ts │ │ │ │ └── unsupported-functionality-error.ts │ │ │ ├── image-model │ │ │ │ ├── v2 │ │ │ │ │ ├── image-model-v2-call-options.ts │ │ │ │ │ ├── image-model-v2-call-warning.ts │ │ │ │ │ ├── image-model-v2.ts │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── json-value │ │ │ │ ├── index.ts │ │ │ │ ├── is-json.ts │ │ │ │ └── json-value.ts │ │ │ ├── language-model │ │ │ │ ├── v2 │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── language-model-v2-call-options.ts │ │ │ │ │ ├── language-model-v2-call-warning.ts │ │ │ │ │ ├── language-model-v2-content.ts │ │ │ │ │ ├── language-model-v2-data-content.ts │ │ │ │ │ ├── language-model-v2-file.ts │ │ │ │ │ ├── language-model-v2-finish-reason.ts │ │ │ │ │ ├── language-model-v2-function-tool.ts │ │ │ │ │ ├── language-model-v2-prompt.ts │ │ │ │ │ ├── language-model-v2-provider-defined-tool.ts │ │ │ │ │ ├── language-model-v2-reasoning.ts │ │ │ │ │ ├── language-model-v2-response-metadata.ts │ │ │ │ │ ├── language-model-v2-source.ts │ │ │ │ │ ├── language-model-v2-stream-part.ts │ │ │ │ │ ├── language-model-v2-text.ts │ │ │ │ │ ├── language-model-v2-tool-call.ts │ │ │ │ │ ├── language-model-v2-tool-choice.ts │ │ │ │ │ ├── language-model-v2-tool-result.ts │ │ │ │ │ ├── language-model-v2-usage.ts │ │ │ │ │ └── language-model-v2.ts │ │ │ │ └── index.ts │ │ │ ├── language-model-middleware │ │ │ │ ├── v2 │ │ │ │ │ ├── index.ts │ │ │ │ │ └── language-model-v2-middleware.ts │ │ │ │ └── index.ts │ │ │ ├── provider │ │ │ │ ├── v2 │ │ │ │ │ ├── index.ts │ │ │ │ │ └── provider-v2.ts │ │ │ │ └── index.ts │ │ │ ├── shared │ │ │ │ ├── v2 │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── shared-v2-headers.ts │ │ │ │ │ ├── shared-v2-provider-metadata.ts │ │ │ │ │ └── shared-v2-provider-options.ts │ │ │ │ └── index.ts │ │ │ ├── speech-model │ │ │ │ ├── v2 │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── speech-model-v2-call-options.ts │ │ │ │ │ ├── speech-model-v2-call-warning.ts │ │ │ │ │ └── speech-model-v2.ts │ │ │ │ └── index.ts │ │ │ ├── transcription-model │ │ │ │ ├── v2 │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── transcription-model-v2-call-options.ts │ │ │ │ │ ├── transcription-model-v2-call-warning.ts │ │ │ │ │ └── transcription-model-v2.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ └── tsup.config.ts │ ├── provider-utils │ │ ├── src │ │ │ ├── test │ │ │ │ ├── convert-array-to-async-iterable.ts │ │ │ │ ├── convert-array-to-readable-stream.ts │ │ │ │ ├── convert-async-iterable-to-array.ts │ │ │ │ ├── convert-readable-stream-to-array.ts │ │ │ │ ├── convert-response-stream-to-array.ts │ │ │ │ ├── index.ts │ │ │ │ ├── is-node-version.ts │ │ │ │ ├── mock-id.ts │ │ │ │ └── test-server.ts │ │ │ ├── types │ │ │ │ ├── assistant-model-message.ts │ │ │ │ ├── content-part.ts │ │ │ │ ├── data-content.ts │ │ │ │ ├── index.ts │ │ │ │ ├── model-message.ts │ │ │ │ ├── provider-options.ts │ │ │ │ ├── system-model-message.ts │ │ │ │ ├── tool-call.ts │ │ │ │ ├── tool-model-message.ts │ │ │ │ ├── tool-result.ts │ │ │ │ ├── tool.test-d.ts │ │ │ │ ├── tool.ts │ │ │ │ └── user-model-message.ts │ │ │ ├── combine-headers.ts │ │ │ ├── convert-async-iterator-to-readable-stream.ts │ │ │ ├── delay.test.ts │ │ │ ├── delay.ts │ │ │ ├── extract-response-headers.ts │ │ │ ├── fetch-function.ts │ │ │ ├── generate-id.test.ts │ │ │ ├── generate-id.ts │ │ │ ├── get-error-message.ts │ │ │ ├── get-from-api.test.ts │ │ │ ├── get-from-api.ts │ │ │ ├── handle-fetch-error.ts │ │ │ ├── index.ts │ │ │ ├── is-abort-error.ts │ │ │ ├── is-url-supported.test.ts │ │ │ ├── is-url-supported.ts │ │ │ ├── load-api-key.ts │ │ │ ├── load-optional-setting.ts │ │ │ ├── load-setting.ts │ │ │ ├── parse-json-event-stream.ts │ │ │ ├── parse-json.test.ts │ │ │ ├── parse-json.ts │ │ │ ├── parse-provider-options.ts │ │ │ ├── post-to-api.ts │ │ │ ├── provider-defined-tool-factory.ts │ │ │ ├── remove-undefined-entries.test.ts │ │ │ ├── remove-undefined-entries.ts │ │ │ ├── resolve.test.ts │ │ │ ├── resolve.ts │ │ │ ├── response-handler.test.ts │ │ │ ├── response-handler.ts │ │ │ ├── schema.ts │ │ │ ├── secure-json-parse.test.ts │ │ │ ├── secure-json-parse.ts │ │ │ ├── uint8-utils.ts │ │ │ ├── validate-types.test.ts │ │ │ ├── validate-types.ts │ │ │ ├── validator.ts │ │ │ ├── without-trailing-slash.ts │ │ │ ├── zod-schema.test.ts │ │ │ └── zod-schema.ts │ │ ├── test.d.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── react │ │ ├── src │ │ │ ├── util │ │ │ │ └── use-stable-value.ts │ │ │ ├── chat.react.ts │ │ │ ├── index.ts │ │ │ ├── setup-test-component.tsx │ │ │ ├── throttle.ts │ │ │ ├── use-chat.ts │ │ │ ├── use-chat.ui.test.tsx │ │ │ ├── use-completion.ts │ │ │ ├── use-completion.ui.test.tsx │ │ │ ├── use-object.ts │ │ │ └── use-object.ui.test.tsx │ │ ├── .eslintrc.js │ │ ├── tsup.config.ts │ │ └── vitest.config.js │ ├── replicate │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── replicate-error.ts │ │ │ ├── replicate-image-model.test.ts │ │ │ ├── replicate-image-model.ts │ │ │ ├── replicate-image-settings.ts │ │ │ ├── replicate-provider.test.ts │ │ │ └── replicate-provider.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── revai │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── revai-api-types.ts │ │ │ ├── revai-config.ts │ │ │ ├── revai-error.test.ts │ │ │ ├── revai-error.ts │ │ │ ├── revai-provider.ts │ │ │ ├── revai-transcription-model.test.ts │ │ │ ├── revai-transcription-model.ts │ │ │ └── revai-transcription-options.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── rsc │ │ ├── src │ │ │ ├── shared-client │ │ │ │ ├── context.tsx │ │ │ │ └── index.ts │ │ │ ├── stream-ui │ │ │ │ ├── index.tsx │ │ │ │ ├── stream-ui.tsx │ │ │ │ └── stream-ui.ui.test.tsx │ │ │ ├── streamable-ui │ │ │ │ ├── create-streamable-ui.tsx │ │ │ │ ├── create-streamable-ui.ui.test.tsx │ │ │ │ └── create-suspended-chunk.tsx │ │ │ ├── streamable-value │ │ │ │ ├── create-streamable-value.test.tsx │ │ │ │ ├── create-streamable-value.ts │ │ │ │ ├── is-streamable-value.ts │ │ │ │ ├── read-streamable-value.tsx │ │ │ │ ├── read-streamable-value.ui.test.tsx │ │ │ │ ├── streamable-value.ts │ │ │ │ └── use-streamable-value.tsx │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ ├── util │ │ │ │ ├── constants.ts │ │ │ │ ├── create-resolvable-promise.ts │ │ │ │ ├── is-async-generator.ts │ │ │ │ ├── is-function.ts │ │ │ │ └── is-generator.ts │ │ │ ├── ai-state.test.ts │ │ │ ├── ai-state.tsx │ │ │ ├── index.ts │ │ │ ├── provider.tsx │ │ │ ├── rsc-client.ts │ │ │ ├── rsc-server.ts │ │ │ ├── types.test-d.ts │ │ │ └── types.ts │ │ ├── tests │ │ │ └── e2e │ │ │ ├── next-server │ │ │ │ └── app │ │ │ │ ├── rsc │ │ │ │ │ ├── actions.jsx │ │ │ │ │ ├── client-utils.js │ │ │ │ │ ├── client.js │ │ │ │ │ └── page.js │ │ │ │ ├── layout.js │ │ │ │ └── page.js │ │ │ └── spec │ │ │ └── streamable.e2e.test.ts │ │ ├── .eslintrc.js │ │ ├── playwright.config.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ ├── vitest.node.config.js │ │ └── vitest.ui.react.config.js │ ├── svelte │ │ ├── src │ │ │ ├── chat.svelte.test.ts │ │ │ ├── chat.svelte.ts │ │ │ ├── completion-context.svelte.ts │ │ │ ├── completion.svelte.test.ts │ │ │ ├── completion.svelte.ts │ │ │ ├── context-provider.ts │ │ │ ├── index.ts │ │ │ ├── structured-object-context.svelte.ts │ │ │ ├── structured-object.svelte.test.ts │ │ │ ├── structured-object.svelte.ts │ │ │ └── utils.svelte.ts │ │ ├── eslint.config.js │ │ ├── svelte.config.js │ │ ├── vite.config.ts │ │ └── vitest-setup-client.ts │ ├── togetherai │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── togetherai-chat-options.ts │ │ │ ├── togetherai-completion-options.ts │ │ │ ├── togetherai-embedding-options.ts │ │ │ ├── togetherai-image-model.test.ts │ │ │ ├── togetherai-image-model.ts │ │ │ ├── togetherai-image-settings.ts │ │ │ ├── togetherai-provider.test.ts │ │ │ └── togetherai-provider.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── valibot │ │ ├── src │ │ │ ├── index.ts │ │ │ └── valibot-schema.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── vercel │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── vercel-chat-options.ts │ │ │ ├── vercel-provider.test.ts │ │ │ └── vercel-provider.ts │ │ ├── tsup.config.ts │ │ ├── vitest.edge.config.js │ │ └── vitest.node.config.js │ ├── vue │ │ ├── src │ │ │ ├── chat.vue.ts │ │ │ ├── chat.vue.ui.test.tsx │ │ │ ├── index.ts │ │ │ ├── setup-test-component.ts │ │ │ ├── shims-vue.d.ts │ │ │ ├── use-completion.ts │ │ │ └── use-completion.ui.test.ts │ │ ├── .eslintrc.js │ │ ├── tsup.config.ts │ │ └── vitest.config.js │ └── xai │ ├── src │ │ ├── convert-to-xai-chat-messages.test.ts │ │ ├── convert-to-xai-chat-messages.ts │ │ ├── get-response-metadata.ts │ │ ├── index.ts │ │ ├── map-xai-finish-reason.ts │ │ ├── xai-chat-language-model.test.ts │ │ ├── xai-chat-language-model.ts │ │ ├── xai-chat-options.ts │ │ ├── xai-chat-prompt.ts │ │ ├── xai-error.ts │ │ ├── xai-image-settings.ts │ │ ├── xai-prepare-tools.ts │ │ ├── xai-provider.test.ts │ │ └── xai-provider.ts │ ├── tsup.config.ts │ ├── vitest.edge.config.js │ └── vitest.node.config.js ├── tools │ ├── analyze-downloads │ │ └── src │ │ ├── analyze-market.ts │ │ ├── analyze-providers.ts │ │ └── analyze-versions.ts │ ├── eslint-config │ │ └── index.js │ └── generate-llms-txt │ └── src │ └── generate-llms-txt.ts └── .eslintrc.js --- File: /ai/.github/workflows/actions/verify-changesets/index.js --- 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 }, ); } } } --- File: /ai/.github/workflows/actions/verify-changesets/test.js --- 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', ); }); --- File: /ai/content/cookbook/00-guides/01-rag-chatbot.mdx --- --- title: RAG Agent description: Learn how to build a RAG Agent with the AI SDK and Next.js tags: [ 'rag', 'chatbot', 'next', 'embeddings', 'database', 'retrieval', 'memory', 'agent', ] --- # RAG Agent Guide In this guide, you will learn how to build a retrieval-augmented generation (RAG) agent. <video src="/images/rag-guide-demo.mp4" autoplay height={540} width={910} controls playsinline /> Before we dive in, let's look at what RAG is, and why we would want to use it. ### What is RAG? RAG stands for retrieval augmented generation. In simple terms, RAG is the process of providing a Large Language Model (LLM) with specific information relevant to the prompt. ### Why is RAG important? While LLMs are powerful, the information they can reason on is restricted to the data they were trained on. This problem becomes apparent when asking an LLM for information outside of their training data, like proprietary data or common knowledge that has occurred after the model’s training cutoff. RAG solves this problem by fetching information relevant to the prompt and then passing that to the model as context. To illustrate with a basic example, imagine asking the model for your favorite food: ```txt **input** What is my favorite food? **generation** I don't have access to personal information about individuals, including their favorite foods. ``` Not surprisingly, the model doesn’t know. But imagine, alongside your prompt, the model received some extra context: ```txt **input** Respond to the user's prompt using only the provided context. user prompt: 'What is my favorite food?' context: user loves chicken nuggets **generation** Your favorite food is chicken nuggets! ``` Just like that, you have augmented the model’s generation by providing relevant information to the query. Assuming the model has the appropriate information, it is now highly likely to return an accurate response to the users query. But how does it retrieve the relevant information? The answer relies on a concept called embedding. <Note> You could fetch any context for your RAG application (eg. Google search). Embeddings and Vector Databases are just a specific retrieval approach to achieve semantic search. </Note> ### Embedding [Embeddings](/docs/ai-sdk-core/embeddings) are a way to represent words, phrases, or images as vectors in a high-dimensional space. In this space, similar words are close to each other, and the distance between words can be used to measure their similarity. In practice, this means that if you embedded the words `cat` and `dog`, you would expect them to be plotted close to each other in vector space. The process of calculating the similarity between two vectors is called ‘cosine similarity’ where a value of 1 would indicate high similarity and a value of -1 would indicate high opposition. <Note> Don’t worry if this seems complicated. a high level understanding is all you need to get started! For a more in-depth introduction to embeddings, check out [this guide](https://jalammar.github.io/illustrated-word2vec/). </Note> As mentioned above, embeddings are a way to represent the semantic meaning of **words and phrases**. The implication here is that the larger the input to your embedding, the lower quality the embedding will be. So how would you approach embedding content longer than a simple phrase? ### Chunking Chunking refers to the process of breaking down a particular source material into smaller pieces. There are many different approaches to chunking and it’s worth experimenting as the most effective approach can differ by use case. A simple and common approach to chunking (and what you will be using in this guide) is separating written content by sentences. Once your source material is appropriately chunked, you can embed each one and then store the embedding and the chunk together in a database. Embeddings can be stored in any database that supports vectors. For this tutorial, you will be using [Postgres](https://www.postgresql.org/) alongside the [pgvector](https://github.com/pgvector/pgvector) plugin. <MDXImage srcLight="/images/rag-guide-1.png" srcDark="/images/rag-guide-1-dark.png" width={800} height={800} /> ### All Together Now Combining all of this together, RAG is the process of enabling the model to respond with information outside of it’s training data by embedding a users query, retrieving the relevant source material (chunks) with the highest semantic similarity, and then passing them alongside the initial query as context. Going back to the example where you ask the model for your favorite food, the prompt preparation process would look like this. <MDXImage srcLight="/images/rag-guide-2.png" srcDark="/images/rag-guide-2-dark.png" width={800} height={800} /> By passing the appropriate context and refining the model’s objective, you are able to fully leverage its strengths as a reasoning machine. Onto the project! ## Project Setup In this project, you will build a agent that will only respond with information that it has within its knowledge base. The agent will be able to both store and retrieve information. This project has many interesting use cases from customer support through to building your own second brain! This project will use the following stack: - [Next.js](https://nextjs.org) 14 (App Router) - [ AI SDK ](/docs) - [OpenAI](https://openai.com) - [ Drizzle ORM ](https://orm.drizzle.team) - [ Postgres ](https://www.postgresql.org/) with [ pgvector ](https://github.com/pgvector/pgvector) - [ shadcn-ui ](https://ui.shadcn.com) and [ TailwindCSS ](https://tailwindcss.com) for styling ### Clone Repo To reduce the scope of this guide, you will be starting with a [repository](https://github.com/vercel/ai-sdk-rag-starter) that already has a few things set up for you: - Drizzle ORM (`lib/db`) including an initial migration and a script to migrate (`db:migrate`) - a basic schema for the `resources` table (this will be for source material) - a Server Action for creating a `resource` To get started, clone the starter repository with the following command: <Snippet text={[ 'git clone https://github.com/vercel/ai-sdk-rag-starter', 'cd ai-sdk-rag-starter', ]} /> First things first, run the following command to install the project’s dependencies: <Snippet text="pnpm install" /> ### Create Database You will need a Postgres database to complete this tutorial. If you don't have Postgres setup on your local machine you can: - Create a free Postgres database with Vercel (recommended - see instructions below); or - Follow [this guide](https://www.prisma.io/dataguide/postgresql/setting-up-a-local-postgresql-database) to set it up locally #### Setting up Postgres with Vercel To set up a Postgres instance on your Vercel account: 1. Go to [Vercel.com](https://vercel.com) and make sure you're logged in 1. Navigate to your team homepage 1. Click on the **Integrations** tab 1. Click **Browse Marketplace** 1. Look for the **Storage** option in the sidebar 1. Select the **Neon** option (recommended, but any other PostgreSQL database provider should work) 1. Click **Install**, then click **Install** again in the top right corner 1. On the "Get Started with Neon" page, click **Create Database** on the right 1. Select your region (e.g., Washington, D.C., U.S. East) 1. Turn off **Auth** 1. Click **Continue** 1. Name your database (you can use the default name or rename it to something like "RagTutorial") 1. Click **Create** in the bottom right corner 1. After seeing "Database created successfully", click **Done** 1. You'll be redirected to your database instance 1. In the Quick Start section, click **Show secrets** 1. Copy the full `DATABASE_URL` environment variable ### Migrate Database Once you have a Postgres database, you need to add the connection string as an environment secret. Make a copy of the `.env.example` file and rename it to `.env`. <Snippet text="cp .env.example .env" /> Open the new `.env` file. You should see an item called `DATABASE_URL`. Copy in your database connection string after the equals sign. With that set up, you can now run your first database migration. Run the following command: <Snippet text="pnpm db:migrate" /> This will first add the `pgvector` extension to your database. Then it will create a new table for your `resources` schema that is defined in `lib/db/schema/resources.ts`. This schema has four columns: `id`, `content`, `createdAt`, and `updatedAt`. <Note> If you experience an error with the migration, see the [troubleshooting section](#troubleshooting-migration-error) below. </Note> ### OpenAI API Key For this guide, you will need an OpenAI API key. To generate an API key, go to [platform.openai.com](http://platform.openai.com/). Once you have your API key, paste it into your `.env` file (`OPENAI_API_KEY`). ## Build Let’s build a quick task list of what needs to be done: 1. Create a table in your database to store embeddings 2. Add logic to chunk and create embeddings when creating resources 3. Create an agent 4. Give the agent tools to query / create resources for it’s knowledge base ### Create Embeddings Table Currently, your application has one table (`resources`) which has a column (`content`) for storing content. Remember, each `resource` (source material) will have to be chunked, embedded, and then stored. Let’s create a table called `embeddings` to store these chunks. Create a new file (`lib/db/schema/embeddings.ts`) and add the following code: ```tsx filename="lib/db/schema/embeddings.ts" import { nanoid } from '@/lib/utils'; import { index, pgTable, text, varchar, vector } from 'drizzle-orm/pg-core'; import { resources } from './resources'; export const embeddings = pgTable( 'embeddings', { id: varchar('id', { length: 191 }) .primaryKey() .$defaultFn(() => nanoid()), resourceId: varchar('resource_id', { length: 191 }).references( () => resources.id, { onDelete: 'cascade' }, ), content: text('content').notNull(), embedding: vector('embedding', { dimensions: 1536 }).notNull(), }, table => ({ embeddingIndex: index('embeddingIndex').using( 'hnsw', table.embedding.op('vector_cosine_ops'), ), }), ); ``` This table has four columns: - `id` - unique identifier - `resourceId` - a foreign key relation to the full source material - `content` - the plain text chunk - `embedding` - the vector representation of the plain text chunk To perform similarity search, you also need to include an index ([HNSW](https://github.com/pgvector/pgvector?tab=readme-ov-file#hnsw) or [IVFFlat](https://github.com/pgvector/pgvector?tab=readme-ov-file#ivfflat)) on this column for better performance. To push this change to the database, run the following command: <Snippet text="pnpm db:push" /> ### Add Embedding Logic Now that you have a table to store embeddings, it’s time to write the logic to create the embeddings. Create a file with the following command: <Snippet text="mkdir lib/ai && touch lib/ai/embedding.ts" /> ### Generate Chunks Remember, to create an embedding, you will start with a piece of source material (unknown length), break it down into smaller chunks, embed each chunk, and then save the chunk to the database. Let’s start by creating a function to break the source material into small chunks. ```tsx filename="lib/ai/embedding.ts" const generateChunks = (input: string): string[] => { return input .trim() .split('.') .filter(i => i !== ''); }; ``` This function will take an input string and split it by periods, filtering out any empty items. This will return an array of strings. It is worth experimenting with different chunking techniques in your projects as the best technique will vary. ### Install AI SDK You will use the AI SDK to create embeddings. This will require two more dependencies, which you can install by running the following command: <Snippet text="pnpm add ai @ai-sdk/react @ai-sdk/openai" /> This will install the [AI SDK](/docs), AI SDK's React hooks, and AI SDK's [OpenAI provider](/providers/ai-sdk-providers/openai). <Note> The AI SDK is designed to be a unified interface to interact with any large language model. This means that you can change model and providers with just one line of code! Learn more about [available providers](/providers) and [building custom providers](/providers/community-providers/custom-providers) in the [providers](/providers) section. </Note> ### Generate Embeddings Let’s add a function to generate embeddings. Copy the following code into your `lib/ai/embedding.ts` file. ```tsx filename="lib/ai/embedding.ts" highlight="1-2,4,13-22" import { embedMany } from 'ai'; import { openai } from '@ai-sdk/openai'; const embeddingModel = openai.embedding('text-embedding-ada-002'); const generateChunks = (input: string): string[] => { return input .trim() .split('.') .filter(i => i !== ''); }; export const generateEmbeddings = async ( value: string, ): Promise<Array<{ embedding: number[]; content: string }>> => { const chunks = generateChunks(value); const { embeddings } = await embedMany({ model: embeddingModel, values: chunks, }); return embeddings.map((e, i) => ({ content: chunks[i], embedding: e })); }; ``` In this code, you first define the model you want to use for the embeddings. In this example, you are using OpenAI’s `text-embedding-ada-002` embedding model. Next, you create an asynchronous function called `generateEmbeddings`. This function will take in the source material (`value`) as an input and return a promise of an array of objects, each containing an embedding and content. Within the function, you first generate chunks for the input. Then, you pass those chunks to the [`embedMany`](/docs/reference/ai-sdk-core/embed-many) function imported from the AI SDK which will return embeddings of the chunks you passed in. Finally, you map over and return the embeddings in a format that is ready to save in the database. ### Update Server Action Open the file at `lib/actions/resources.ts`. This file has one function, `createResource`, which, as the name implies, allows you to create a resource. ```tsx filename="lib/actions/resources.ts" 'use server'; import { NewResourceParams, insertResourceSchema, resources, } from '@/lib/db/schema/resources'; import { db } from '../db'; export const createResource = async (input: NewResourceParams) => { try { const { content } = insertResourceSchema.parse(input); const [resource] = await db .insert(resources) .values({ content }) .returning(); return 'Resource successfully created.'; } catch (e) { if (e instanceof Error) return e.message.length > 0 ? e.message : 'Error, please try again.'; } }; ``` This function is a [Server Action](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#with-client-components), as denoted by the `“use server”;` directive at the top of the file. This means that it can be called anywhere in your Next.js application. This function will take an input, run it through a [Zod](https://zod.dev) schema to ensure it adheres to the correct schema, and then creates a new resource in the database. This is the ideal location to generate and store embeddings of the newly created resources. Update the file with the following code: ```tsx filename="lib/actions/resources.ts" highlight="9-10,21-27,29" 'use server'; import { NewResourceParams, insertResourceSchema, resources, } from '@/lib/db/schema/resources'; import { db } from '../db'; import { generateEmbeddings } from '../ai/embedding'; import { embeddings as embeddingsTable } from '../db/schema/embeddings'; export const createResource = async (input: NewResourceParams) => { try { const { content } = insertResourceSchema.parse(input); const [resource] = await db .insert(resources) .values({ content }) .returning(); const embeddings = await generateEmbeddings(content); await db.insert(embeddingsTable).values( embeddings.map(embedding => ({ resourceId: resource.id, ...embedding, })), ); return 'Resource successfully created and embedded.'; } catch (error) { return error instanceof Error && error.message.length > 0 ? error.message : 'Error, please try again.'; } }; ``` First, you call the `generateEmbeddings` function created in the previous step, passing in the source material (`content`). Once you have your embeddings (`e`) of the source material, you can save them to the database, passing the `resourceId` alongside each embedding. ### Create Root Page Great! Let's build the frontend. The AI SDK’s [`useChat`](/docs/reference/ai-sdk-ui/use-chat) hook allows you to easily create a conversational user interface for your agent. Replace your root page (`app/page.tsx`) with the following code. ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat(); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <div className="space-y-4"> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> <div> <div className="font-bold">{m.role}</div> {m.parts.map(part => { switch (part.type) { case 'text': return <p>{part.text}</p>; } })} </div> </div> ))} </div> <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` The `useChat` hook enables the streaming of chat messages from your AI provider (you will be using OpenAI), manages the state for chat input, and updates the UI automatically as new messages are received. Run the following command to start the Next.js dev server: <Snippet text="pnpm run dev" /> Head to [http://localhost:3000](http://localhost:3000/). You should see an empty screen with an input bar floating at the bottom. Try to send a message. The message shows up in the UI for a fraction of a second and then disappears. This is because you haven’t set up the corresponding API route to call the model! By default, `useChat` will send a POST request to the `/api/chat` endpoint with the `messages` as the request body. <Note>You can customize the endpoint in the useChat configuration object</Note> ### Create API Route In Next.js, you can create custom request handlers for a given route using [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers). Route Handlers are defined in a `route.ts` file and can export HTTP methods like `GET`, `POST`, `PUT`, `PATCH` etc. Create a file at `app/api/chat/route.ts` by running the following command: <Snippet text="mkdir -p app/api/chat && touch app/api/chat/route.ts" /> Open the file and add the following code: ```tsx filename="app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; 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(); } ``` In this code, you declare and export an asynchronous function called POST. You retrieve the `messages` from the request body and then pass them to the [`streamText`](/docs/reference/ai-sdk-core/stream-text) function imported from the AI SDK, alongside the model you would like to use. Finally, you return the model’s response in `UIMessageStreamResponse` format. Head back to the browser and try to send a message again. You should see a response from the model streamed directly in! ### Refining your prompt While you now have a working agent, it isn't doing anything special. Let’s add system instructions to refine and restrict the model’s behavior. In this case, you want the model to only use information it has retrieved to generate responses. Update your route handler with the following code: ```tsx filename="app/api/chat/route.ts" highlight="12-14" import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), system: `You are a helpful assistant. Check your knowledge base before answering any questions. Only respond to questions using information from tool calls. if no relevant information is found in the tool calls, respond, "Sorry, I don't know."`, messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } ``` Head back to the browser and try to ask the model what your favorite food is. The model should now respond exactly as you instructed above (“Sorry, I don’t know”) given it doesn’t have any relevant information. In its current form, your agent is now, well, useless. How do you give the model the ability to add and query information? ### Using Tools A [tool](/docs/foundations/tools) is a function that can be called by the model to perform a specific task. You can think of a tool like a program you give to the model that it can run as and when it deems necessary. Let’s see how you can create a tool to give the model the ability to create, embed and save a resource to your agents’ knowledge base. ### Add Resource Tool Update your route handler with the following code: ```tsx filename="app/api/chat/route.ts" highlight="18-29" import { createResource } from '@/lib/actions/resources'; import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, tool, UIMessage } from 'ai'; import { z } from 'zod'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), system: `You are a helpful assistant. Check your knowledge base before answering any questions. Only respond to questions using information from tool calls. if no relevant information is found in the tool calls, respond, "Sorry, I don't know."`, messages: convertToModelMessages(messages), tools: { addResource: tool({ description: `add a resource to your knowledge base. If the user provides a random piece of knowledge unprompted, use this tool without asking for confirmation.`, inputSchema: z.object({ content: z .string() .describe('the content or resource to add to the knowledge base'), }), execute: async ({ content }) => createResource({ content }), }), }, }); return result.toUIMessageStreamResponse(); } ``` In this code, you define a tool called `addResource`. This tool has three elements: - **description**: description of the tool that will influence when the tool is picked. - **inputSchema**: [Zod schema](/docs/foundations/tools#schema-specification-and-validation-with-zod) that defines the input necessary for the tool to run. - **execute**: An asynchronous function that is called with the arguments from the tool call. In simple terms, on each generation, the model will decide whether it should call the tool. If it deems it should call the tool, it will extract the input and then append a new `message` to the `messages` array of type `tool-call`. The AI SDK will then run the `execute` function with the parameters provided by the `tool-call` message. Head back to the browser and tell the model your favorite food. You should see an empty response in the UI. Did anything happen? Let’s see. Run the following command in a new terminal window. <Snippet text="pnpm db:studio" /> This will start Drizzle Studio where we can view the rows in our database. You should see a new row in both the `embeddings` and `resources` table with your favorite food! Let’s make a few changes in the UI to communicate to the user when a tool has been called. Head back to your root page (`app/page.tsx`) and add the following code: ```tsx filename="app/page.tsx" highlight="14-32" 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat(); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <div className="space-y-4"> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> <div> <div className="font-bold">{m.role}</div> {m.parts.map(part => { switch (part.type) { case 'text': return <p>{part.text}</p>; case 'tool-addResource': case 'tool-getInformation': return ( <p> call{part.state === 'output-available' ? 'ed' : 'ing'}{' '} tool: {part.type} <pre className="my-4 bg-zinc-100 p-2 rounded-sm"> {JSON.stringify(part.input, null, 2)} </pre> </p> ); } })} </div> </div> ))} </div> <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` With this change, you now conditionally render the tool that has been called directly in the UI. Save the file and head back to browser. Tell the model your favorite movie. You should see which tool is called in place of the model’s typical text response. <Note> Don't worry about the `tool-getInformation` tool case in the switch statement - we'll add that tool in a later section. </Note> ### Improving UX with Multi-Step Calls It would be nice if the model could summarize the action too. However, technically, once the model calls a tool, it has completed its generation as it ‘generated’ a tool call. How could you achieve this desired behaviour? The AI SDK has a feature called [`stopWhen`](/docs/ai-sdk-core/tools-and-tool-calling#multi-step-calls) which allows stopping conditions when the model generates a tool call. If those stopping conditions haven't been hit, the AI SDK will automatically send tool call results back to the model! Open your root page (`api/chat/route.ts`) and add the following key to the `streamText` configuration object: ```tsx filename="api/chat/route.ts" highlight="8,24" import { createResource } from '@/lib/actions/resources'; import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, tool, UIMessage, stepCountIs, } from 'ai'; import { z } from 'zod'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), system: `You are a helpful assistant. Check your knowledge base before answering any questions. Only respond to questions using information from tool calls. if no relevant information is found in the tool calls, respond, "Sorry, I don't know."`, messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), tools: { addResource: tool({ description: `add a resource to your knowledge base. If the user provides a random piece of knowledge unprompted, use this tool without asking for confirmation.`, inputSchema: z.object({ content: z .string() .describe('the content or resource to add to the knowledge base'), }), execute: async ({ content }) => createResource({ content }), }), }, }); return result.toUIMessageStreamResponse(); } ``` Head back to the browser and tell the model your favorite pizza topping (note: pineapple is not an option). You should see a follow-up response from the model confirming the action. ### Retrieve Resource Tool The model can now add and embed arbitrary information to your knowledge base. However, it still isn’t able to query it. Let’s create a new tool to allow the model to answer questions by finding relevant information in your knowledge base. To find similar content, you will need to embed the users query, search the database for semantic similarities, then pass those items to the model as context alongside the query. To achieve this, let’s update your embedding logic file (`lib/ai/embedding.ts`): ```tsx filename="lib/ai/embedding.ts" highlight="1,3-5,27-34,36-49" import { embed, embedMany } from 'ai'; import { openai } from '@ai-sdk/openai'; import { db } from '../db'; import { cosineDistance, desc, gt, sql } from 'drizzle-orm'; import { embeddings } from '../db/schema/embeddings'; const embeddingModel = openai.embedding('text-embedding-ada-002'); const generateChunks = (input: string): string[] => { return input .trim() .split('.') .filter(i => i !== ''); }; export const generateEmbeddings = async ( value: string, ): Promise<Array<{ embedding: number[]; content: string }>> => { const chunks = generateChunks(value); const { embeddings } = await embedMany({ model: embeddingModel, values: chunks, }); return embeddings.map((e, i) => ({ content: chunks[i], embedding: e })); }; export const generateEmbedding = async (value: string): Promise<number[]> => { const input = value.replaceAll('\\n', ' '); const { embedding } = await embed({ model: embeddingModel, value: input, }); return embedding; }; export const findRelevantContent = async (userQuery: string) => { const userQueryEmbedded = await generateEmbedding(userQuery); const similarity = sql<number>`1 - (${cosineDistance( embeddings.embedding, userQueryEmbedded, )})`; const similarGuides = await db .select({ name: embeddings.content, similarity }) .from(embeddings) .where(gt(similarity, 0.5)) .orderBy(t => desc(t.similarity)) .limit(4); return similarGuides; }; ``` In this code, you add two functions: - `generateEmbedding`: generate a single embedding from an input string - `findRelevantContent`: embeds the user’s query, searches the database for similar items, then returns relevant items With that done, it’s onto the final step: creating the tool. Go back to your route handler (`api/chat/route.ts`) and add a new tool called `getInformation`: ```ts filename="api/chat/route.ts" highlight="11,37-43" import { createResource } from '@/lib/actions/resources'; import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, tool, UIMessage, stepCountIs, } from 'ai'; import { z } from 'zod'; import { findRelevantContent } from '@/lib/ai/embedding'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), system: `You are a helpful assistant. Check your knowledge base before answering any questions. Only respond to questions using information from tool calls. if no relevant information is found in the tool calls, respond, "Sorry, I don't know."`, tools: { addResource: tool({ description: `add a resource to your knowledge base. If the user provides a random piece of knowledge unprompted, use this tool without asking for confirmation.`, inputSchema: z.object({ content: z .string() .describe('the content or resource to add to the knowledge base'), }), execute: async ({ content }) => createResource({ content }), }), getInformation: tool({ description: `get information from your knowledge base to answer questions.`, inputSchema: z.object({ question: z.string().describe('the users question'), }), execute: async ({ question }) => findRelevantContent(question), }), }, }); return result.toUIMessageStreamResponse(); } ``` Head back to the browser, refresh the page, and ask for your favorite food. You should see the model call the `getInformation` tool, and then use the relevant information to formulate a response! ## Conclusion Congratulations, you have successfully built an AI agent that can dynamically add and retrieve information to and from a knowledge base. Throughout this guide, you learned how to create and store embeddings, set up server actions to manage resources, and use tools to extend the capabilities of your agent. ## Troubleshooting Migration Error If you experience an error with the migration, open your migration file (`lib/db/migrations/0000_yielding_bloodaxe.sql`), cut (copy and remove) the first line, and run it directly on your postgres instance. You should now be able to run the updated migration. If you're using the Vercel setup above, you can run the command directly by either: - Going to the Neon console and entering the command there, or - Going back to the Vercel platform, navigating to the Quick Start section of your database, and finding the PSQL connection command (second tab). This will connect to your instance in the terminal where you can run the command directly. [More info](https://github.com/vercel/ai-sdk-rag-starter/issues/1). --- File: /ai/content/cookbook/00-guides/02-multi-modal-chatbot.mdx --- --- title: Multi-Modal Agent description: Learn how to build a multi-modal agent that can process images and PDFs with the AI SDK. tags: ['multi-modal', 'agent', 'images', 'pdf', 'vision', 'next'] --- # Multi-Modal Agent In this guide, you will build a multi-modal agent capable of understanding both images and PDFs. Multi-modal refers to the ability of the agent to understand and generate responses in multiple formats. In this guide, we'll focus on images and PDFs - two common document types that modern language models can process natively. <Note> For a complete list of providers and their multi-modal capabilities, visit the [providers documentation](/providers/ai-sdk-providers). </Note> We'll build this agent using OpenAI's GPT-4o, but the same code works seamlessly with other providers - you can switch between them by changing just one line of code. ## Prerequisites To follow this quickstart, you'll need: - Node.js 18+ and pnpm installed on your local development machine. - An OpenAI API key. If you haven't obtained your OpenAI API key, you can do so by [signing up](https://platform.openai.com/signup/) on the OpenAI website. ## Create Your Application Start by creating a new Next.js application. This command will create a new directory named `multi-modal-agent` and set up a basic Next.js application inside it. <div className="mb-4"> <Note> Be sure to select yes when prompted to use the App Router. If you are looking for the Next.js Pages Router quickstart guide, you can find it [here](/docs/getting-started/nextjs-pages-router). </Note> </div> <Snippet text="pnpm create next-app@latest multi-modal-agent" /> Navigate to the newly created directory: <Snippet text="cd multi-modal-agent" /> ### Install dependencies Install `ai` and `@ai-sdk/openai`, the AI SDK package and the AI SDK's [ OpenAI provider ](/providers/ai-sdk-providers/openai) respectively. <Note> The AI SDK is designed to be a unified interface to interact with any large language model. This means that you can change model and providers with just one line of code! Learn more about [available providers](/providers) and [building custom providers](/providers/community-providers/custom-providers) in the [providers](/providers) section. </Note> <div className="my-4"> <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add ai @ai-sdk/react @ai-sdk/openai" dark /> </Tab> <Tab> <Snippet text="npm install ai @ai-sdk/react @ai-sdk/openai" dark /> </Tab> <Tab> <Snippet text="yarn add ai @ai-sdk/react @ai-sdk/openai" dark /> </Tab> </Tabs> </div> ### Configure OpenAI API key Create a `.env.local` file in your project root and add your OpenAI API Key. This key is used to authenticate your application with the OpenAI service. <Snippet text="touch .env.local" /> Edit the `.env.local` file: ```env filename=".env.local" OPENAI_API_KEY=xxxxxxxxx ``` Replace `xxxxxxxxx` with your actual OpenAI API key. <Note className="mb-4"> The AI SDK's OpenAI Provider will default to using the `OPENAI_API_KEY` environment variable. </Note> ## Implementation Plan To build a multi-modal agent, you will need to: - Create a Route Handler to handle incoming chat messages and generate responses. - Wire up the UI to display chat messages, provide a user input, and handle submitting new messages. - Add the ability to upload images and PDFs and attach them alongside the chat messages. ## Create a Route Handler Create a route handler, `app/api/chat/route.ts` and add the following code: ```tsx filename="app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { streamText, convertToModelMessages, type UIMessage } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; 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(); } ``` Let's take a look at what is happening in this code: 1. Define an asynchronous `POST` request handler and extract `messages` from the body of the request. The `messages` variable contains a history of the conversation between you and the agent and provides the agent with the necessary context to make the next generation. 2. Convert the UI messages to model messages using `convertToModelMessages`, which transforms the UI-focused message format to the format expected by the language model. 3. Call [`streamText`](/docs/reference/ai-sdk-core/stream-text), which is imported from the `ai` package. This function accepts a configuration object that contains a `model` provider (imported from `@ai-sdk/openai`) and `messages` (converted in step 2). You can pass additional [settings](/docs/ai-sdk-core/settings) to further customise the model's behaviour. 4. The `streamText` function returns a [`StreamTextResult`](/docs/reference/ai-sdk-core/stream-text#result-object). This result object contains the [ `toUIMessageStreamResponse` ](/docs/reference/ai-sdk-core/stream-text#to-ui-message-stream-response) function which converts the result to a streamed response object. 5. Finally, return the result to the client to stream the response. This Route Handler creates a POST request endpoint at `/api/chat`. ## Wire up the UI Now that you have a Route Handler that can query a large language model (LLM), it's time to setup your frontend. [ AI SDK UI ](/docs/ai-sdk-ui) abstracts the complexity of a chat interface into one hook, [`useChat`](/docs/reference/ai-sdk-ui/use-chat). Update your root page (`app/page.tsx`) with the following code to show a list of chat messages and provide a user message input: ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts.map((part, index) => { if (part.type === 'text') { return <span key={`${m.id}-text-${index}`}>{part.text}</span>; } return null; })} </div> ))} <form onSubmit={async event => { event.preventDefault(); sendMessage({ role: 'user', parts: [{ type: 'text', text: input }], }); setInput(''); }} className="fixed bottom-0 w-full max-w-md mb-8 border border-gray-300 rounded shadow-xl" > <input className="w-full p-2" value={input} placeholder="Say something..." onChange={e => setInput(e.target.value)} /> </form> </div> ); } ``` <Note> Make sure you add the `"use client"` directive to the top of your file. This allows you to add interactivity with Javascript. </Note> This page utilizes the `useChat` hook, configured with `DefaultChatTransport` to specify the API endpoint. The `useChat` hook provides multiple utility functions and state variables: - `messages` - the current chat messages (an array of objects with `id`, `role`, and `parts` properties). - `sendMessage` - function to send a new message to the AI. - Each message contains a `parts` array that can include text, images, PDFs, and other content types. - Files are converted to data URLs before being sent to maintain compatibility across different environments. ## Add File Upload To make your agent multi-modal, let's add the ability to upload and send both images and PDFs to the model. In v5, files are sent as part of the message's `parts` array. Files are converted to data URLs using the FileReader API before being sent to the server. Update your root page (`app/page.tsx`) with the following code: ```tsx filename="app/page.tsx" highlight="4-5,10-12,15-39,46-81,87-97" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useRef, useState } from 'react'; import Image from 'next/image'; async function convertFilesToDataURLs(files: FileList) { return Promise.all( Array.from(files).map( file => new Promise<{ type: 'file'; mediaType: string; url: string; }>((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { resolve({ type: 'file', mediaType: file.type, url: reader.result as string, }); }; reader.onerror = reject; reader.readAsDataURL(file); }), ), ); } export default function Chat() { const [input, setInput] = useState(''); const [files, setFiles] = useState<FileList | undefined>(undefined); const fileInputRef = useRef<HTMLInputElement>(null); const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts.map((part, index) => { if (part.type === 'text') { return <span key={`${m.id}-text-${index}`}>{part.text}</span>; } if (part.type === 'file' && part.mediaType?.startsWith('image/')) { return ( <Image key={`${m.id}-image-${index}`} src={part.url} width={500} height={500} alt={`attachment-${index}`} /> ); } if (part.type === 'file' && part.mediaType === 'application/pdf') { return ( <iframe key={`${m.id}-pdf-${index}`} src={part.url} width={500} height={600} title={`pdf-${index}`} /> ); } return null; })} </div> ))} <form className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl space-y-2" onSubmit={async event => { event.preventDefault(); const fileParts = files && files.length > 0 ? await convertFilesToDataURLs(files) : []; sendMessage({ role: 'user', parts: [{ type: 'text', text: input }, ...fileParts], }); setInput(''); setFiles(undefined); if (fileInputRef.current) { fileInputRef.current.value = ''; } }} > <input type="file" accept="image/*,application/pdf" className="" onChange={event => { if (event.target.files) { setFiles(event.target.files); } }} multiple ref={fileInputRef} /> <input className="w-full p-2" value={input} placeholder="Say something..." onChange={e => setInput(e.target.value)} /> </form> </div> ); } ``` In this code, you: 1. Add a helper function `convertFilesToDataURLs` to convert file uploads to data URLs. 1. Create state to hold the input text, files, and a ref to the file input field. 1. Configure `useChat` with `DefaultChatTransport` to specify the API endpoint. 1. Display messages using the `parts` array structure, rendering text, images, and PDFs appropriately. 1. Update the `onSubmit` function to send messages with the `sendMessage` function, including both text and file parts. 1. Add a file input field to the form, including an `onChange` handler to handle updating the files state. ## Running Your Application With that, you have built everything you need for your multi-modal agent! To start your application, use the command: <Snippet text="pnpm run dev" /> Head to your browser and open http://localhost:3000. You should see an input field and a button to upload files. Try uploading an image or PDF and asking the model questions about it. Watch as the model's response is streamed back to you! ## Using Other Providers With the AI SDK's unified provider interface you can easily switch to other providers that support multi-modal capabilities: ```tsx filename="app/api/chat/route.ts" // Using Anthropic import { anthropic } from '@ai-sdk/anthropic'; const result = streamText({ model: anthropic('claude-sonnet-4-20250514'), messages: convertToModelMessages(messages), }); // Using Google import { google } from '@ai-sdk/google'; const result = streamText({ model: google('gemini-2.5-flash'), messages: convertToModelMessages(messages), }); ``` Install the provider package (`@ai-sdk/anthropic` or `@ai-sdk/google`) and update your API keys in `.env.local`. The rest of your code remains the same. <Note> Different providers may have varying file size limits and performance characteristics. Check the [provider documentation](/providers/ai-sdk-providers) for specific details. </Note> ## Where to Next? You've built a multi-modal AI agent using the AI SDK! Experiment and extend the functionality of this application further by exploring [tool calling](/docs/ai-sdk-core/tools-and-tool-calling). --- File: /ai/content/cookbook/00-guides/03-slackbot.mdx --- --- title: Slackbot Agent Guide description: Learn how to use the AI SDK to build an AI Agent in Slack. tags: ['agents', 'chatbot'] --- # Building an AI Agent in Slack with the AI SDK In this guide, you will learn how to build a Slackbot powered by the AI SDK. The bot will be able to respond to direct messages and mentions in channels using the full context of the thread. ## Slack App Setup Before we start building, you'll need to create and configure a Slack app: 1. Go to [api.slack.com/apps](https://api.slack.com/apps) 2. Click "Create New App" and choose "From scratch" 3. Give your app a name and select your workspace 4. Under "OAuth & Permissions", add the following bot token scopes: - `app_mentions:read` - `chat:write` - `im:history` - `im:write` - `assistant:write` 5. Install the app to your workspace (button under "OAuth Tokens" subsection) 6. Copy the Bot User OAuth Token and Signing Secret for the next step 7. Under App Home -> Show Tabs -> Chat Tab, check "Allow users to send Slash commands and messages from the chat tab" ## Project Setup This project uses the following stack: - [AI SDK by Vercel](/docs) - [Slack Web API](https://api.slack.com/web) - [Vercel](https://vercel.com) - [OpenAI](https://openai.com) ## Getting Started 1. Clone [the repository](https://github.com/vercel-labs/ai-sdk-slackbot) and check out the `starter` branch <Snippet text={[ 'git clone https://github.com/vercel-labs/ai-sdk-slackbot.git', 'cd ai-sdk-slackbot', 'git checkout starter', ]} /> 2. Install dependencies <Snippet text={['pnpm install']} /> ## Project Structure The starter repository already includes: - Slack utilities (`lib/slack-utils.ts`) including functions for validating incoming requests, converting Slack threads to AI SDK compatible message formats, and getting the Slackbot's user ID - General utility functions (`lib/utils.ts`) including initial Exa setup - Files to handle the different types of Slack events (`lib/handle-messages.ts` and `lib/handle-app-mention.ts`) - An API endpoint (`POST`) for Slack events (`api/events.ts`) ## Event Handler First, let's take a look at our API route (`api/events.ts`): ```typescript import type { SlackEvent } from '@slack/web-api'; import { assistantThreadMessage, handleNewAssistantMessage, } from '../lib/handle-messages'; import { waitUntil } from '@vercel/functions'; import { handleNewAppMention } from '../lib/handle-app-mention'; import { verifyRequest, getBotId } from '../lib/slack-utils'; export async function POST(request: Request) { const rawBody = await request.text(); const payload = JSON.parse(rawBody); const requestType = payload.type as 'url_verification' | 'event_callback'; // See https://api.slack.com/events/url_verification if (requestType === 'url_verification') { return new Response(payload.challenge, { status: 200 }); } await verifyRequest({ requestType, request, rawBody }); try { const botUserId = await getBotId(); const event = payload.event as SlackEvent; if (event.type === 'app_mention') { waitUntil(handleNewAppMention(event, botUserId)); } if (event.type === 'assistant_thread_started') { waitUntil(assistantThreadMessage(event)); } if ( event.type === 'message' && !event.subtype && event.channel_type === 'im' && !event.bot_id && !event.bot_profile && event.bot_id !== botUserId ) { waitUntil(handleNewAssistantMessage(event, botUserId)); } return new Response('Success!', { status: 200 }); } catch (error) { console.error('Error generating response', error); return new Response('Error generating response', { status: 500 }); } } ``` This file defines a `POST` function that handles incoming requests from Slack. First, you check the request type to see if it's a URL verification request. If it is, you respond with the challenge string provided by Slack. If it's an event callback, you verify the request and then have access to the event data. This is where you can implement your event handling logic. You then handle three types of events: `app_mention`, `assistant_thread_started`, and `message`: - For `app_mention`, you call `handleNewAppMention` with the event and the bot user ID. - For `assistant_thread_started`, you call `assistantThreadMessage` with the event. - For `message`, you call `handleNewAssistantMessage` with the event and the bot user ID. Finally, you respond with a success message to Slack. Note, each handler function is wrapped in a `waitUntil` function. Let's take a look at what this means and why it's important. ### The waitUntil Function Slack expects a response within 3 seconds to confirm the request is being handled. However, generating AI responses can take longer. If you don't respond to the Slack request within 3 seconds, Slack will send another request, leading to another invocation of your API route, another call to the LLM, and ultimately another response to the user. To solve this, you can use the `waitUntil` function, which allows you to run your AI logic after the response is sent, without blocking the response itself. This means, your API endpoint will: 1. Immediately respond to Slack (within 3 seconds) 2. Continue processing the message asynchronously 3. Send the AI response when it's ready ## Event Handlers Let's look at how each event type is currently handled. ### App Mentions When a user mentions your bot in a channel, the `app_mention` event is triggered. The `handleNewAppMention` function in `handle-app-mention.ts` processes these mentions: 1. Checks if the message is from a bot to avoid infinite response loops 2. Creates a status updater to show the bot is "thinking" 3. If the mention is in a thread, it retrieves the thread history 4. Calls the LLM with the message content (using the `generateResponse` function which you will implement in the next section) 5. Updates the initial "thinking" message with the AI response Here's the code for the `handleNewAppMention` function: ```typescript filename="lib/handle-app-mention.ts" import { AppMentionEvent } from '@slack/web-api'; import { client, getThread } from './slack-utils'; import { generateResponse } from './ai'; const updateStatusUtil = async ( initialStatus: string, event: AppMentionEvent, ) => { const initialMessage = await client.chat.postMessage({ channel: event.channel, thread_ts: event.thread_ts ?? event.ts, text: initialStatus, }); if (!initialMessage || !initialMessage.ts) throw new Error('Failed to post initial message'); const updateMessage = async (status: string) => { await client.chat.update({ channel: event.channel, ts: initialMessage.ts as string, text: status, }); }; return updateMessage; }; export async function handleNewAppMention( event: AppMentionEvent, botUserId: string, ) { console.log('Handling app mention'); if (event.bot_id || event.bot_id === botUserId || event.bot_profile) { console.log('Skipping app mention'); return; } const { thread_ts, channel } = event; const updateMessage = await updateStatusUtil('is thinking...', event); if (thread_ts) { const messages = await getThread(channel, thread_ts, botUserId); const result = await generateResponse(messages, updateMessage); updateMessage(result); } else { const result = await generateResponse( [{ role: 'user', content: event.text }], updateMessage, ); updateMessage(result); } } ``` Now let's see how new assistant threads and messages are handled. ### Assistant Thread Messages When a user starts a thread with your assistant, the `assistant_thread_started` event is triggered. The `assistantThreadMessage` function in `handle-messages.ts` handles this: 1. Posts a welcome message to the thread 2. Sets up suggested prompts to help users get started Here's the code for the `assistantThreadMessage` function: ```typescript filename="lib/handle-messages.ts" import type { AssistantThreadStartedEvent } from '@slack/web-api'; import { client } from './slack-utils'; export async function assistantThreadMessage( event: AssistantThreadStartedEvent, ) { const { channel_id, thread_ts } = event.assistant_thread; console.log(`Thread started: ${channel_id} ${thread_ts}`); console.log(JSON.stringify(event)); await client.chat.postMessage({ channel: channel_id, thread_ts: thread_ts, text: "Hello, I'm an AI assistant built with the AI SDK by Vercel!", }); await client.assistant.threads.setSuggestedPrompts({ channel_id: channel_id, thread_ts: thread_ts, prompts: [ { title: 'Get the weather', message: 'What is the current weather in London?', }, { title: 'Get the news', message: 'What is the latest Premier League news from the BBC?', }, ], }); } ``` ### Direct Messages For direct messages to your bot, the `message` event is triggered and the event is handled by the `handleNewAssistantMessage` function in `handle-messages.ts`: 1. Verifies the message isn't from a bot 2. Updates the status to show the response is being generated 3. Retrieves the conversation history 4. Calls the LLM with the conversation context 5. Posts the LLM's response to the thread Here's the code for the `handleNewAssistantMessage` function: ```typescript filename="lib/handle-messages.ts" import type { GenericMessageEvent } from '@slack/web-api'; import { client, getThread } from './slack-utils'; import { generateResponse } from './ai'; export async function handleNewAssistantMessage( event: GenericMessageEvent, botUserId: string, ) { if ( event.bot_id || event.bot_id === botUserId || event.bot_profile || !event.thread_ts ) return; const { thread_ts, channel } = event; const updateStatus = updateStatusUtil(channel, thread_ts); updateStatus('is thinking...'); const messages = await getThread(channel, thread_ts, botUserId); const result = await generateResponse(messages, updateStatus); await client.chat.postMessage({ channel: channel, thread_ts: thread_ts, text: result, unfurl_links: false, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: result, }, }, ], }); updateStatus(''); } ``` With the event handlers in place, let's now implement the AI logic. ## Implementing AI Logic The core of our application is the `generateResponse` function in `lib/generate-response.ts`, which processes messages and generates responses using the AI SDK. Here's how to implement it: ```typescript filename="lib/generate-response.ts" import { openai } from '@ai-sdk/openai'; import { generateText, ModelMessage } from 'ai'; export const generateResponse = async ( messages: ModelMessage[], updateStatus?: (status: string) => void, ) => { const { text } = await generateText({ model: openai('gpt-4o-mini'), system: `You are a Slack bot assistant. Keep your responses concise and to the point. - Do not tag users. - Current date is: ${new Date().toISOString().split('T')[0]}`, messages, }); // Convert markdown to Slack mrkdwn format return text.replace(/\[(.*?)\]\((.*?)\)/g, '<$2|$1>').replace(/\*\*/g, '*'); }; ``` This basic implementation: 1. Uses the AI SDK's `generateText` function to call OpenAI's `gpt-4o` model 2. Provides a system prompt to guide the model's behavior 3. Formats the response for Slack's markdown format ## Enhancing with Tools The real power of the AI SDK comes from tools that enable your bot to perform actions. Let's add two useful tools: ```typescript filename="lib/generate-response.ts" import { openai } from '@ai-sdk/openai'; import { generateText, tool, ModelMessage, stepCountIs } from 'ai'; import { z } from 'zod'; import { exa } from './utils'; export const generateResponse = async ( messages: ModelMessage[], updateStatus?: (status: string) => void, ) => { const { text } = await generateText({ model: openai('gpt-4o'), system: `You are a Slack bot assistant. Keep your responses concise and to the point. - Do not tag users. - Current date is: ${new Date().toISOString().split('T')[0]} - Always include sources in your final response if you use web search.`, messages, stopWhen: stepCountIs(10), tools: { getWeather: tool({ description: 'Get the current weather at a location', inputSchema: z.object({ latitude: z.number(), longitude: z.number(), city: z.string(), }), execute: async ({ latitude, longitude, city }) => { updateStatus?.(`is getting weather for ${city}...`); const response = await fetch( `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,weathercode,relativehumidity_2m&timezone=auto`, ); const weatherData = await response.json(); return { temperature: weatherData.current.temperature_2m, weatherCode: weatherData.current.weathercode, humidity: weatherData.current.relativehumidity_2m, city, }; }, }), searchWeb: tool({ description: 'Use this to search the web for information', inputSchema: z.object({ query: z.string(), specificDomain: z .string() .nullable() .describe( 'a domain to search if the user specifies e.g. bbc.com. Should be only the domain name without the protocol', ), }), execute: async ({ query, specificDomain }) => { updateStatus?.(`is searching the web for ${query}...`); const { results } = await exa.searchAndContents(query, { livecrawl: 'always', numResults: 3, includeDomains: specificDomain ? [specificDomain] : undefined, }); return { results: results.map(result => ({ title: result.title, url: result.url, snippet: result.text.slice(0, 1000), })), }; }, }), }, }); // Convert markdown to Slack mrkdwn format return text.replace(/\[(.*?)\]\((.*?)\)/g, '<$2|$1>').replace(/\*\*/g, '*'); }; ``` In this updated implementation: 1. You added two tools: - `getWeather`: Fetches weather data for a specified location - `searchWeb`: Searches the web for information using the Exa API 2. You set `stopWhen: stepCountIs(10)` to enable multi-step conversations. This defines the stopping conditions of your agent, when the model generates a tool call. This will automatically send any tool results back to the LLM to trigger additional tool calls or responses as the LLM deems necessary. This turns your LLM call from a one-off operation into a multi-step agentic flow. ## How It Works When a user interacts with your bot: 1. The Slack event is received and processed by your API endpoint 2. The user's message and the thread history is passed to the `generateResponse` function 3. The AI SDK processes the message and may invoke tools as needed 4. The response is formatted for Slack and sent back to the user The tools are automatically invoked based on the user's intent. For example, if a user asks "What's the weather in London?", the AI will: 1. Recognize this as a weather query 2. Call the `getWeather` tool with London's coordinates (inferred by the LLM) 3. Process the weather data 4. Generate a final response, answering the user's question ## Deploying the App 1. Install the Vercel CLI <Snippet text={['pnpm install -g vercel']} /> 2. Deploy the app <Snippet text={['vercel deploy']} /> 3. Copy the deployment URL and update the Slack app's Event Subscriptions to point to your Vercel URL 4. Go to your project's deployment settings (Your project -> Settings -> Environment Variables) and add your environment variables ```bash SLACK_BOT_TOKEN=your_slack_bot_token SLACK_SIGNING_SECRET=your_slack_signing_secret OPENAI_API_KEY=your_openai_api_key EXA_API_KEY=your_exa_api_key ``` <Note> Make sure to redeploy your app after updating environment variables. </Note> 5. Head back to the [https://api.slack.com/](https://api.slack.com/) and navigate to the "Event Subscriptions" page. Enable events and add your deployment URL. ```bash https://your-vercel-url.vercel.app/api/events ``` 6. On the Events Subscription page, subscribe to the following events. - `app_mention` - `assistant_thread_started` - `message:im` Finally, head to Slack and test the app by sending a message to the bot. ## Next Steps You've built a Slack chatbot powered by the AI SDK! Here are some ways you could extend it: 1. Add memory for specific users to give the LLM context of previous interactions 2. Implement more tools like database queries or knowledge base searches 3. Add support for rich message formatting with blocks 4. Add analytics to track usage patterns <Note> In a production environment, it is recommended to implement a robust queueing system to ensure messages are properly handled. </Note> --- File: /ai/content/cookbook/00-guides/04-natural-language-postgres.mdx --- --- title: Natural Language Postgres description: Learn how to build a Next.js app that lets you talk to a PostgreSQL database in natural language. tags: ['agents', 'next', 'tools'] --- # Natural Language Postgres Guide In this guide, you will learn how to build an app that uses AI to interact with a PostgreSQL database using natural language. The application will: - Generate SQL queries from a natural language input - Explain query components in plain English - Create a chart to visualise query results You can find a completed version of this project at [natural-language-postgres.vercel.app](https://natural-language-postgres.vercel.app). ## Project setup This project uses the following stack: - [Next.js](https://nextjs.org) (App Router) - [AI SDK](/docs) - [OpenAI](https://openai.com) - [Zod](https://zod.dev) - [Postgres](https://www.postgresql.org/) with [ Vercel Postgres ](https://vercel.com/postgres) - [shadcn-ui](https://ui.shadcn.com) and [TailwindCSS](https://tailwindcss.com) for styling - [Recharts](https://recharts.org) for data visualization ### Clone repo To focus on the AI-powered functionality rather than project setup and configuration we've prepared a starter repository which includes a database schema and a few components. Clone the starter repository and check out the `starter` branch: <Snippet text={[ 'git clone https://github.com/vercel-labs/natural-language-postgres', 'cd natural-language-postgres', 'git checkout starter', ]} /> ### Project setup and data Let's set up the project and seed the database with the dataset: 1. Install dependencies: <Snippet text={['pnpm install']} /> 2. Copy the example environment variables file: <Snippet text={['cp .env.example .env']} /> 3. Add your environment variables to `.env`: ```bash filename=".env" OPENAI_API_KEY="your_api_key_here" POSTGRES_URL="..." POSTGRES_PRISMA_URL="..." POSTGRES_URL_NO_SSL="..." POSTGRES_URL_NON_POOLING="..." POSTGRES_USER="..." POSTGRES_HOST="..." POSTGRES_PASSWORD="..." POSTGRES_DATABASE="..." ``` 4. This project uses CB Insights' Unicorn Companies dataset. You can download the dataset by following these instructions: - Navigate to [CB Insights Unicorn Companies](https://www.cbinsights.com/research-unicorn-companies) - Enter in your email. You will receive a link to download the dataset. - Save it as `unicorns.csv` in your project root <Note> You will need a Postgres database to complete this tutorial. If you don't have Postgres setup on your local machine you can: - Create a free Postgres database with Vercel (recommended - see instructions below); or - Follow [this guide](https://www.prisma.io/dataguide/postgresql/setting-up-a-local-postgresql-database) to set it up locally </Note> #### Setting up Postgres with Vercel To set up a Postgres instance on your Vercel account: 1. Go to [Vercel.com](https://vercel.com) and make sure you're logged in 1. Navigate to your team homepage 1. Click on the **Integrations** tab 1. Click **Browse Marketplace** 1. Look for the **Storage** option in the sidebar 1. Select the **Neon** option (recommended, but any other PostgreSQL database provider should work) 1. Click **Install**, then click **Install** again in the top right corner 1. On the "Get Started with Neon" page, click **Create Database** on the right 1. Select your region (e.g., Washington, D.C., U.S. East) 1. Turn off **Auth** 1. Click **Continue** 1. Name your database (you can use the default name or rename it to something like "NaturalLanguagePostgres") 1. Click **Create** in the bottom right corner 1. After seeing "Database created successfully", click **Done** 1. You'll be redirected to your database instance 1. In the Quick Start section, click **Show secrets** 1. Copy the full `DATABASE_URL` environment variable and use it to populate the Postgres environment variables in your `.env` file ### About the dataset The Unicorn List dataset contains the following information about unicorn startups (companies with a valuation above $1bn): - Company name - Valuation - Date joined (unicorn status) - Country - City - Industry - Select investors This dataset contains over 1000 rows of data over 7 columns, giving us plenty of structured data to analyze. This makes it perfect for exploring various SQL queries that can reveal interesting insights about the unicorn startup ecosystem. 5. Now that you have the dataset downloaded and added to your project, you can initialize the database with the following command: <Snippet text={['pnpm run seed']} /> Note: this step can take a little while. You should see a message indicating the Unicorns table has been created and then that the database has been seeded successfully. <Note> Remember, the dataset should be named `unicorns.csv` and located in root of your project. </Note> 6. Start the development server: <Snippet text={['pnpm run dev']} /> Your application should now be running at [http://localhost:3000](http://localhost:3000). ## Project structure The starter repository already includes everything that you will need, including: - Database seed script (`lib/seed.ts`) - Basic components built with shadcn/ui (`components/`) - Function to run SQL queries (`app/actions.ts`) - Type definitions for the database schema (`lib/types.ts`) ### Existing components The application contains a single page in `app/page.tsx` that serves as the main interface. At the top, you'll find a header (`header.tsx`) displaying the application title and description. Below that is an input field and search button (`search.tsx`) where you can enter natural language queries. Initially, the page shows a collection of suggested example queries (`suggested-queries.tsx`) that you can click to quickly try out the functionality. When you submit a query: - The suggested queries section disappears and a loading state appears - Once complete, a card appears with "TODO - IMPLEMENT ABOVE" (`query-viewer.tsx`) which will eventually show your generated SQL - Below that is an empty results area with "No results found" (`results.tsx`) After you implement the core functionality: - The results section will display data in a table format - A toggle button will allow switching between table and chart views - The chart view will visualize your query results Let's implement the AI-powered functionality to bring it all together. ## Building the application As a reminder, this application will have three main features: 1. Generate SQL queries from natural language 2. Create a chart from the query results 3. Explain SQL queries in plain English For each of these features, you'll use the AI SDK via [ Server Actions ](https://react.dev/reference/rsc/server-actions) to interact with OpenAI's GPT-4o and GPT-4o-mini models. Server Actions are a powerful React Server Component feature that allows you to call server-side functions directly from your frontend code. Let's start with generating a SQL query from natural language. ## Generate SQL queries ### Providing context For the model to generate accurate SQL queries, it needs context about your database schema, tables, and relationships. You will communicate this information through a prompt that should include: 1. Schema information 2. Example data formats 3. Available SQL operations 4. Best practices for query structure 5. Nuanced advice for specific fields Let's write a prompt that includes all of this information: ```txt You are a SQL (postgres) and data visualization expert. Your job is to help the user write a SQL query to retrieve the data they need. The table schema is as follows: unicorns ( id SERIAL PRIMARY KEY, company VARCHAR(255) NOT NULL UNIQUE, valuation DECIMAL(10, 2) NOT NULL, date_joined DATE, country VARCHAR(255) NOT NULL, city VARCHAR(255) NOT NULL, industry VARCHAR(255) NOT NULL, select_investors TEXT NOT NULL ); Only retrieval queries are allowed. For things like industry, company names and other string fields, use the ILIKE operator and convert both the search term and the field to lowercase using LOWER() function. For example: LOWER(industry) ILIKE LOWER('%search_term%'). Note: select_investors is a comma-separated list of investors. Trim whitespace to ensure you're grouping properly. Note, some fields may be null or have only one value. When answering questions about a specific field, ensure you are selecting the identifying column (ie. what is Vercel's valuation would select company and valuation'). The industries available are: - healthcare & life sciences - consumer & retail - financial services - enterprise tech - insurance - media & entertainment - industrials - health If the user asks for a category that is not in the list, infer based on the list above. Note: valuation is in billions of dollars so 10b would be 10.0. Note: if the user asks for a rate, return it as a decimal. For example, 0.1 would be 10%. If the user asks for 'over time' data, return by year. When searching for UK or USA, write out United Kingdom or United States respectively. EVERY QUERY SHOULD RETURN QUANTITATIVE DATA THAT CAN BE PLOTTED ON A CHART! There should always be at least two columns. If the user asks for a single column, return the column and the count of the column. If the user asks for a rate, return the rate as a decimal. For example, 0.1 would be 10%. ``` There are several important elements of this prompt: - Schema description helps the model understand exactly what data fields to work with - Includes rules for handling queries based on common SQL patterns - for example, always using ILIKE for case-insensitive string matching - Explains how to handle edge cases in the dataset, like dealing with the comma-separated investors field and ensuring whitespace is properly handled - Instead of having the model guess at industry categories, it provides the exact list that exists in the data, helping avoid mismatches - The prompt helps standardize data transformations - like knowing to interpret "10b" as "10.0" billion dollars, or that rates should be decimal values - Clear rules ensure the query output will be chart-friendly by always including at least two columns of data that can be plotted This prompt structure provides a strong foundation for query generation, but you should experiment and iterate based on your specific needs and the model you're using. ### Create a Server Action With the prompt done, let's create a Server Action. Open `app/actions.ts`. You should see one action already defined (`runGeneratedSQLQuery`). Add a new action. This action should be asynchronous and take in one parameter - the natural language query. ```ts filename="app/actions.ts" /* ...rest of the file... */ export const generateQuery = async (input: string) => {}; ``` In this action, you'll use the `generateObject` function from the AI SDK which allows you to constrain the model's output to a pre-defined schema. This process, sometimes called structured output, ensures the model returns only the SQL query without any additional prefixes, explanations, or formatting that would require manual parsing. ```ts filename="app/actions.ts" /* ...other imports... */ import { generateObject } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; /* ...rest of the file... */ export const generateQuery = async (input: string) => { 'use server'; try { const result = await generateObject({ model: openai('gpt-4o'), system: `You are a SQL (postgres) ...`, // SYSTEM PROMPT AS ABOVE - OMITTED FOR BREVITY prompt: `Generate the query necessary to retrieve the data the user wants: ${input}`, schema: z.object({ query: z.string(), }), }); return result.object.query; } catch (e) { console.error(e); throw new Error('Failed to generate query'); } }; ``` Note, you are constraining the output to a single string field called `query` using `zod`, a TypeScript schema validation library. This will ensure the model only returns the SQL query itself. The resulting generated query will then be returned. ### Update the frontend With the Server Action in place, you can now update the frontend to call this action when the user submits a natural language query. In the root page (`app/page.tsx`), you should see a `handleSubmit` function that is called when the user submits a query. Import the `generateQuery` function and call it with the user's input. ```typescript filename="app/page.tsx" highlight="21" /* ...other imports... */ import { runGeneratedSQLQuery, generateQuery } from './actions'; /* ...rest of the file... */ const handleSubmit = async (suggestion?: string) => { clearExistingData(); const question = suggestion ?? inputValue; if (inputValue.length === 0 && !suggestion) return; if (question.trim()) { setSubmitted(true); } setLoading(true); setLoadingStep(1); setActiveQuery(''); try { const query = await generateQuery(question); if (query === undefined) { toast.error('An error occurred. Please try again.'); setLoading(false); return; } setActiveQuery(query); setLoadingStep(2); const companies = await runGeneratedSQLQuery(query); const columns = companies.length > 0 ? Object.keys(companies[0]) : []; setResults(companies); setColumns(columns); setLoading(false); } catch (e) { toast.error('An error occurred. Please try again.'); setLoading(false); } }; /* ...rest of the file... */ ``` Now, when the user submits a natural language query (ie. "how many unicorns are from San Francisco?"), that question will be sent to your newly created Server Action. The Server Action will call the model, passing in your system prompt and the users query, and return the generated SQL query in a structured format. This query is then passed to the `runGeneratedSQLQuery` action to run the query against your database. The results are then saved in local state and displayed to the user. Save the file, make sure the dev server is running, and then head to `localhost:3000` in your browser. Try submitting a natural language query and see the generated SQL query and results. You should see a SQL query generated and displayed under the input field. You should also see the results of the query displayed in a table below the input field. Try clicking the SQL query to see the full query if it's too long to display in the input field. You should see a button on the right side of the input field with a question mark icon. Clicking this button currently does nothing, but you'll add the "explain query" functionality to it in the next step. ## Explain SQL Queries Next, let's add the ability to explain SQL queries in plain English. This feature helps users understand how the generated SQL query works by breaking it down into logical sections. As with the SQL query generation, you'll need a prompt to guide the model when explaining queries. Let's craft a prompt for the explain query functionality: ```txt You are a SQL (postgres) expert. Your job is to explain to the user write a SQL query you wrote to retrieve the data they asked for. The table schema is as follows: unicorns ( id SERIAL PRIMARY KEY, company VARCHAR(255) NOT NULL UNIQUE, valuation DECIMAL(10, 2) NOT NULL, date_joined DATE, country VARCHAR(255) NOT NULL, city VARCHAR(255) NOT NULL, industry VARCHAR(255) NOT NULL, select_investors TEXT NOT NULL ); When you explain you must take a section of the query, and then explain it. Each "section" should be unique. So in a query like: "SELECT * FROM unicorns limit 20", the sections could be "SELECT *", "FROM UNICORNS", "LIMIT 20". If a section doesn't have any explanation, include it, but leave the explanation empty. ``` Like the prompt for generating SQL queries, you provide the model with the schema of the database. Additionally, you provide an example of what each section of the query might look like. This helps the model understand the structure of the query and how to break it down into logical sections. ### Create a Server Action Add a new Server Action to generate explanations for SQL queries. This action takes two parameters - the original natural language input and the generated SQL query. ```ts filename="app/actions.ts" /* ...rest of the file... */ export const explainQuery = async (input: string, sqlQuery: string) => { 'use server'; try { const result = await generateObject({ model: openai('gpt-4o'), system: `You are a SQL (postgres) expert. ...`, // SYSTEM PROMPT AS ABOVE - OMITTED FOR BREVITY prompt: `Explain the SQL query you generated to retrieve the data the user wanted. Assume the user is not an expert in SQL. Break down the query into steps. Be concise. User Query: ${input} Generated SQL Query: ${sqlQuery}`, }); return result.object; } catch (e) { console.error(e); throw new Error('Failed to generate query'); } }; ``` This action uses the `generateObject` function again. However, you haven't defined the schema yet. Let's define it in another file so it can also be used as a type in your components. Update your `lib/types.ts` file to include the schema for the explanations: ```ts filename="lib/types.ts" import { z } from 'zod'; /* ...rest of the file... */ export const explanationSchema = z.object({ section: z.string(), explanation: z.string(), }); export type QueryExplanation = z.infer<typeof explanationSchema>; ``` This schema defines the structure of the explanation that the model will generate. Each explanation will have a `section` and an `explanation`. The `section` is the part of the query being explained, and the `explanation` is the plain English explanation of that section. Go back to your `actions.ts` file and import and use the `explanationSchema`: ```ts filename="app/actions.ts" highlight="2,19,20" // other imports import { explanationSchema } from '@/lib/types'; /* ...rest of the file... */ export const explainQuery = async (input: string, sqlQuery: string) => { 'use server'; try { const result = await generateObject({ model: openai('gpt-4o'), system: `You are a SQL (postgres) expert. ...`, // SYSTEM PROMPT AS ABOVE - OMITTED FOR BREVITY prompt: `Explain the SQL query you generated to retrieve the data the user wanted. Assume the user is not an expert in SQL. Break down the query into steps. Be concise. User Query: ${input} Generated SQL Query: ${sqlQuery}`, schema: explanationSchema, output: 'array', }); return result.object; } catch (e) { console.error(e); throw new Error('Failed to generate query'); } }; ``` <Note> You can use `output: "array"` to indicate to the model that you expect an array of objects matching the schema to be returned. </Note> ### Update query viewer Next, update the `query-viewer.tsx` component to display these explanations. The `handleExplainQuery` function is called every time the user clicks the question icon button on the right side of the query. Let's update this function to use the new `explainQuery` action: ```ts filename="components/query-viewer.tsx" highlight="2,10,11" /* ...other imports... */ import { explainQuery } from '@/app/actions'; /* ...rest of the component... */ const handleExplainQuery = async () => { setQueryExpanded(true); setLoadingExplanation(true); const explanations = await explainQuery(inputValue, activeQuery); setQueryExplanations(explanations); setLoadingExplanation(false); }; /* ...rest of the component... */ ``` Now when users click the explanation button (the question mark icon), the component will: 1. Show a loading state 2. Send the active SQL query and the users natural language query to your Server Action 3. The model will generate an array of explanations 4. The explanations will be set in the component state and rendered in the UI Submit a new query and then click the explanation button. Hover over different elements of the query. You should see the explanations for each section! ## Visualizing query results Finally, let's render the query results visually in a chart. There are two approaches you could take: 1. Send both the query and data to the model and ask it to return the data in a visualization-ready format. While this provides complete control over the visualization, it requires the model to send back all of the data, which significantly increases latency and costs. 2. Send the query and data to the model and ask it to generate a chart configuration (fixed-size and not many tokens) that maps your data appropriately. This configuration specifies how to visualize the information while delivering the insights from your natural language query. Importantly, this is done without requiring the model return the full dataset. Since you don't know the SQL query or data shape beforehand, let's use the second approach to dynamically generate chart configurations based on the query results and user intent. ### Generate the chart configuration For this feature, you'll create a Server Action that takes the query results and the user's original natural language query to determine the best visualization approach. Your application is already set up to use `shadcn` charts (which uses [`Recharts`](https://recharts.org/en-US/) under the hood) so the model will need to generate: - Chart type (bar, line, area, or pie) - Axis mappings - Visual styling Let's start by defining the schema for the chart configuration in `lib/types.ts`: ```ts filename="lib/types.ts" /* ...rest of the file... */ export const configSchema = z .object({ description: z .string() .describe( 'Describe the chart. What is it showing? What is interesting about the way the data is displayed?', ), takeaway: z.string().describe('What is the main takeaway from the chart?'), type: z.enum(['bar', 'line', 'area', 'pie']).describe('Type of chart'), title: z.string(), xKey: z.string().describe('Key for x-axis or category'), yKeys: z .array(z.string()) .describe( 'Key(s) for y-axis values this is typically the quantitative column', ), multipleLines: z .boolean() .describe( 'For line charts only: whether the chart is comparing groups of data.', ) .optional(), measurementColumn: z .string() .describe( 'For line charts only: key for quantitative y-axis column to measure against (eg. values, counts etc.)', ) .optional(), lineCategories: z .array(z.string()) .describe( 'For line charts only: Categories used to compare different lines or data series. Each category represents a distinct line in the chart.', ) .optional(), colors: z .record( z.string().describe('Any of the yKeys'), z.string().describe('Color value in CSS format (e.g., hex, rgb, hsl)'), ) .describe('Mapping of data keys to color values for chart elements') .optional(), legend: z.boolean().describe('Whether to show legend'), }) .describe('Chart configuration object'); export type Config = z.infer<typeof configSchema>; ``` <Note> Replace the existing `export type Config = any;` type with the new one. </Note> This schema makes extensive use of Zod's `.describe()` function to give the model extra context about each of the key's you are expecting in the chart configuration. This will help the model understand the purpose of each key and generate more accurate results. Another important technique to note here is that you are defining `description` and `takeaway` fields. Not only are these useful for the user to quickly understand what the chart means and what they should take away from it, but they also force the model to generate a description of the data first, before it attempts to generate configuration attributes like axis and columns. This will help the model generate more accurate and relevant chart configurations. ### Create the Server Action Create a new action in `app/actions.ts`: ```ts /* ...other imports... */ import { Config, configSchema, explanationsSchema, Result } from '@/lib/types'; /* ...rest of the file... */ export const generateChartConfig = async ( results: Result[], userQuery: string, ) => { 'use server'; try { const { object: config } = await generateObject({ model: openai('gpt-4o'), system: 'You are a data visualization expert.', prompt: `Given the following data from a SQL query result, generate the chart config that best visualises the data and answers the users query. For multiple groups use multi-lines. Here is an example complete config: export const chartConfig = { type: "pie", xKey: "month", yKeys: ["sales", "profit", "expenses"], colors: { sales: "#4CAF50", // Green for sales profit: "#2196F3", // Blue for profit expenses: "#F44336" // Red for expenses }, legend: true } User Query: ${userQuery} Data: ${JSON.stringify(results, null, 2)}`, schema: configSchema, }); // Override with shadcn theme colors const colors: Record<string, string> = {}; config.yKeys.forEach((key, index) => { colors[key] = `hsl(var(--chart-${index + 1}))`; }); const updatedConfig = { ...config, colors }; return { config: updatedConfig }; } catch (e) { console.error(e); throw new Error('Failed to generate chart suggestion'); } }; ``` ### Update the chart component With the action in place, you'll want to trigger it automatically after receiving query results. This ensures the visualization appears almost immediately after data loads. Update the `handleSubmit` function in your root page (`app/page.tsx`) to generate and set the chart configuration after running the query: ```typescript filename="app/page.tsx" highlight="38,39" /* ...other imports... */ import { getCompanies, generateQuery, generateChartConfig } from './actions'; /* ...rest of the file... */ const handleSubmit = async (suggestion?: string) => { clearExistingData(); const question = suggestion ?? inputValue; if (inputValue.length === 0 && !suggestion) return; if (question.trim()) { setSubmitted(true); } setLoading(true); setLoadingStep(1); setActiveQuery(''); try { const query = await generateQuery(question); if (query === undefined) { toast.error('An error occurred. Please try again.'); setLoading(false); return; } setActiveQuery(query); setLoadingStep(2); const companies = await runGeneratedSQLQuery(query); const columns = companies.length > 0 ? Object.keys(companies[0]) : []; setResults(companies); setColumns(columns); setLoading(false); const { config } = await generateChartConfig(companies, question); setChartConfig(config); } catch (e) { toast.error('An error occurred. Please try again.'); setLoading(false); } }; /* ...rest of the file... */ ``` Now when users submit queries, the application will: 1. Generate and run the SQL query 2. Display the table results 3. Generate a chart configuration for the results 4. Allow toggling between table and chart views Head back to the browser and test the application with a few queries. You should see the chart visualization appear after the table results. ## Next steps You've built an AI-powered SQL analysis tool that can convert natural language to SQL queries, visualize query results, and explain SQL queries in plain English. You could, for example, extend the application to use your own data sources or add more advanced features like customizing the chart configuration schema to support more chart types and options. You could also add more complex SQL query generation capabilities. --- File: /ai/content/cookbook/00-guides/05-computer-use.mdx --- --- title: Get started with Computer Use description: Get started with Claude's Computer Use capabilities with the AI SDK tags: ['computer-use', 'tools'] --- # Get started with Computer Use With the [release of Computer Use in Claude 3.5 Sonnet](https://www.anthropic.com/news/3-5-models-and-computer-use), you can now direct AI models to interact with computers like humans do - moving cursors, clicking buttons, and typing text. This capability enables automation of complex tasks while leveraging Claude's advanced reasoning abilities. The AI SDK is a powerful TypeScript toolkit for building AI applications with large language models (LLMs) like Anthropic's Claude alongside popular frameworks like React, Next.js, Vue, Svelte, Node.js, and more. In this guide, you will learn how to integrate Computer Use into your AI SDK applications. <Note> Computer Use is currently in beta with some [ limitations ](https://docs.anthropic.com/en/docs/build-with-claude/computer-use#understand-computer-use-limitations). The feature may be error-prone at times. Anthropic recommends starting with low-risk tasks and implementing appropriate safety measures. </Note> ## Computer Use Anthropic recently released a new version of the Claude 3.5 Sonnet model which is capable of 'Computer Use'. This allows the model to interact with computer interfaces through basic actions like: - Moving the cursor - Clicking buttons - Typing text - Taking screenshots - Reading screen content ## How It Works Computer Use enables the model to read and interact with on-screen content through a series of coordinated steps. Here's how the process works: 1. **Start with a prompt and tools** Add Anthropic-defined Computer Use tools to your request and provide a task (prompt) for the model. For example: "save an image to your downloads folder." 2. **Select the right tool** The model evaluates which computer tools can help accomplish the task. It then sends a formatted `tool_call` to use the appropriate tool. 3. **Execute the action and return results** The AI SDK processes Claude's request by running the selected tool. The results can then be sent back to Claude through a `tool_result` message. 4. **Complete the task through iterations** Claude analyzes each result to determine if more actions are needed. It continues requesting tool use and processing results until it completes your task or requires additional input. ### Available Tools There are three main tools available in the Computer Use API: 1. **Computer Tool**: Enables basic computer control like mouse movement, clicking, and keyboard input 2. **Text Editor Tool**: Provides functionality for viewing and editing text files 3. **Bash Tool**: Allows execution of bash commands ### Implementation Considerations Computer Use tools in the AI SDK are predefined interfaces that require your own implementation of the execution layer. While the SDK provides the type definitions and structure for these tools, you need to: 1. Set up a controlled environment for Computer Use execution 2. Implement core functionality like mouse control and keyboard input 3. Handle screenshot capture and processing 4. Set up rules and limits for how Claude can interact with your system The recommended approach is to start with [ Anthropic's reference implementation ](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo), which provides: - A containerized environment configured for safe Computer Use - Ready-to-use (Python) implementations of Computer Use tools - An agent loop for API interaction and tool execution - A web interface for monitoring and control This reference implementation serves as a foundation to understand the requirements before building your own custom solution. ## Getting Started with the AI SDK <Note> If you have never used the AI SDK before, start by following the [Getting Started guide](/docs/getting-started). </Note> <Note> For a working example of Computer Use implementation with Next.js and the AI SDK, check out our [AI SDK Computer Use Template](https://github.com/vercel-labs/ai-sdk-computer-use). </Note> First, ensure you have the AI SDK and [Anthropic AI SDK provider](/providers/ai-sdk-providers/anthropic) installed: <Snippet text="pnpm add ai @ai-sdk/anthropic" /> You can add Computer Use to your AI SDK applications using provider-defined-client tools. These tools accept various input parameters (like display height and width in the case of the computer tool) and then require that you define an execute function. Here's how you could set up the Computer Tool with the AI SDK: ```ts import { anthropic } from '@ai-sdk/anthropic'; import { getScreenshot, executeComputerAction } from '@/utils/computer-use'; const computerTool = anthropic.tools.computer_20250124({ displayWidthPx: 1920, displayHeightPx: 1080, execute: async ({ action, coordinate, text }) => { switch (action) { case 'screenshot': { return { type: 'image', data: getScreenshot(), }; } default: { return executeComputerAction(action, coordinate, text); } } }, toModelOutput(result) { return typeof result === 'string' ? [{ type: 'text', text: result }] : [{ type: 'image', data: result.data, mediaType: 'image/png' }]; }, }); ``` The `computerTool` handles two main actions: taking screenshots via `getScreenshot()` and executing computer actions like mouse movements and clicks through `executeComputerAction()`. Remember, you have to implement this execution logic (eg. the `getScreenshot` and `executeComputerAction` functions) to handle the actual computer interactions. The `execute` function should handle all low-level interactions with the operating system. Finally, to send tool results back to the model, use the [`toModelOutput()`](/docs/foundations/prompts#multi-modal-tool-results) function to convert text and image responses into a format the model can process. The AI SDK includes experimental support for these multi-modal tool results when using Anthropic's models. <Note> Computer Use requires appropriate safety measures like using virtual machines, limiting access to sensitive data, and implementing human oversight for critical actions. </Note> ### Using Computer Tools with Text Generation Once your tool is defined, you can use it with both the [`generateText`](/docs/reference/ai-sdk-core/generate-text) and [`streamText`](/docs/reference/ai-sdk-core/stream-text) functions. For one-shot text generation, use `generateText`: ```ts const result = await generateText({ model: anthropic('claude-sonnet-4-20250514'), prompt: 'Move the cursor to the center of the screen and take a screenshot', tools: { computer: computerTool }, }); console.log(result.text); ``` For streaming responses, use `streamText` to receive updates in real-time: ```ts const result = streamText({ model: anthropic('claude-sonnet-4-20250514'), prompt: 'Open the browser and navigate to vercel.com', tools: { computer: computerTool }, }); for await (const chunk of result.textStream) { console.log(chunk); } ``` ### Configure Multi-Step (Agentic) Generations To allow the model to perform multiple steps without user intervention, use the `stopWhen` parameter. This will automatically send any tool results back to the model to trigger a subsequent generation: ```ts highlight="1,7" import { stepCountIs } from 'ai'; const stream = streamText({ model: anthropic('claude-sonnet-4-20250514'), prompt: 'Open the browser and navigate to vercel.com', tools: { computer: computerTool }, stopWhen: stepCountIs(10), // experiment with this value based on your use case }); ``` ### Combine Multiple Tools You can combine multiple tools in a single request to enable more complex workflows. The AI SDK supports all three of Claude's Computer Use tools: ```ts const computerTool = anthropic.tools.computer_20250124({ ... }); const bashTool = anthropic.tools.bash_20250124({ execute: async ({ command, restart }) => execSync(command).toString() }); const textEditorTool = anthropic.tools.textEditor_20250124({ execute: async ({ command, path, file_text, insert_line, new_str, old_str, view_range }) => { // Handle file operations based on command switch(command) { return executeTextEditorFunction({ command, path, fileText: file_text, insertLine: insert_line, newStr: new_str, oldStr: old_str, viewRange: view_range }); } } }); const response = await generateText({ model: anthropic("claude-sonnet-4-20250514"), prompt: "Create a new file called example.txt, write 'Hello World' to it, and run 'cat example.txt' in the terminal", tools: { computer: computerTool, bash: bashTool, str_replace_editor: textEditorTool, }, }); ``` <Note> Always implement appropriate [security measures](#security-measures) and obtain user consent before enabling Computer Use in production applications. </Note> ### Best Practices for Computer Use To get the best results when using Computer Use: 1. Specify simple, well-defined tasks with explicit instructions for each step 2. Prompt Claude to verify outcomes through screenshots 3. Use keyboard shortcuts when UI elements are difficult to manipulate 4. Include example screenshots for repeatable tasks 5. Provide explicit tips in system prompts for known tasks ## Security Measures Remember, Computer Use is a beta feature. Please be aware that it poses unique risks that are distinct from standard API features or chat interfaces. These risks are heightened when using Computer Use to interact with the internet. To minimize risks, consider taking precautions such as: 1. Use a dedicated virtual machine or container with minimal privileges to prevent direct system attacks or accidents. 2. Avoid giving the model access to sensitive data, such as account login information, to prevent information theft. 3. Limit internet access to an allowlist of domains to reduce exposure to malicious content. 4. Ask a human to confirm decisions that may result in meaningful real-world consequences as well as any tasks requiring affirmative consent, such as accepting cookies, executing financial transactions, or agreeing to terms of service. --- File: /ai/content/cookbook/00-guides/17-gemini-2-5.mdx --- --- title: Get started with Gemini 2.5 description: Get started with Gemini 2.5 using the AI SDK. tags: ['getting-started'] --- # Get started with Gemini 2.5 With the release of [Gemini 2.5](https://developers.googleblog.com/gemini-2-5-thinking-model-updates/), there has never been a better time to start building AI applications, particularly those that require complex reasoning capabilities and advanced intelligence. The [AI SDK](/) is a powerful TypeScript toolkit for building AI applications with large language models (LLMs) like Gemini 2.5 alongside popular frameworks like React, Next.js, Vue, Svelte, Node.js, and more. ## Gemini 2.5 Gemini 2.5 is Google's most advanced model family to date, offering exceptional capabilities across reasoning, instruction following, coding, and knowledge tasks. The Gemini 2.5 model family consists of: - [Gemini 2.5 Pro](https://ai.google.dev/gemini-api/docs/models#gemini-2.5-pro): Best for coding and highly complex tasks - [Gemini 2.5 Flash](https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash): Fast performance on everyday tasks - [Gemini 2.5 Flash-Lite](https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-lite): Best for high volume cost-efficient tasks ## Getting Started with the AI SDK The AI SDK is the TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js, and more. Integrating LLMs into applications is complicated and heavily dependent on the specific model provider you use. The AI SDK abstracts away the differences between model providers, eliminates boilerplate code for building chatbots, and allows you to go beyond text output to generate rich, interactive components. At the center of the AI SDK is [AI SDK Core](/docs/ai-sdk-core/overview), which provides a unified API to call any LLM. The code snippet below is all you need to call Gemini 2.5 with the AI SDK: ```ts import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; const { text } = await generateText({ model: google('gemini-2.5-flash'), prompt: 'Explain the concept of the Hilbert space.', }); console.log(text); ``` ### Thinking Capability The Gemini 2.5 series models use an internal "thinking process" that significantly improves their reasoning and multi-step planning abilities, making them highly effective for complex tasks such as coding, advanced mathematics, and data analysis. You can control the amount of thinking using the `thinkingConfig` provider option and specifying a thinking budget in tokens. Additionally, you can request thinking summaries by setting `includeThoughts` to `true`. ```ts import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; const { text, reasoning } = await generateText({ model: google('gemini-2.5-flash'), prompt: 'What is the sum of the first 10 prime numbers?', providerOptions: { google: { thinkingConfig: { thinkingBudget: 8192, includeThoughts: true, }, }, }, }); console.log(text); // text response console.log(reasoning); // reasoning summary ``` ### Using Tools with the AI SDK Gemini 2.5 supports tool calling, allowing it to interact with external systems and perform discrete tasks. Here's an example of using tool calling with the AI SDK: ```ts import { z } from 'zod'; import { generateText, tool, stepCountIs } from 'ai'; import { google } from '@ai-sdk/google'; const result = await generateText({ model: google('gemini-2.5-flash'), prompt: 'What is the weather in San Francisco?', tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), // Optional, enables multi step calling }); console.log(result.text); console.log(result.steps); ``` ### Using Google Search with Gemini With [search grounding](https://ai.google.dev/gemini-api/docs/google-search), Gemini can access to the latest information using Google search. Here's an example of using Google Search with the AI SDK: ```ts import { google } from '@ai-sdk/google'; import { GoogleGenerativeAIProviderMetadata } from '@ai-sdk/google'; import { generateText } from 'ai'; const { text, sources, providerMetadata } = await generateText({ model: google('gemini-2.5-flash'), tools: { google_search: google.tools.googleSearch({}), }, prompt: 'List the top 5 San Francisco news from the past week.' + 'You must include the date of each article.', }); // access the grounding metadata. Casting to the provider metadata type // is optional but provides autocomplete and type safety. const metadata = providerMetadata?.google as | GoogleGenerativeAIProviderMetadata | undefined; const groundingMetadata = metadata?.groundingMetadata; const safetyRatings = metadata?.safetyRatings; ``` ### Building Interactive Interfaces AI SDK Core can be paired with [AI SDK UI](/docs/ai-sdk-ui/overview), another powerful component of the AI SDK, to streamline the process of building chat, completion, and assistant interfaces with popular frameworks like Next.js, Nuxt, SvelteKit, and SolidStart. AI SDK UI provides robust abstractions that simplify the complex tasks of managing chat streams and UI updates on the frontend, enabling you to develop dynamic AI-driven interfaces more efficiently. With four main hooks — [`useChat`](/docs/reference/ai-sdk-ui/use-chat), [`useCompletion`](/docs/reference/ai-sdk-ui/use-completion), [`useObject`](/docs/reference/ai-sdk-ui/use-object), and [`useAssistant`](/docs/reference/ai-sdk-ui/use-assistant) — you can incorporate real-time chat capabilities, text completions, streamed JSON, and interactive assistant features into your app. Let's explore building a chatbot with [Next.js](https://nextjs.org), the AI SDK, and Gemini 2.5 Flash: In a new Next.js application, first install the AI SDK and the Google Generative AI provider: <Snippet text="pnpm install ai @ai-sdk/google" /> Then, create a route handler for the chat endpoint: ```tsx filename="app/api/chat/route.ts" import { google } from '@ai-sdk/google'; import { streamText, UIMessage, convertToModelMessages } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: google('gemini-2.5-flash'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } ``` Finally, update the root page (`app/page.tsx`) to use the `useChat` hook: ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat(); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'Gemini: '} {message.parts.map((part, i) => { switch (part.type) { case 'text': return <div key={`${message.id}-${i}`}>{part.text}</div>; } })} </div> ))} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` The useChat hook on your root page (`app/page.tsx`) will make a request to your AI provider endpoint (`app/api/chat/route.ts`) whenever the user submits a message. The messages are then displayed in the chat UI. ## Get Started Ready to dive in? Here's how you can begin: 1. Explore the documentation at [ai-sdk.dev/docs](/docs) to understand the capabilities of the AI SDK. 2. Check out practical examples at [ai-sdk.dev/examples](/examples) to see the SDK in action. 3. Dive deeper with advanced guides on topics like Retrieval-Augmented Generation (RAG) at [ai-sdk.dev/docs/guides](/docs/guides). 4. Use ready-to-deploy AI templates at [vercel.com/templates?type=ai](https://vercel.com/templates?type=ai). 5. Read more about the [Google Generative AI provider](/providers/ai-sdk-providers/google-generative-ai). --- File: /ai/content/cookbook/00-guides/18-claude-4.mdx --- --- title: Get started with Claude 4 description: Get started with Claude 4 using the AI SDK. tags: ['getting-started'] --- # Get started with Claude 4 With the release of Claude 4, there has never been a better time to start building AI applications, particularly those that require complex reasoning capabilities and advanced intelligence. The [AI SDK](/) is a powerful TypeScript toolkit for building AI applications with large language models (LLMs) like Claude 4 alongside popular frameworks like React, Next.js, Vue, Svelte, Node.js, and more. ## Claude 4 Claude 4 is Anthropic's most advanced model family to date, offering exceptional capabilities across reasoning, instruction following, coding, and knowledge tasks. Available in two variants—Sonnet and Opus—Claude 4 delivers state-of-the-art performance with enhanced reliability and control. Claude 4 builds on the extended thinking capabilities introduced in Claude 3.7, allowing for even more sophisticated problem-solving through careful, step-by-step reasoning. Claude 4 excels at complex reasoning, code generation and analysis, detailed content creation, and agentic capabilities, making it ideal for powering sophisticated AI workflows, customer-facing agents, and applications requiring nuanced understanding and responses. Claude Opus 4 is an excellent coding model, leading on SWE-bench (72.5%) and Terminal-bench (43.2%), with the ability to sustain performance on long-running tasks that require focused effort and thousands of steps. Claude Sonnet 4 significantly improves on Sonnet 3.7, excelling in coding with 72.7% on SWE-bench while balancing performance and efficiency. ### Prompt Engineering for Claude 4 Models Claude 4 models respond well to clear, explicit instructions. The following best practices can help achieve optimal performance: 1. **Provide explicit instructions**: Clearly state what you want the model to do, including specific steps or formats for the response. 2. **Include context and motivation**: Explain why a task is being performed to help the model better understand the underlying goals. 3. **Avoid negative examples**: When providing examples, only demonstrate the behavior you want to see, not what you want to avoid. ## Getting Started with the AI SDK The AI SDK is the TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js, and more. Integrating LLMs into applications is complicated and heavily dependent on the specific model provider you use. The AI SDK abstracts away the differences between model providers, eliminates boilerplate code for building chatbots, and allows you to go beyond text output to generate rich, interactive components. At the center of the AI SDK is [AI SDK Core](/docs/ai-sdk-core/overview), which provides a unified API to call any LLM. The code snippet below is all you need to call Claude 4 Sonnet with the AI SDK: ```ts import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; const { text, reasoningText, reasoning } = await generateText({ model: anthropic('claude-sonnet-4-20250514'), prompt: 'How will quantum computing impact cryptography by 2050?', }); console.log(text); ``` ### Reasoning Ability Claude 4 enhances the extended thinking capabilities first introduced in Claude 3.7 Sonnet—the ability to solve complex problems with careful, step-by-step reasoning. Additionally, both Opus 4 and Sonnet 4 can now use tools during extended thinking, allowing Claude to alternate between reasoning and tool use to improve responses. You can enable extended thinking using the `thinking` provider option and specifying a thinking budget in tokens. For interleaved thinking (where Claude can think in between tool calls) you'll need to enable a beta feature using the `anthropic-beta` header: ```ts import { anthropic, AnthropicProviderOptions } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; const { text, reasoningText, reasoning } = await generateText({ model: anthropic('claude-sonnet-4-20250514'), prompt: 'How will quantum computing impact cryptography by 2050?', providerOptions: { anthropic: { thinking: { type: 'enabled', budgetTokens: 15000 }, } satisfies AnthropicProviderOptions, }, headers: { 'anthropic-beta': 'interleaved-thinking-2025-05-14', }, }); console.log(text); // text response console.log(reasoningText); // reasoning text console.log(reasoning); // reasoning details including redacted reasoning ``` ### Building Interactive Interfaces AI SDK Core can be paired with [AI SDK UI](/docs/ai-sdk-ui/overview), another powerful component of the AI SDK, to streamline the process of building chat, completion, and assistant interfaces with popular frameworks like Next.js, Nuxt, SvelteKit, and SolidStart. AI SDK UI provides robust abstractions that simplify the complex tasks of managing chat streams and UI updates on the frontend, enabling you to develop dynamic AI-driven interfaces more efficiently. With four main hooks — [`useChat`](/docs/reference/ai-sdk-ui/use-chat), [`useCompletion`](/docs/reference/ai-sdk-ui/use-completion), [`useObject`](/docs/reference/ai-sdk-ui/use-object), and [`useAssistant`](/docs/reference/ai-sdk-ui/use-assistant) — you can incorporate real-time chat capabilities, text completions, streamed JSON, and interactive assistant features into your app. Let's explore building a chatbot with [Next.js](https://nextjs.org), the AI SDK, and Claude Sonnet 4: In a new Next.js application, first install the AI SDK and the Anthropic provider: <Snippet text="pnpm install ai @ai-sdk/anthropic" /> Then, create a route handler for the chat endpoint: ```tsx filename="app/api/chat/route.ts" import { anthropic, AnthropicProviderOptions } from '@ai-sdk/anthropic'; import { streamText, convertToModelMessages, type UIMessage } from 'ai'; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: anthropic('claude-sonnet-4-20250514'), messages: convertToModelMessages(messages), headers: { 'anthropic-beta': 'interleaved-thinking-2025-05-14', }, providerOptions: { anthropic: { thinking: { type: 'enabled', budgetTokens: 15000 }, } satisfies AnthropicProviderOptions, }, }); return result.toUIMessageStreamResponse({ sendReasoning: true, }); } ``` <Note> You can forward the model's reasoning tokens to the client with `sendReasoning: true` in the `toUIMessageStreamResponse` method. </Note> Finally, update the root page (`app/page.tsx`) to use the `useChat` hook: ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat' }), }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (input.trim()) { sendMessage({ text: input }); setInput(''); } }; return ( <div className="flex flex-col h-screen max-w-2xl mx-auto p-4"> <div className="flex-1 overflow-y-auto space-y-4 mb-4"> {messages.map(message => ( <div key={message.id} className={`p-3 rounded-lg ${ message.role === 'user' ? 'bg-blue-50 ml-auto' : 'bg-gray-50' }`} > <p className="font-semibold"> {message.role === 'user' ? 'You' : 'Claude 4'} </p> {message.parts.map((part, index) => { if (part.type === 'text') { return ( <div key={index} className="mt-1"> {part.text} </div> ); } if (part.type === 'reasoning') { return ( <pre key={index} className="bg-gray-100 p-2 rounded mt-2 text-xs overflow-x-auto" > <details> <summary className="cursor-pointer"> View reasoning </summary> {part.text} </details> </pre> ); } })} </div> ))} </div> <form onSubmit={handleSubmit} className="flex gap-2"> <input name="prompt" value={input} onChange={e => setInput(e.target.value)} className="flex-1 p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Ask Claude 4 something..." /> <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" > Send </button> </form> </div> ); } ``` <Note> You can access the model's reasoning tokens with the `reasoning` part on the message `parts`. The reasoning text is available in the `text` property of the reasoning part. </Note> The useChat hook on your root page (`app/page.tsx`) will make a request to your LLM provider endpoint (`app/api/chat/route.ts`) whenever the user submits a message. The messages are then displayed in the chat UI. ### Claude 4 Model Variants Claude 4 is available in two variants, each optimized for different use cases: - **Claude Sonnet 4**: Balanced performance suitable for most enterprise applications, with significant improvements over Sonnet 3.7. - **Claude Opus 4**: Anthropic's most powerful model and the best coding model available. Excels at sustained performance on long-running tasks that require focused effort and thousands of steps, with the ability to work continuously for several hours. ## Get Started Ready to dive in? Here's how you can begin: 1. Explore the documentation at [ai-sdk.dev/docs](/docs) to understand the capabilities of the AI SDK. 2. Check out practical examples at [ai-sdk.dev/examples](/examples) to see the SDK in action. 3. Dive deeper with advanced guides on topics like Retrieval-Augmented Generation (RAG) at [ai-sdk.dev/docs/guides](/docs/guides). 4. Use ready-to-deploy AI templates at [vercel.com/templates?type=ai](https://vercel.com/templates?type=ai). --- File: /ai/content/cookbook/00-guides/19-openai-responses.mdx --- --- title: OpenAI Responses API description: Get started with the OpenAI Responses API using the AI SDK. tags: ['getting-started', 'agents'] --- # Get started with OpenAI Responses API With the [release of OpenAI's responses API](https://openai.com/index/new-tools-for-building-agents/), there has never been a better time to start building AI applications, particularly those that require a deeper understanding of the world. The [AI SDK](/) is a powerful TypeScript toolkit for building AI applications with large language models (LLMs) alongside popular frameworks like React, Next.js, Vue, Svelte, Node.js, and more. ## OpenAI Responses API OpenAI recently released the Responses API, a brand new way to build applications on OpenAI's platform. The new API offers a way to persist chat history, a web search tool for grounding LLM responses, file search tool for finding relevant files, and a computer use tool for building agents that can interact with and operate computers. Let's explore how to use the Responses API with the AI SDK. ## Getting Started with the AI SDK The AI SDK is the TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js, and more. Integrating LLMs into applications is complicated and heavily dependent on the specific model provider you use. The AI SDK abstracts away the differences between model providers, eliminates boilerplate code for building chatbots, and allows you to go beyond text output to generate rich, interactive components. At the center of the AI SDK is [AI SDK Core](/docs/ai-sdk-core/overview), which provides a unified API to call any LLM. The code snippet below is all you need to call GPT-4o with the new Responses API using the AI SDK: ```ts import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; const { text } = await generateText({ model: openai.responses('gpt-4o'), prompt: 'Explain the concept of quantum entanglement.', }); ``` ### Generating Structured Data While text generation can be useful, you might want to generate structured JSON data. For example, you might want to extract information from text, classify data, or generate synthetic data. AI SDK Core provides two functions ([`generateObject`](/docs/reference/ai-sdk-core/generate-object) and [`streamObject`](/docs/reference/ai-sdk-core/stream-object)) to generate structured data, allowing you to constrain model outputs to a specific schema. ```ts import { generateObject } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const { object } = await generateObject({ model: openai.responses('gpt-4o'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.object({ name: z.string(), amount: z.string() })), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); ``` This code snippet will generate a type-safe recipe that conforms to the specified zod schema. ### Using Tools with the AI SDK The Responses API supports tool calling out of the box, allowing it to interact with external systems and perform discrete tasks. Here's an example of using tool calling with the AI SDK: ```ts import { generateText, tool } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const { text } = await generateText({ model: openai.responses('gpt-4o'), prompt: 'What is the weather like today in San Francisco?', tools: { getWeather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), // enable multi-step 'agentic' LLM calls }); ``` This example demonstrates how `stopWhen` transforms a single LLM call into an agent. The `stopWhen: stepCountIs(5)` parameter allows the model to autonomously call tools, analyze results, and make additional tool calls as needed - turning what would be a simple one-shot completion into an intelligent agent that can chain multiple actions together to complete complex tasks. ### Web Search Tool The Responses API introduces a built-in tool for grounding responses called `webSearch`. With this tool, the model can access the internet to find relevant information for its responses. ```ts import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; const result = await generateText({ model: openai.responses('gpt-4o-mini'), prompt: 'What happened in San Francisco last week?', tools: { web_search_preview: openai.tools.webSearchPreview(), }, }); console.log(result.text); console.log(result.sources); ``` The `webSearch` tool also allows you to specify query-specific metadata that can be used to improve the quality of the search results. ```ts import { generateText } from 'ai'; const result = await generateText({ model: openai.responses('gpt-4o-mini'), prompt: 'What happened in San Francisco last week?', tools: { web_search_preview: openai.tools.webSearchPreview({ searchContextSize: 'high', userLocation: { type: 'approximate', city: 'San Francisco', region: 'California', }, }), }, }); console.log(result.text); console.log(result.sources); ``` ## Using Persistence With the Responses API, you can persist chat history with OpenAI across requests. This allows you to send just the user's last message and OpenAI can access the entire chat history: ```tsx filename="app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; const result1 = await generateText({ model: openai.responses('gpt-4o-mini'), prompt: 'Invent a new holiday and describe its traditions.', }); const result2 = await generateText({ model: openai.responses('gpt-4o-mini'), prompt: 'Summarize in 2 sentences', providerOptions: { openai: { previousResponseId: result1.providerMetadata?.openai.responseId as string, }, }, }); ``` ## Migrating from Completions API Migrating from the OpenAI Completions API (via the AI SDK) to the new Responses API is simple. To migrate, simply change your provider instance from `openai(modelId)` to `openai.responses(modelId)`: ```ts import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; // Completions API const { text } = await generateText({ model: openai('gpt-4o'), prompt: 'Explain the concept of quantum entanglement.', }); // Responses API const { text } = await generateText({ model: openai.responses('gpt-4o'), prompt: 'Explain the concept of quantum entanglement.', }); ``` When using the Responses API, provider specific options that were previously specified on the model provider instance have now moved to the `providerOptions` object: ```ts import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; // Completions API const { text } = await generateText({ model: openai('gpt-4o'), prompt: 'Explain the concept of quantum entanglement.', providerOptions: { openai: { parallelToolCalls: false, }, }, }); // Responses API const { text } = await generateText({ model: openai.responses('gpt-4o'), prompt: 'Explain the concept of quantum entanglement.', providerOptions: { openai: { parallelToolCalls: false, }, }, }); ``` ## Get Started Ready to get started? Here's how you can dive in: 1. Explore the documentation at [ai-sdk.dev/docs](/docs) to understand the full capabilities of the AI SDK. 2. Check out practical examples at [ai-sdk.dev/examples](/examples) to see the SDK in action and get inspired for your own projects. 3. Dive deeper with advanced guides on topics like Retrieval-Augmented Generation (RAG) and multi-modal chat at [ai-sdk.dev/docs/guides](/docs/guides). 4. Check out ready-to-deploy AI templates at [vercel.com/templates?type=ai](https://vercel.com/templates?type=ai). --- File: /ai/content/cookbook/00-guides/20-sonnet-3-7.mdx --- --- title: Get started with Claude 3.7 Sonnet description: Get started with Claude 3.7 Sonnet using the AI SDK. tags: ['getting-started'] --- # Get started with Claude 3.7 Sonnet With the [release of Claude 3.7 Sonnet](https://www.anthropic.com/news/claude-3-7-sonnet), there has never been a better time to start building AI applications, particularly those that require complex reasoning capabilities. The [AI SDK](/) is a powerful TypeScript toolkit for building AI applications with large language models (LLMs) like Claude 3.7 Sonnet alongside popular frameworks like React, Next.js, Vue, Svelte, Node.js, and more. ## Claude 3.7 Sonnet Claude 3.7 Sonnet is Anthropic's most intelligent model to date and the first Claude model to offer extended thinking—the ability to solve complex problems with careful, step-by-step reasoning. With Claude 3.7 Sonnet, you can balance speed and quality by choosing between standard thinking for near-instant responses or extended thinking or advanced reasoning. Claude 3.7 Sonnet is state-of-the-art for coding, and delivers advancements in computer use, agentic capabilities, complex reasoning, and content generation. With frontier performance and more control over speed, Claude 3.7 Sonnet is a great choice for powering AI agents, especially customer-facing agents, and complex AI workflows. ## Getting Started with the AI SDK The AI SDK is the TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js, and more. Integrating LLMs into applications is complicated and heavily dependent on the specific model provider you use. The AI SDK abstracts away the differences between model providers, eliminates boilerplate code for building chatbots, and allows you to go beyond text output to generate rich, interactive components. At the center of the AI SDK is [AI SDK Core](/docs/ai-sdk-core/overview), which provides a unified API to call any LLM. The code snippet below is all you need to call Claude 3.7 Sonnet with the AI SDK: ```ts import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; const { text, reasoning, reasoningDetails } = await generateText({ model: anthropic('claude-3-7-sonnet-20250219'), prompt: 'How many people will live in the world in 2040?', }); console.log(text); // text response ``` The unified interface also means that you can easily switch between providers by changing just two lines of code. For example, to use Claude 3.7 Sonnet via Amazon Bedrock: ```ts import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; const { reasoning, text } = await generateText({ model: bedrock('anthropic.claude-3-7-sonnet-20250219-v1:0'), prompt: 'How many people will live in the world in 2040?', }); ``` ### Reasoning Ability Claude 3.7 Sonnet introduces a new extended thinking—the ability to solve complex problems with careful, step-by-step reasoning. You can enable it using the `thinking` provider option and specifying a thinking budget in tokens: ```ts import { anthropic, AnthropicProviderOptions } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; const { text, reasoning, reasoningDetails } = await generateText({ model: anthropic('claude-3-7-sonnet-20250219'), prompt: 'How many people will live in the world in 2040?', providerOptions: { anthropic: { thinking: { type: 'enabled', budgetTokens: 12000 }, } satisfies AnthropicProviderOptions, }, }); console.log(reasoning); // reasoning text console.log(reasoningDetails); // reasoning details including redacted reasoning console.log(text); // text response ``` ### Building Interactive Interfaces AI SDK Core can be paired with [AI SDK UI](/docs/ai-sdk-ui/overview), another powerful component of the AI SDK, to streamline the process of building chat, completion, and assistant interfaces with popular frameworks like Next.js, Nuxt, and SvelteKit. AI SDK UI provides robust abstractions that simplify the complex tasks of managing chat streams and UI updates on the frontend, enabling you to develop dynamic AI-driven interfaces more efficiently. With four main hooks — [`useChat`](/docs/reference/ai-sdk-ui/use-chat), [`useCompletion`](/docs/reference/ai-sdk-ui/use-completion), and [`useObject`](/docs/reference/ai-sdk-ui/use-object) — you can incorporate real-time chat capabilities, text completions, streamed JSON, and interactive assistant features into your app. Let's explore building a chatbot with [Next.js](https://nextjs.org), the AI SDK, and Claude 3.7 Sonnet: In a new Next.js application, first install the AI SDK and the Anthropic provider: <Snippet text="pnpm install ai @ai-sdk/anthropic" /> Then, create a route handler for the chat endpoint: ```tsx filename="app/api/chat/route.ts" import { anthropic, AnthropicProviderOptions } from '@ai-sdk/anthropic'; import { streamText, convertToModelMessages, type UIMessage } from 'ai'; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: anthropic('claude-3-7-sonnet-20250219'), messages: convertToModelMessages(messages), providerOptions: { anthropic: { thinking: { type: 'enabled', budgetTokens: 12000 }, } satisfies AnthropicProviderOptions, }, }); return result.toUIMessageStreamResponse({ sendReasoning: true, }); } ``` <Note> You can forward the model's reasoning tokens to the client with `sendReasoning: true` in the `toUIMessageStreamResponse` method. </Note> Finally, update the root page (`app/page.tsx`) to use the `useChat` hook: ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat' }), }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (input.trim()) { sendMessage({ text: input }); setInput(''); } }; return ( <> {messages.map(message => ( <div key={message.id}> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, index) => { // text parts: if (part.type === 'text') { return <div key={index}>{part.text}</div>; } // reasoning parts: if (part.type === 'reasoning') { return <pre key={index}>{part.text}</pre>; } })} </div> ))} <form onSubmit={handleSubmit}> <input name="prompt" value={input} onChange={e => setInput(e.target.value)} /> <button type="submit">Send</button> </form> </> ); } ``` <Note> You can access the model's reasoning tokens with the `reasoning` part on the message `parts`. </Note> The useChat hook on your root page (`app/page.tsx`) will make a request to your LLM provider endpoint (`app/api/chat/route.ts`) whenever the user submits a message. The messages are then displayed in the chat UI. ## Get Started Ready to dive in? Here's how you can begin: 1. Explore the documentation at [ai-sdk.dev/docs](/docs) to understand the capabilities of the AI SDK. 2. Check out practical examples at [ai-sdk.dev/examples](/examples) to see the SDK in action. 3. Dive deeper with advanced guides on topics like Retrieval-Augmented Generation (RAG) at [ai-sdk.dev/docs/guides](/docs/guides). 4. Use ready-to-deploy AI templates at [vercel.com/templates?type=ai](https://vercel.com/templates?type=ai). Claude 3.7 Sonnet opens new opportunities for reasoning-intensive AI applications. Start building today and leverage the power of advanced reasoning in your AI projects. --- File: /ai/content/cookbook/00-guides/21-llama-3_1.mdx --- --- title: Get started with Llama 3.1 description: Get started with Llama 3.1 using the AI SDK. tags: ['getting-started'] --- # Get started with Llama 3.1 <Note> The current generation of Llama models is 3.3. Please note that while this guide focuses on Llama 3.1, the newer Llama 3.3 models are now available and may offer improved capabilities. The concepts and integration techniques described here remain applicable, though you may want to use the latest generation models for optimal performance. </Note> With the [release of Llama 3.1](https://ai.meta.com/blog/meta-llama-3-1/), there has never been a better time to start building AI applications. The [AI SDK](/) is a powerful TypeScript toolkit for building AI application with large language models (LLMs) like Llama 3.1 alongside popular frameworks like React, Next.js, Vue, Svelte, Node.js, and more ## Llama 3.1 The release of Meta's Llama 3.1 is an important moment in AI development. As the first state-of-the-art open weight AI model, Llama 3.1 is helping accelerate developers building AI apps. Available in 8B, 70B, and 405B sizes, these instruction-tuned models work well for tasks like dialogue generation, translation, reasoning, and code generation. ## Benchmarks Llama 3.1 surpasses most available open-source chat models on common industry benchmarks and even outperforms some closed-source models, offering superior performance in language nuances, contextual understanding, and complex multi-step tasks. The models' refined post-training processes significantly improve response alignment, reduce false refusal rates, and enhance answer diversity, making Llama 3.1 a powerful and accessible tool for building generative AI applications. ![Llama 3.1 Benchmarks](/images/llama-3_1-benchmarks.png) Source: [Meta AI - Llama 3.1 Model Card](https://github.com/meta-llama/llama-models/blob/main/models/llama3_1/MODEL_CARD.md) ## Choosing Model Size Llama 3.1 includes a new 405B parameter model, becoming the largest open-source model available today. This model is designed to handle the most complex and demanding tasks. When choosing between the different sizes of Llama 3.1 models (405B, 70B, 8B), consider the trade-off between performance and computational requirements. The 405B model offers the highest accuracy and capability for complex tasks but requires significant computational resources. The 70B model provides a good balance of performance and efficiency for most applications, while the 8B model is suitable for simpler tasks or resource-constrained environments where speed and lower computational overhead are priorities. ## Getting Started with the AI SDK The AI SDK is the TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js, and more. Integrating LLMs into applications is complicated and heavily dependent on the specific model provider you use. The AI SDK abstracts away the differences between model providers, eliminates boilerplate code for building chatbots, and allows you to go beyond text output to generate rich, interactive components. At the center of the AI SDK is [AI SDK Core](/docs/ai-sdk-core/overview), which provides a unified API to call any LLM. The code snippet below is all you need to call Llama 3.1 (using [DeepInfra](https://deepinfra.com)) with the AI SDK: ```ts import { deepinfra } from '@ai-sdk/deepinfra'; import { generateText } from 'ai'; const { text } = await generateText({ model: deepinfra('meta-llama/Meta-Llama-3.1-405B-Instruct'), prompt: 'What is love?', }); ``` <Note> Llama 3.1 is available to use with many AI SDK providers including [DeepInfra](/providers/ai-sdk-providers/deepinfra), [Amazon Bedrock](/providers/ai-sdk-providers/amazon-bedrock), [Baseten](/providers/openai-compatible-providers/baseten) [Fireworks](/providers/ai-sdk-providers/fireworks), and more. </Note> AI SDK Core abstracts away the differences between model providers, allowing you to focus on building great applications. Prefer to use [Amazon Bedrock](/providers/ai-sdk-providers/amazon-bedrock)? The unified interface also means that you can easily switch between models by changing just two lines of code. ```tsx highlight="2,5" import { generateText } from 'ai'; import { bedrock } from '@ai-sdk/amazon-bedrock'; const { text } = await generateText({ model: bedrock('meta.llama3-1-405b-instruct-v1'), prompt: 'What is love?', }); ``` ### Streaming the Response To stream the model's response as it's being generated, update your code snippet to use the [`streamText`](/docs/reference/ai-sdk-core/stream-text) function. ```tsx import { streamText } from 'ai'; import { deepinfra } from '@ai-sdk/deepinfra'; const { textStream } = streamText({ model: deepinfra('meta-llama/Meta-Llama-3.1-405B-Instruct'), prompt: 'What is love?', }); ``` ### Generating Structured Data While text generation can be useful, you might want to generate structured JSON data. For example, you might want to extract information from text, classify data, or generate synthetic data. AI SDK Core provides two functions ([`generateObject`](/docs/reference/ai-sdk-core/generate-object) and [`streamObject`](/docs/reference/ai-sdk-core/stream-object)) to generate structured data, allowing you to constrain model outputs to a specific schema. ```ts import { generateObject } from 'ai'; import { deepinfra } from '@ai-sdk/deepinfra'; import { z } from 'zod'; const { object } = await generateObject({ model: deepinfra('meta-llama/Meta-Llama-3.1-70B-Instruct'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.object({ name: z.string(), amount: z.string() })), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); ``` This code snippet will generate a type-safe recipe that conforms to the specified zod schema. ### Tools While LLMs have incredible generation capabilities, they struggle with discrete tasks (e.g. mathematics) and interacting with the outside world (e.g. getting the weather). The solution: tools, which are like programs that you provide to the model, which it can choose to call as necessary. ### Using Tools with the AI SDK The AI SDK supports tool usage across several of its functions, including [`generateText`](/docs/reference/ai-sdk-core/generate-text) and [`streamUI`](/docs/reference/ai-sdk-rsc/stream-ui). By passing one or more tools to the `tools` parameter, you can extend the capabilities of LLMs, allowing them to perform discrete tasks and interact with external systems. Here's an example of how you can use a tool with the AI SDK and Llama 3.1: ```ts import { generateText, tool } from 'ai'; import { deepinfra } from '@ai-sdk/deepinfra'; import { z } from 'zod'; const { text } = await generateText({ model: deepinfra('meta-llama/Meta-Llama-3.1-70B-Instruct'), prompt: 'What is the weather like today?', tools: { getWeather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, }); ``` In this example, the `getWeather` tool allows the model to fetch real-time weather data, enhancing its ability to provide accurate and up-to-date information. ### Agents Agents take your AI applications a step further by allowing models to execute multiple steps (i.e. tools) in a non-deterministic way, making decisions based on context and user input. Agents use LLMs to choose the next step in a problem-solving process. They can reason at each step and make decisions based on the evolving context. ### Implementing Agents with the AI SDK The AI SDK supports agent implementation through the `maxSteps` parameter. This allows the model to make multiple decisions and tool calls in a single interaction. Here's an example of an agent that solves math problems: ```tsx import { generateText, tool } from 'ai'; import { deepinfra } from '@ai-sdk/deepinfra'; import * as mathjs from 'mathjs'; import { z } from 'zod'; const problem = 'Calculate the profit for a day if revenue is $5000 and expenses are $3500.'; const { text: answer } = await generateText({ model: deepinfra('meta-llama/Meta-Llama-3.1-70B-Instruct'), system: 'You are solving math problems. Reason step by step. Use the calculator when necessary.', prompt: problem, tools: { calculate: tool({ description: 'A tool for evaluating mathematical expressions.', inputSchema: z.object({ expression: z.string() }), execute: async ({ expression }) => mathjs.evaluate(expression), }), }, maxSteps: 5, }); ``` In this example, the agent can use the calculator tool multiple times if needed, reasoning through the problem step by step. ### Building Interactive Interfaces AI SDK Core can be paired with [AI SDK UI](/docs/ai-sdk-ui/overview), another powerful component of the AI SDK, to streamline the process of building chat, completion, and assistant interfaces with popular frameworks like Next.js, Nuxt, and SvelteKit. AI SDK UI provides robust abstractions that simplify the complex tasks of managing chat streams and UI updates on the frontend, enabling you to develop dynamic AI-driven interfaces more efficiently. With four main hooks — [`useChat`](/docs/reference/ai-sdk-ui/use-chat), [`useCompletion`](/docs/reference/ai-sdk-ui/use-completion), and [`useObject`](/docs/reference/ai-sdk-ui/use-object) — you can incorporate real-time chat capabilities, text completions, streamed JSON, and interactive assistant features into your app. Let's explore building a chatbot with [Next.js](https://nextjs.org), the AI SDK, and Llama 3.1 (via [DeepInfra](https://deepinfra.com)): ```tsx filename="app/api/chat/route.ts" import { deepinfra } from '@ai-sdk/deepinfra'; import { convertToModelMessages, streamText } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: deepinfra('meta-llama/Meta-Llama-3.1-70B-Instruct'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } ``` ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; export default function Page() { const { messages, input, handleInputChange, handleSubmit } = useChat(); return ( <> {messages.map(message => ( <div key={message.id}> {message.role === 'user' ? 'User: ' : 'AI: '} {message.content} </div> ))} <form onSubmit={handleSubmit}> <input name="prompt" value={input} onChange={handleInputChange} /> <button type="submit">Submit</button> </form> </> ); } ``` The useChat hook on your root page (`app/page.tsx`) will make a request to your AI provider endpoint (`app/api/chat/route.ts`) whenever the user submits a message. The messages are then streamed back in real-time and displayed in the chat UI. This enables a seamless chat experience where the user can see the AI response as soon as it is available, without having to wait for the entire response to be received. ### Going Beyond Text The AI SDK's React Server Components (RSC) API enables you to create rich, interactive interfaces that go beyond simple text generation. With the [`streamUI`](/docs/reference/ai-sdk-rsc/stream-ui) function, you can dynamically stream React components from the server to the client. Let's dive into how you can leverage tools with [AI SDK RSC](/docs/ai-sdk-rsc/overview) to build a generative user interface with Next.js (App Router). First, create a Server Action. ```tsx filename="app/actions.tsx" 'use server'; import { streamUI } from '@ai-sdk/rsc'; import { deepinfra } from '@ai-sdk/deepinfra'; import { z } from 'zod'; export async function streamComponent() { const result = await streamUI({ model: deepinfra('meta-llama/Meta-Llama-3.1-70B-Instruct'), prompt: 'Get the weather for San Francisco', text: ({ content }) => <div>{content}</div>, tools: { getWeather: { description: 'Get the weather for a location', inputSchema: z.object({ location: z.string() }), generate: async function* ({ location }) { yield <div>loading...</div>; const weather = '25c'; // await getWeather(location); return ( <div> the weather in {location} is {weather}. </div> ); }, }, }, }); return result.value; } ``` In this example, if the model decides to use the `getWeather` tool, it will first yield a `div` while fetching the weather data, then return a weather component with the fetched data (note: static data in this example). This allows for a more dynamic and responsive UI that can adapt based on the AI's decisions and external data. On the frontend, you can call this Server Action like any other asynchronous function in your application. In this case, the function returns a regular React component. ```tsx filename="app/page.tsx" 'use client'; import { useState } from 'react'; import { streamComponent } from './actions'; export default function Page() { const [component, setComponent] = useState<React.ReactNode>(); return ( <div> <form onSubmit={async e => { e.preventDefault(); setComponent(await streamComponent()); }} > <button>Stream Component</button> </form> <div>{component}</div> </div> ); } ``` To see AI SDK RSC in action, check out our open-source [Next.js Gemini Chatbot](https://gemini.vercel.ai/). ## Migrate from OpenAI One of the key advantages of the AI SDK is its unified API, which makes it incredibly easy to switch between different AI models and providers. This flexibility is particularly useful when you want to migrate from one model to another, such as moving from OpenAI's GPT models to Meta's Llama models hosted on DeepInfra. Here's how simple the migration process can be: **OpenAI Example:** ```tsx import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; const { text } = await generateText({ model: openai('gpt-4.1'), prompt: 'What is love?', }); ``` **Llama on DeepInfra Example:** ```tsx import { generateText } from 'ai'; import { deepinfra } from '@ai-sdk/deepinfra'; const { text } = await generateText({ model: deepinfra('meta-llama/Meta-Llama-3.1-70B-Instruct'), prompt: 'What is love?', }); ``` Thanks to the unified API, the core structure of the code remains the same. The main differences are: 1. Creating a DeepInfra client 2. Changing the model name from `openai("gpt-4.1")` to `deepinfra("meta-llama/Meta-Llama-3.1-70B-Instruct")`. With just these few changes, you've migrated from using OpenAI's GPT-4-Turbo to Meta's Llama 3.1 hosted on DeepInfra. The `generateText` function and its usage remain identical, showcasing the power of the AI SDK's unified API. This feature allows you to easily experiment with different models, compare their performance, and choose the best one for your specific use case without having to rewrite large portions of your codebase. ## Prompt Engineering and Fine-tuning While the Llama 3.1 family of models are powerful out-of-the-box, their performance can be enhanced through effective prompt engineering and fine-tuning techniques. ### Prompt Engineering Prompt engineering is the practice of crafting input prompts to elicit desired outputs from language models. It involves structuring and phrasing prompts in ways that guide the model towards producing more accurate, relevant, and coherent responses. For more information on prompt engineering techniques (specific to Llama models), check out these resources: - [Official Llama 3.1 Prompt Guide](https://llama.meta.com/docs/how-to-guides/prompting) - [Prompt Engineering with Llama 3](https://github.com/amitsangani/Llama/blob/main/Llama_3_Prompt_Engineering.ipynb) - [How to prompt Llama 3](https://huggingface.co/blog/llama3#how-to-prompt-llama-3) ### Fine-tuning Fine-tuning involves further training a pre-trained model on a specific dataset or task to customize its performance for particular use cases. This process allows you to adapt Llama 3.1 to your specific domain or application, potentially improving its accuracy and relevance for your needs. To learn more about fine-tuning Llama models, check out these resources: - [Official Fine-tuning Llama Guide](https://llama.meta.com/docs/how-to-guides/fine-tuning) - [Fine-tuning and Inference with Llama 3](https://docs.inferless.com/how-to-guides/how-to-finetune--and-inference-llama3) - [Fine-tuning Models with Fireworks AI](https://docs.fireworks.ai/fine-tuning/fine-tuning-models) - [Fine-tuning Llama with Modal](https://modal.com/docs/examples/llm-finetuning) ## Conclusion The AI SDK offers a powerful and flexible way to integrate cutting-edge AI models like Llama 3.1 into your applications. With AI SDK Core, you can seamlessly switch between different AI models and providers by changing just two lines of code. This flexibility allows for quick experimentation and adaptation, reducing the time required to change models from days to minutes. The AI SDK ensures that your application remains clean and modular, accelerating development and future-proofing against the rapidly evolving landscape. Ready to get started? Here's how you can dive in: 1. Explore the documentation at [ai-sdk.dev/docs](/docs) to understand the full capabilities of the AI SDK. 2. Check out practical examples at [ai-sdk.dev/examples](/examples) to see the SDK in action and get inspired for your own projects. 3. Dive deeper with advanced guides on topics like Retrieval-Augmented Generation (RAG) and multi-modal chat at [ai-sdk.dev/docs/guides](/docs/guides). 4. Check out ready-to-deploy AI templates at [vercel.com/templates?type=ai](https://vercel.com/templates?type=ai). --- File: /ai/content/cookbook/00-guides/22-gpt-4-5.mdx --- --- title: Get started with GPT-4.5 description: Get started with GPT-4.5 using the AI SDK. tags: ['getting-started'] --- # Get started with OpenAI GPT-4.5 With the [release of OpenAI's GPT-4.5 model](https://openai.com/index/introducing-gpt-4-5), there has never been a better time to start building AI applications, particularly those that require a deeper understanding of the world. The [AI SDK](/) is a powerful TypeScript toolkit for building AI applications with large language models (LLMs) like OpenAI GPT-4.5 alongside popular frameworks like React, Next.js, Vue, Svelte, Node.js, and more. ## OpenAI GPT-4.5 OpenAI recently released GPT-4.5, their largest and best model for chat yet. GPT‑4.5 is a step forward in scaling up pretraining and post-training. By scaling unsupervised learning, GPT‑4.5 improves its ability to recognize patterns, draw connections, and generate creative insights without reasoning. Based on early testing, developers may find GPT‑4.5 particularly useful for applications that benefit from its higher emotional intelligence and creativity such as writing help, communication, learning, coaching, and brainstorming. It also shows strong capabilities in agentic planning and execution, including multi-step coding workflows and complex task automation. ### Benchmarks GPT-4.5 demonstrates impressive performance across various benchmarks: - **SimpleQA Accuracy**: 62.5% (higher is better) - **SimpleQA Hallucination Rate**: 37.1% (lower is better) [Source](https://openai.com/index/introducing-gpt-4-5) ### Prompt Engineering for GPT-4.5 GPT-4.5 performs best with the following approach: 1. **Be clear and specific**: GPT-4.5 responds well to direct, well-structured prompts. 2. **Use delimiters for clarity**: Use delimiters like triple quotation marks, XML tags, or section titles to clearly indicate distinct parts of the input. ## Getting Started with the AI SDK The AI SDK is the TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js, and more. Integrating LLMs into applications is complicated and heavily dependent on the specific model provider you use. The AI SDK abstracts away the differences between model providers, eliminates boilerplate code for building chatbots, and allows you to go beyond text output to generate rich, interactive components. At the center of the AI SDK is [AI SDK Core](/docs/ai-sdk-core/overview), which provides a unified API to call any LLM. The code snippet below is all you need to call OpenAI GPT-4.5 with the AI SDK: ```ts import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; const { text } = await generateText({ model: openai('gpt-4.5-preview'), prompt: 'Explain the concept of quantum entanglement.', }); ``` ### Generating Structured Data While text generation can be useful, you might want to generate structured JSON data. For example, you might want to extract information from text, classify data, or generate synthetic data. AI SDK Core provides two functions ([`generateObject`](/docs/reference/ai-sdk-core/generate-object) and [`streamObject`](/docs/reference/ai-sdk-core/stream-object)) to generate structured data, allowing you to constrain model outputs to a specific schema. ```ts import { generateObject } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const { object } = await generateObject({ model: openai('gpt-4.5-preview'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.object({ name: z.string(), amount: z.string() })), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); ``` This code snippet will generate a type-safe recipe that conforms to the specified zod schema. ### Using Tools with the AI SDK GPT-4.5 supports tool calling out of the box, allowing it to interact with external systems and perform discrete tasks. Here's an example of using tool calling with the AI SDK: ```ts import { generateText, tool } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const { text } = await generateText({ model: openai('gpt-4.5-preview'), prompt: 'What is the weather like today in San Francisco?', tools: { getWeather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, }); ``` In this example, the `getWeather` tool allows the model to fetch real-time weather data (simulated for simplicity), enhancing its ability to provide accurate and up-to-date information. ### Building Interactive Interfaces AI SDK Core can be paired with [AI SDK UI](/docs/ai-sdk-ui/overview), another powerful component of the AI SDK, to streamline the process of building chat, completion, and assistant interfaces with popular frameworks like Next.js, Nuxt, and SvelteKit. AI SDK UI provides robust abstractions that simplify the complex tasks of managing chat streams and UI updates on the frontend, enabling you to develop dynamic AI-driven interfaces more efficiently. With four main hooks — [`useChat`](/docs/reference/ai-sdk-ui/use-chat), [`useCompletion`](/docs/reference/ai-sdk-ui/use-completion), and [`useObject`](/docs/reference/ai-sdk-ui/use-object) — you can incorporate real-time chat capabilities, text completions, streamed JSON, and interactive assistant features into your app. Let's explore building a chatbot with [Next.js](https://nextjs.org), the AI SDK, and OpenAI GPT-4.5: In a new Next.js application, first install the AI SDK and the OpenAI provider: <Snippet text="pnpm install ai @ai-sdk/openai @ai-sdk/react" /> Then, create a route handler for the chat endpoint: ```tsx filename="app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; // Allow responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4.5-preview'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } ``` Finally, update the root page (`app/page.tsx`) to use the `useChat` hook: ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; export default function Page() { const { messages, input, handleInputChange, handleSubmit, error } = useChat(); return ( <> {messages.map(message => ( <div key={message.id}> {message.role === 'user' ? 'User: ' : 'AI: '} {message.content} </div> ))} <form onSubmit={handleSubmit}> <input name="prompt" value={input} onChange={handleInputChange} /> <button type="submit">Submit</button> </form> </> ); } ``` The useChat hook on your root page (`app/page.tsx`) will make a request to your AI provider endpoint (`app/api/chat/route.ts`) whenever the user submits a message. The messages are then displayed in the chat UI. ## Get Started Ready to get started? Here's how you can dive in: 1. Explore the documentation at [ai-sdk.dev/docs](/docs) to understand the full capabilities of the AI SDK. 2. Check out practical examples at [ai-sdk.dev/examples](/examples) to see the SDK in action and get inspired for your own projects. 3. Dive deeper with advanced guides on topics like Retrieval-Augmented Generation (RAG) and multi-modal chat at [ai-sdk.dev/docs/guides](/docs/guides). 4. Check out ready-to-deploy AI templates at [vercel.com/templates?type=ai](https://vercel.com/templates?type=ai). --- File: /ai/content/cookbook/00-guides/23-o1.mdx --- --- title: Get started with OpenAI o1 description: Get started with OpenAI o1 using the AI SDK. tags: ['getting-started', 'reasoning'] --- # Get started with OpenAI o1 With the [release of OpenAI's o1 series models](https://openai.com/index/introducing-openai-o1-preview/), there has never been a better time to start building AI applications, particularly those that require complex reasoning capabilities. The [AI SDK](/) is a powerful TypeScript toolkit for building AI applications with large language models (LLMs) like OpenAI o1 alongside popular frameworks like React, Next.js, Vue, Svelte, Node.js, and more. ## OpenAI o1 OpenAI released a series of AI models designed to spend more time thinking before responding. They can reason through complex tasks and solve harder problems than previous models in science, coding, and math. These models, named the o1 series, are trained with reinforcement learning and can "think before they answer". As a result, they are able to produce a long internal chain of thought before responding to a prompt. There are three reasoning models available in the API: 1. [**o1**](https://platform.openai.com/docs/models#o1): Designed to reason about hard problems using broad general knowledge about the world. 1. [**o1-preview**](https://platform.openai.com/docs/models#o1): The original preview version of o1 - slower than o1 but supports streaming. 1. [**o1-mini**](https://platform.openai.com/docs/models#o1): A faster and cheaper version of o1, particularly adept at coding, math, and science tasks where extensive general knowledge isn't required. o1-mini supports streaming. | Model | Streaming | Tools | Object Generation | Reasoning Effort | | ---------- | ------------------- | ------------------- | ------------------- | ------------------- | | o1 | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | o1-preview | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | o1-mini | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | ### Benchmarks OpenAI o1 models excel in scientific reasoning, with impressive performance across various domains: - Ranking in the 89th percentile on competitive programming questions (Codeforces) - Placing among the top 500 students in the US in a qualifier for the USA Math Olympiad (AIME) - Exceeding human PhD-level accuracy on a benchmark of physics, biology, and chemistry problems (GPQA) [Source](https://openai.com/index/learning-to-reason-with-llms/) ### Prompt Engineering for o1 Models The o1 models perform best with straightforward prompts. Some prompt engineering techniques, like few-shot prompting or instructing the model to "think step by step," may not enhance performance and can sometimes hinder it. Here are some best practices: 1. Keep prompts simple and direct: The models excel at understanding and responding to brief, clear instructions without the need for extensive guidance. 2. Avoid chain-of-thought prompts: Since these models perform reasoning internally, prompting them to "think step by step" or "explain your reasoning" is unnecessary. 3. Use delimiters for clarity: Use delimiters like triple quotation marks, XML tags, or section titles to clearly indicate distinct parts of the input, helping the model interpret different sections appropriately. 4. Limit additional context in retrieval-augmented generation (RAG): When providing additional context or documents, include only the most relevant information to prevent the model from overcomplicating its response. ## Getting Started with the AI SDK The AI SDK is the TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js, and more. Integrating LLMs into applications is complicated and heavily dependent on the specific model provider you use. The AI SDK abstracts away the differences between model providers, eliminates boilerplate code for building chatbots, and allows you to go beyond text output to generate rich, interactive components. At the center of the AI SDK is [AI SDK Core](/docs/ai-sdk-core/overview), which provides a unified API to call any LLM. The code snippet below is all you need to call OpenAI o1-mini with the AI SDK: ```ts import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; const { text } = await generateText({ model: openai('o1-mini'), prompt: 'Explain the concept of quantum entanglement.', }); ``` <Note> To use the o1 series of models, you must either be using @ai-sdk/openai version 0.0.59 or greater, or set `temperature: 1`. </Note> AI SDK Core abstracts away the differences between model providers, allowing you to focus on building great applications. The unified interface also means that you can easily switch between models by changing just one line of code. ```ts highlight="5" import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; const { text } = await generateText({ model: openai('o1'), prompt: 'Explain the concept of quantum entanglement.', }); ``` <Note> System messages are automatically converted to OpenAI developer messages. </Note> ### Refining Reasoning Effort You can control the amount of reasoning effort expended by o1 through the `reasoningEffort` parameter. This parameter can be set to `'low'`, `'medium'`, or `'high'` to adjust how much time and computation the model spends on internal reasoning before producing a response. ```ts highlight="9" import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; // Reduce reasoning effort for faster responses const { text } = await generateText({ model: openai('o1'), prompt: 'Explain quantum entanglement briefly.', providerOptions: { openai: { reasoningEffort: 'low' }, }, }); ``` <Note> The `reasoningEffort` parameter is only supported by o1 and has no effect on other models. </Note> ### Generating Structured Data While text generation can be useful, you might want to generate structured JSON data. For example, you might want to extract information from text, classify data, or generate synthetic data. AI SDK Core provides two functions ([`generateObject`](/docs/reference/ai-sdk-core/generate-object) and [`streamObject`](/docs/reference/ai-sdk-core/stream-object)) to generate structured data, allowing you to constrain model outputs to a specific schema. ```ts import { generateObject } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const { object } = await generateObject({ model: openai('o1'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.object({ name: z.string(), amount: z.string() })), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); ``` This code snippet will generate a type-safe recipe that conforms to the specified zod schema. <Note> Structured object generation is only supported with o1, not o1-preview or o1-mini. </Note> ### Tools While LLMs have incredible generation capabilities, they struggle with discrete tasks (e.g. mathematics) and interacting with the outside world (e.g. getting the weather). The solution: [tools](/docs/foundations/tools), which are like programs that you provide to the model, which it can choose to call as necessary. ### Using Tools with the AI SDK The AI SDK supports tool usage across several of its functions, like [`generateText`](/docs/reference/ai-sdk-core/generate-text) and [`streamText`](/docs/reference/ai-sdk-core/stream-text). By passing one or more tools to the `tools` parameter, you can extend the capabilities of LLMs, allowing them to perform discrete tasks and interact with external systems. Here's an example of how you can use a tool with the AI SDK and o1: ```ts import { generateText, tool } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const { text } = await generateText({ model: openai('o1'), prompt: 'What is the weather like today?', tools: { getWeather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, }); ``` In this example, the `getWeather` tool allows the model to fetch real-time weather data (simulated for simplicity), enhancing its ability to provide accurate and up-to-date information. <Note>Tools are only compatible with o1, not o1-preview or o1-mini.</Note> ### Building Interactive Interfaces AI SDK Core can be paired with [AI SDK UI](/docs/ai-sdk-ui/overview), another powerful component of the AI SDK, to streamline the process of building chat, completion, and assistant interfaces with popular frameworks like Next.js, Nuxt, and SvelteKit. AI SDK UI provides robust abstractions that simplify the complex tasks of managing chat streams and UI updates on the frontend, enabling you to develop dynamic AI-driven interfaces more efficiently. With four main hooks — [`useChat`](/docs/reference/ai-sdk-ui/use-chat), [`useCompletion`](/docs/reference/ai-sdk-ui/use-completion), and [`useObject`](/docs/reference/ai-sdk-ui/use-object) — you can incorporate real-time chat capabilities, text completions, streamed JSON, and interactive assistant features into your app. Let's explore building a chatbot with [Next.js](https://nextjs.org), the AI SDK, and OpenAI o1: ```tsx filename="app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; // Allow responses up to 5 minutes export const maxDuration = 300; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('o1-mini'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } ``` ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; export default function Page() { const { messages, input, handleInputChange, handleSubmit, error } = useChat(); return ( <> {messages.map(message => ( <div key={message.id}> {message.role === 'user' ? 'User: ' : 'AI: '} {message.content} </div> ))} <form onSubmit={handleSubmit}> <input name="prompt" value={input} onChange={handleInputChange} /> <button type="submit">Submit</button> </form> </> ); } ``` The useChat hook on your root page (`app/page.tsx`) will make a request to your AI provider endpoint (`app/api/chat/route.ts`) whenever the user submits a message. The messages are then displayed in the chat UI. ## Get Started Ready to get started? Here's how you can dive in: 1. Explore the documentation at [ai-sdk.dev/docs](/docs) to understand the full capabilities of the AI SDK. 1. Check out our support for the o1 series of reasoning models in the [OpenAI Provider](/providers/ai-sdk-providers/openai#reasoning-models). 1. Check out practical examples at [ai-sdk.dev/examples](/examples) to see the SDK in action and get inspired for your own projects. 1. Dive deeper with advanced guides on topics like Retrieval-Augmented Generation (RAG) and multi-modal chat at [ai-sdk.dev/docs/guides](/docs/guides). 1. Check out ready-to-deploy AI templates at [vercel.com/templates?type=ai](https://vercel.com/templates?type=ai). --- File: /ai/content/cookbook/00-guides/24-o3.mdx --- --- title: Get started with OpenAI o3-mini description: Get started with OpenAI o3-mini using the AI SDK. tags: ['getting-started', 'reasoning'] --- # Get started with OpenAI o3-mini With the [release of OpenAI's o3-mini model](https://openai.com/index/openai-o3-mini/), there has never been a better time to start building AI applications, particularly those that require complex STEM reasoning capabilities. The [AI SDK](/) is a powerful TypeScript toolkit for building AI applications with large language models (LLMs) like OpenAI o3-mini alongside popular frameworks like React, Next.js, Vue, Svelte, Node.js, and more. ## OpenAI o3-mini OpenAI recently released a new AI model optimized for STEM reasoning that excels in science, math, and coding tasks. o3-mini matches o1's performance in these domains while delivering faster responses and lower costs. The model supports tool calling, structured outputs, and system messages, making it a great option for a wide range of applications. o3-mini offers three reasoning effort levels: 1. [**Low**]: Optimized for speed while maintaining solid reasoning capabilities 2. [**Medium**]: Balanced approach matching o1's performance levels 3. [**High**]: Enhanced reasoning power exceeding o1 in many STEM domains | Model | Streaming | Tool Calling | Structured Output | Reasoning Effort | Image Input | | ------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | o3-mini | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | ### Benchmarks OpenAI o3-mini demonstrates impressive performance across technical domains: - 87.3% accuracy on AIME competition math questions - 79.7% accuracy on PhD-level science questions (GPQA Diamond) - 2130 Elo rating on competitive programming (Codeforces) - 49.3% accuracy on verified software engineering tasks (SWE-bench) <Note>These benchmark results are using high reasoning effort setting.</Note> [Source](https://openai.com/index/openai-o3-mini/) ### Prompt Engineering for o3-mini The o3-mini model performs best with straightforward prompts. Some prompt engineering techniques, like few-shot prompting or instructing the model to "think step by step," may not enhance performance and can sometimes hinder it. Here are some best practices: 1. Keep prompts simple and direct: The model excels at understanding and responding to brief, clear instructions without the need for extensive guidance. 2. Avoid chain-of-thought prompts: Since the model performs reasoning internally, prompting it to "think step by step" or "explain your reasoning" is unnecessary. 3. Use delimiters for clarity: Use delimiters like triple quotation marks, XML tags, or section titles to clearly indicate distinct parts of the input. ## Getting Started with the AI SDK The AI SDK is the TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js, and more. Integrating LLMs into applications is complicated and heavily dependent on the specific model provider you use. The AI SDK abstracts away the differences between model providers, eliminates boilerplate code for building chatbots, and allows you to go beyond text output to generate rich, interactive components. At the center of the AI SDK is [AI SDK Core](/docs/ai-sdk-core/overview), which provides a unified API to call any LLM. The code snippet below is all you need to call OpenAI o3-mini with the AI SDK: ```ts import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; const { text } = await generateText({ model: openai('o3-mini'), prompt: 'Explain the concept of quantum entanglement.', }); ``` <Note> To use o3-mini, you must be using @ai-sdk/openai version 1.1.9 or greater. </Note> <Note> System messages are automatically converted to OpenAI developer messages. </Note> ### Refining Reasoning Effort You can control the amount of reasoning effort expended by o3-mini through the `reasoningEffort` parameter. This parameter can be set to `low`, `medium`, or `high` to adjust how much time and computation the model spends on internal reasoning before producing a response. ```ts highlight="9" import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; // Reduce reasoning effort for faster responses const { text } = await generateText({ model: openai('o3-mini'), prompt: 'Explain quantum entanglement briefly.', providerOptions: { openai: { reasoningEffort: 'low' }, }, }); ``` ### Generating Structured Data While text generation can be useful, you might want to generate structured JSON data. For example, you might want to extract information from text, classify data, or generate synthetic data. AI SDK Core provides two functions ([`generateObject`](/docs/reference/ai-sdk-core/generate-object) and [`streamObject`](/docs/reference/ai-sdk-core/stream-object)) to generate structured data, allowing you to constrain model outputs to a specific schema. ```ts import { generateObject } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const { object } = await generateObject({ model: openai('o3-mini'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.object({ name: z.string(), amount: z.string() })), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); ``` This code snippet will generate a type-safe recipe that conforms to the specified zod schema. ### Using Tools with the AI SDK o3-mini supports tool calling out of the box, allowing it to interact with external systems and perform discrete tasks. Here's an example of using tool calling with the AI SDK: ```ts import { generateText, tool } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const { text } = await generateText({ model: openai('o3-mini'), prompt: 'What is the weather like today in San Francisco?', tools: { getWeather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, }); ``` In this example, the `getWeather` tool allows the model to fetch real-time weather data (simulated for simplicity), enhancing its ability to provide accurate and up-to-date information. ### Building Interactive Interfaces AI SDK Core can be paired with [AI SDK UI](/docs/ai-sdk-ui/overview), another powerful component of the AI SDK, to streamline the process of building chat, completion, and assistant interfaces with popular frameworks like Next.js, Nuxt, and SvelteKit. AI SDK UI provides robust abstractions that simplify the complex tasks of managing chat streams and UI updates on the frontend, enabling you to develop dynamic AI-driven interfaces more efficiently. With four main hooks — [`useChat`](/docs/reference/ai-sdk-ui/use-chat), [`useCompletion`](/docs/reference/ai-sdk-ui/use-completion), and [`useObject`](/docs/reference/ai-sdk-ui/use-object) — you can incorporate real-time chat capabilities, text completions, streamed JSON, and interactive assistant features into your app. Let's explore building a chatbot with [Next.js](https://nextjs.org), the AI SDK, and OpenAI o3-mini: In a new Next.js application, first install the AI SDK and the DeepSeek provider: <Snippet text="pnpm install ai @ai-sdk/openai @ai-sdk/react" /> Then, create a route handler for the chat endpoint: ```tsx filename="app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; // Allow responses up to 5 minutes export const maxDuration = 300; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('o3-mini'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } ``` Finally, update the root page (`app/page.tsx`) to use the `useChat` hook: ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; export default function Page() { const { messages, input, handleInputChange, handleSubmit, error } = useChat(); return ( <> {messages.map(message => ( <div key={message.id}> {message.role === 'user' ? 'User: ' : 'AI: '} {message.content} </div> ))} <form onSubmit={handleSubmit}> <input name="prompt" value={input} onChange={handleInputChange} /> <button type="submit">Submit</button> </form> </> ); } ``` The useChat hook on your root page (`app/page.tsx`) will make a request to your AI provider endpoint (`app/api/chat/route.ts`) whenever the user submits a message. The messages are then displayed in the chat UI. ## Get Started Ready to get started? Here's how you can dive in: 1. Explore the documentation at [ai-sdk.dev/docs](/docs) to understand the full capabilities of the AI SDK. 2. Check out our support for o3-mini in the [OpenAI Provider](/providers/ai-sdk-providers/openai#reasoning-models). 3. Check out practical examples at [ai-sdk.dev/examples](/examples) to see the SDK in action and get inspired for your own projects. 4. Dive deeper with advanced guides on topics like Retrieval-Augmented Generation (RAG) and multi-modal chat at [ai-sdk.dev/docs/guides](/docs/guides). 5. Check out ready-to-deploy AI templates at [vercel.com/templates?type=ai](https://vercel.com/templates?type=ai). --- File: /ai/content/cookbook/00-guides/25-r1.mdx --- --- title: Get started with DeepSeek R1 description: Get started with DeepSeek R1 using the AI SDK. tags: ['getting-started', 'reasoning'] --- # Get started with DeepSeek R1 With the [release of DeepSeek R1](https://api-docs.deepseek.com/news/news250528), there has never been a better time to start building AI applications, particularly those that require complex reasoning capabilities. The [AI SDK](/) is a powerful TypeScript toolkit for building AI applications with large language models (LLMs) like DeepSeek R1 alongside popular frameworks like React, Next.js, Vue, Svelte, Node.js, and more. ## DeepSeek R1 DeepSeek R1 is a series of advanced AI models designed to tackle complex reasoning tasks in science, coding, and mathematics. These models are optimized to "think before they answer," producing detailed internal chains of thought that aid in solving challenging problems. The series includes two primary variants: - **DeepSeek R1-Zero**: Trained exclusively with reinforcement learning (RL) without any supervised fine-tuning. It exhibits advanced reasoning capabilities but may struggle with readability and formatting. - **DeepSeek R1**: Combines reinforcement learning with cold-start data and supervised fine-tuning to improve both reasoning performance and the readability of outputs. ### Benchmarks DeepSeek R1 models excel in reasoning tasks, delivering competitive performance across key benchmarks: - **AIME 2024 (Pass\@1)**: 79.8% - **MATH-500 (Pass\@1)**: 97.3% - **Codeforces (Percentile)**: Top 4% (96.3%) - **GPQA Diamond (Pass\@1)**: 71.5% [Source](https://github.com/deepseek-ai/DeepSeek-R1?tab=readme-ov-file#4-evaluation-results) ### Prompt Engineering for DeepSeek R1 Models DeepSeek R1 models excel with structured and straightforward prompts. The following best practices can help achieve optimal performance: 1. **Use a structured format**: Leverage the model’s preferred output structure with `<think>` tags for reasoning and `<answer>` tags for the final result. 2. **Prefer zero-shot prompts**: Avoid few-shot prompting as it can degrade performance; instead, directly state the problem clearly. 3. **Specify output expectations**: Guide the model by defining desired formats, such as markdown for readability or XML-like tags for clarity. ## Getting Started with the AI SDK The AI SDK is the TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js, and more. Integrating LLMs into applications is complicated and heavily dependent on the specific model provider you use. The AI SDK abstracts away the differences between model providers, eliminates boilerplate code for building chatbots, and allows you to go beyond text output to generate rich, interactive components. At the center of the AI SDK is [AI SDK Core](/docs/ai-sdk-core/overview), which provides a unified API to call any LLM. The code snippet below is all you need to call DeepSeek R1 with the AI SDK: ```ts import { deepseek } from '@ai-sdk/deepseek'; import { generateText } from 'ai'; const { reasoningText, text } = await generateText({ model: deepseek('deepseek-reasoner'), prompt: 'Explain quantum entanglement.', }); ``` The unified interface also means that you can easily switch between providers by changing just two lines of code. For example, to use DeepSeek R1 via Fireworks: ```ts import { fireworks } from '@ai-sdk/fireworks'; import { generateText, wrapLanguageModel, extractReasoningMiddleware, } from 'ai'; // middleware to extract reasoning tokens const enhancedModel = wrapLanguageModel({ model: fireworks('accounts/fireworks/models/deepseek-r1'), middleware: extractReasoningMiddleware({ tagName: 'think' }), }); const { reasoningText, text } = await generateText({ model: enhancedModel, prompt: 'Explain quantum entanglement.', }); ``` Or to use Groq's `deepseek-r1-distill-llama-70b` model: ```ts import { groq } from '@ai-sdk/groq'; import { generateText, wrapLanguageModel, extractReasoningMiddleware, } from 'ai'; // middleware to extract reasoning tokens const enhancedModel = wrapLanguageModel({ model: groq('deepseek-r1-distill-llama-70b'), middleware: extractReasoningMiddleware({ tagName: 'think' }), }); const { reasoningText, text } = await generateText({ model: enhancedModel, prompt: 'Explain quantum entanglement.', }); ``` <Note id="deepseek-r1-middleware"> The AI SDK provides a [middleware](/docs/ai-sdk-core/middleware) (`extractReasoningMiddleware`) that can be used to extract the reasoning tokens from the model's output. When using DeepSeek-R1 series models with third-party providers like Together AI, we recommend using the `startWithReasoning` option in the `extractReasoningMiddleware` function, as they tend to bypass thinking patterns. </Note> ### Model Provider Comparison You can use DeepSeek R1 with the AI SDK through various providers. Here's a comparison of the providers that support DeepSeek R1: | Provider | Model ID | Reasoning Tokens | | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------- | | [DeepSeek](/providers/ai-sdk-providers/deepseek) | [`deepseek-reasoner`](https://api-docs.deepseek.com/guides/reasoning_model) | <Check size={18} /> | | [Fireworks](/providers/ai-sdk-providers/fireworks) | [`accounts/fireworks/models/deepseek-r1`](https://fireworks.ai/models/fireworks/deepseek-r1) | Requires Middleware | | [Groq](/providers/ai-sdk-providers/groq) | [`deepseek-r1-distill-llama-70b`](https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Llama-70B) | Requires Middleware | | [Azure](/providers/ai-sdk-providers/azure) | [`DeepSeek-R1`](https://ai.azure.com/explore/models/DeepSeek-R1/version/1/registry/azureml-deepseek#code-samples) | Requires Middleware | | [Together AI](/providers/ai-sdk-providers/togetherai) | [`deepseek-ai/DeepSeek-R1`](https://www.together.ai/models/deepseek-r1) | Requires Middleware | | [FriendliAI](/providers/community-providers/friendliai) | [`deepseek-r1`](https://huggingface.co/deepseek-ai/DeepSeek-R1) | Requires Middleware | | [LangDB](/providers/community-providers/langdb) | [`deepseek/deepseek-reasoner`](https://docs.langdb.ai/guides/deepseek) | Requires Middleware | ### Building Interactive Interfaces AI SDK Core can be paired with [AI SDK UI](/docs/ai-sdk-ui/overview), another powerful component of the AI SDK, to streamline the process of building chat, completion, and assistant interfaces with popular frameworks like Next.js, Nuxt, and SvelteKit. AI SDK UI provides robust abstractions that simplify the complex tasks of managing chat streams and UI updates on the frontend, enabling you to develop dynamic AI-driven interfaces more efficiently. With four main hooks — [`useChat`](/docs/reference/ai-sdk-ui/use-chat), [`useCompletion`](/docs/reference/ai-sdk-ui/use-completion), and [`useObject`](/docs/reference/ai-sdk-ui/use-object) — you can incorporate real-time chat capabilities, text completions, streamed JSON, and interactive assistant features into your app. Let's explore building a chatbot with [Next.js](https://nextjs.org), the AI SDK, and DeepSeek R1: In a new Next.js application, first install the AI SDK and the DeepSeek provider: <Snippet text="pnpm install ai @ai-sdk/deepseek @ai-sdk/react" /> Then, create a route handler for the chat endpoint: ```tsx filename="app/api/chat/route.ts" import { deepseek } from '@ai-sdk/deepseek'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: deepseek('deepseek-reasoner'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ sendReasoning: true, }); } ``` <Note> You can forward the model's reasoning tokens to the client with `sendReasoning: true` in the `toDataStreamResponse` method. </Note> Finally, update the root page (`app/page.tsx`) to use the `useChat` hook: ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat(); const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); if (input.trim()) { sendMessage({ text: input }); setInput(''); } }; return ( <> {messages.map(message => ( <div key={message.id}> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, index) => { if (part.type === 'reasoning') { return <pre key={index}>{part.text}</pre>; } if (part.type === 'text') { return <span key={index}>{part.text}</span>; } return null; })} </div> ))} <form onSubmit={handleSubmit}> <input name="prompt" value={input} onChange={e => setInput(e.target.value)} /> <button type="submit">Submit</button> </form> </> ); } ``` <Note> You can access the model's reasoning tokens through the `parts` array on the `message` object, where reasoning parts have `type: 'reasoning'`. </Note> The useChat hook on your root page (`app/page.tsx`) will make a request to your AI provider endpoint (`app/api/chat/route.ts`) whenever the user submits a message. The messages are then displayed in the chat UI. ## Limitations While DeepSeek R1 models are powerful, they have certain limitations: - No tool-calling support: DeepSeek R1 cannot directly interact with APIs or external tools. - No object generation support: DeepSeek R1 does not support structured object generation. However, you can combine it with models that support structured object generation (like gpt-4o-mini) to generate objects. See the [structured object generation with a reasoning model recipe](/cookbook/node/generate-object-reasoning) for more information. ## Get Started Ready to dive in? Here's how you can begin: 1. Explore the documentation at [ai-sdk.dev/docs](/docs) to understand the capabilities of the AI SDK. 2. Check out practical examples at [ai-sdk.dev/examples](/examples) to see the SDK in action. 3. Dive deeper with advanced guides on topics like Retrieval-Augmented Generation (RAG) at [ai-sdk.dev/docs/guides](/docs/guides). 4. Use ready-to-deploy AI templates at [vercel.com/templates?type=ai](https://vercel.com/templates?type=ai). DeepSeek R1 opens new opportunities for reasoning-intensive AI applications. Start building today and leverage the power of advanced reasoning in your AI projects. --- File: /ai/content/cookbook/00-guides/index.mdx --- --- title: Guides description: Learn how to build AI applications with the AI SDK --- # Guides These use-case specific guides are intended to help you build real applications with the AI SDK. <IndexCards cards={[ { title: 'RAG Chatbot', description: 'Learn how to build a retrieval-augmented generation chatbot with the AI SDK.', href: '/cookbook/guides/rag-chatbot', }, { title: 'Multimodal Chatbot', description: 'Learn how to build a multimodal chatbot with the AI SDK.', href: '/cookbook/guides/multi-modal-chatbot', }, { title: 'Get started with Llama 3.1', description: 'Get started with Llama 3.1 using the AI SDK.', href: '/cookbook/guides/llama-3_1', }, { title: 'Get started with OpenAI o1', description: 'Get started with OpenAI o1 using the AI SDK.', href: '/cookbook/guides/o1', }, { title: 'Get started with Gemini 2.5', description: 'Get started with Gemini 2.5 using the AI SDK.', href: '/cookbook/guides/gemini-2-5', }, ]} /> --- File: /ai/content/cookbook/01-next/10-generate-text.mdx --- --- title: Generate Text description: Learn how to generate text using the AI SDK and Next.js. tags: ['next'] --- # Generate Text A situation may arise when you need to generate text based on a prompt. For example, you may want to generate a response to a question or summarize a body of text. The `generateText` function can be used to generate text based on the input prompt. <Browser> <TextGeneration /> </Browser> ## Client Let's create a simple React component that will make a POST request to the `/api/completion` endpoint when a button is clicked. The endpoint will generate text based on the input prompt. ```tsx filename="app/page.tsx" 'use client'; import { useState } from 'react'; export default function Page() { const [generation, setGeneration] = useState(''); const [isLoading, setIsLoading] = useState(false); return ( <div> <div onClick={async () => { setIsLoading(true); await fetch('/api/completion', { method: 'POST', body: JSON.stringify({ prompt: 'Why is the sky blue?', }), }).then(response => { response.json().then(json => { setGeneration(json.text); setIsLoading(false); }); }); }} > Generate </div> {isLoading ? 'Loading...' : generation} </div> ); } ``` ## Server Let's create a route handler for `/api/completion` that will generate text based on the input prompt. The route will call the `generateText` function from the `ai` module, which will then generate text based on the input prompt and return it. ```typescript filename='app/api/completion/route.ts' import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; export async function POST(req: Request) { const { prompt }: { prompt: string } = await req.json(); const { text } = await generateText({ model: openai('gpt-4o'), system: 'You are a helpful assistant.', prompt, }); return Response.json({ text }); } ``` --- <GithubLink link="https://github.com/vercel/ai/blob/main/examples/next-openai-pages/pages/basics/generate-text/index.tsx" /> --- File: /ai/content/cookbook/01-next/11-generate-text-with-chat-prompt.mdx --- --- title: Generate Text with Chat Prompt description: Learn how to generate text with chat prompt using the AI SDK and Next.js tags: ['next', 'streaming', 'chat'] --- # Generate Text with Chat Prompt Previously, you were able to generate text and objects using either a single message prompt, a system prompt, or a combination of both of them. However, there may be times when you want to generate text based on a series of messages. A chat completion allows you to generate text based on a series of messages. This series of messages can be any series of interactions between any number of systems, but the most popular and relatable use case has been a series of messages that represent a conversation between a user and a model. <Browser> <ChatGeneration history={[ { role: 'User', content: 'How is it going?' }, { role: 'Assistant', content: 'All good, how may I help you?' }, ]} inputMessage={{ role: 'User', content: 'Why is the sky blue?' }} outputMessage={{ role: 'Assistant', content: 'The sky is blue because of rayleigh scattering.', }} /> </Browser> ## Client Let's start by creating a simple chat interface with an input field that sends the user's message and displays the conversation history. You will call the `/api/chat` endpoint to generate the assistant's response. ```tsx filename='app/page.tsx' 'use client'; import type { ModelMessage } from 'ai'; import { useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const [messages, setMessages] = useState<ModelMessage[]>([]); return ( <div> <input value={input} onChange={event => { setInput(event.target.value); }} onKeyDown={async event => { if (event.key === 'Enter') { setMessages(currentMessages => [ ...currentMessages, { role: 'user', content: input }, ]); const response = await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ messages: [...messages, { role: 'user', content: input }], }), }); const { messages: newMessages } = await response.json(); setMessages(currentMessages => [ ...currentMessages, ...newMessages, ]); } }} /> {messages.map((message, index) => ( <div key={`${message.role}-${index}`}> {typeof message.content === 'string' ? message.content : message.content .filter(part => part.type === 'text') .map((part, partIndex) => ( <div key={partIndex}>{part.text}</div> ))} </div> ))} </div> ); } ``` ## Server Next, let's create the `/api/chat` endpoint that generates the assistant's response based on the conversation history. ```typescript filename='app/api/chat/route.ts' import { openai } from '@ai-sdk/openai'; import { generateText, type ModelMessage } from 'ai'; export async function POST(req: Request) { const { messages }: { messages: ModelMessage[] } = await req.json(); const { response } = await generateText({ model: openai('gpt-4o'), system: 'You are a helpful assistant.', messages, }); return Response.json({ messages: response.messages }); } ``` --- <GithubLink link="https://github.com/vercel/ai/blob/main/examples/next-openai-pages/pages/chat/generate-chat/index.tsx" /> --- File: /ai/content/cookbook/01-next/12-generate-image-with-chat-prompt.mdx --- --- title: Generate Image with Chat Prompt description: Learn how to generate an image with a chat prompt using the AI SDK and Next.js tags: ['next', 'streaming', 'chat', 'image generation', 'tools'] --- # Generate Image with Chat Prompt When building a chatbot, you may want to allow the user to generate an image. This can be done by creating a tool that generates an image using the [`experimental_generateImage`](/docs/reference/ai-sdk-core/generate-image#generateimage) function from the AI SDK. ## Server Let's create an endpoint at `/api/chat` that generates the assistant's response based on the conversation history. You will also define a tool called `generateImage` that will generate an image based on the assistant's response. ```typescript filename='tools/get-weather.ts' import { openai } from '@ai-sdk/openai'; import { experimental_generateImage, tool } from 'ai'; import z from 'zod'; export const generateImage = tool({ description: 'Generate an image', inputSchema: z.object({ prompt: z.string().describe('The prompt to generate the image from'), }), execute: async ({ prompt }) => { const { image } = await experimental_generateImage({ model: openai.imageModel('dall-e-3'), prompt, }); // in production, save this image to blob storage and return a URL return { image: image.base64, prompt }; }, }); ``` ```typescript filename='app/api/chat/route.ts' import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, type InferUITools, stepCountIs, streamText, type UIMessage, } from 'ai'; import { generateImage } from '@/tools/get-weather'; const tools = { generateImage, }; export type ChatTools = InferUITools<typeof tools>; export async function POST(request: Request) { const { messages }: { messages: UIMessage[] } = await request.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), tools, }); return result.toUIMessageStreamResponse(); } ``` <Note> In production, you should save the generated image to a blob storage and return a URL instead of the base64 image data. If you don't, the base64 image data will be sent to the model which may cause the generation to fail. </Note> ## Client Let's create a simple chat interface with `useChat`. You will call the `/api/chat` endpoint to generate the assistant's response. If the assistant's response contains a `generateImage` tool invocation, you will display the tool result (the image in base64 format and the prompt) using the Next `Image` component. ```tsx filename='app/page.tsx' 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, type UIMessage } from 'ai'; import Image from 'next/image'; import { type FormEvent, useState } from 'react'; import type { ChatTools } from './api/chat/route'; type ChatMessage = UIMessage<never, never, ChatTools>; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat<ChatMessage>({ transport: new DefaultChatTransport({ api: '/api/chat', }), }); const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { setInput(event.target.value); }; const handleSubmit = async (event: FormEvent<HTMLFormElement>) => { event.preventDefault(); sendMessage({ parts: [{ type: 'text', text: input }], }); setInput(''); }; return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <div className="space-y-4"> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> <div key={message.id}> <div className="font-bold">{message.role}</div> {message.parts.map((part, partIndex) => { const { type } = part; if (type === 'text') { return ( <div key={`${message.id}-part-${partIndex}`}> {part.text} </div> ); } if (type === 'tool-generateImage') { const { state, toolCallId } = part; if (state === 'input-available') { return ( <div key={`${message.id}-part-${partIndex}`}> Generating image... </div> ); } if (state === 'output-available') { const { input, output } = part; return ( <Image key={toolCallId} src={`data:image/png;base64,${output.image}`} alt={input.prompt} height={400} width={400} /> ); } } })} </div> </div> ))} </div> <form onSubmit={handleSubmit}> <input className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" value={input} placeholder="Say something..." onChange={handleInputChange} /> </form> </div> ); } ``` --- File: /ai/content/cookbook/01-next/122-caching-middleware.mdx --- --- title: Caching Middleware description: Learn how to create a caching middleware with Next.js and KV. tags: ['next', 'streaming', 'caching', 'middleware'] --- # Caching Middleware <Note type="warning">This example is not yet updated to v5.</Note> Let's create a simple chat interface that uses [`LanguageModelMiddleware`](/docs/ai-sdk-core/middleware) to cache the assistant's responses in fast KV storage. ## Client Let's create a simple chat interface that allows users to send messages to the assistant and receive responses. You will integrate the `useChat` hook from `@ai-sdk/react` to stream responses. ```tsx filename='app/page.tsx' 'use client'; import { useChat } from '@ai-sdk/react'; export default function Chat() { const { messages, input, handleInputChange, handleSubmit, error } = useChat(); if (error) return <div>{error.message}</div>; return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <div className="space-y-4"> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> <div> <div className="font-bold">{m.role}</div> {m.toolInvocations ? ( <pre>{JSON.stringify(m.toolInvocations, null, 2)}</pre> ) : ( <p>{m.content}</p> )} </div> </div> ))} </div> <form onSubmit={handleSubmit}> <input className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" value={input} placeholder="Say something..." onChange={handleInputChange} /> </form> </div> ); } ``` ## Middleware Next, you will create a `LanguageModelMiddleware` that caches the assistant's responses in KV storage. `LanguageModelMiddleware` has two methods: `wrapGenerate` and `wrapStream`. `wrapGenerate` is called when using [`generateText`](/docs/reference/ai-sdk-core/generate-text) and [`generateObject`](/docs/reference/ai-sdk-core/generate-object), while `wrapStream` is called when using [`streamText`](/docs/reference/ai-sdk-core/stream-text) and [`streamObject`](/docs/reference/ai-sdk-core/stream-object). For `wrapGenerate`, you can cache the response directly. Instead, for `wrapStream`, you cache an array of the stream parts, which can then be used with [`simulateReadableStream`](/docs/reference/ai-sdk-core/simulate-readable-stream) function to create a simulated `ReadableStream` that returns the cached response. In this way, the cached response is returned chunk-by-chunk as if it were being generated by the model. You can control the initial delay and delay between chunks by adjusting the `initialDelayInMs` and `chunkDelayInMs` parameters of `simulateReadableStream`. ```tsx filename='ai/middleware.ts' import { Redis } from '@upstash/redis'; import { type LanguageModelV1, type LanguageModelV2Middleware, type LanguageModelV1StreamPart, simulateReadableStream, } from 'ai'; const redis = new Redis({ url: process.env.KV_URL, token: process.env.KV_TOKEN, }); export const cacheMiddleware: LanguageModelV2Middleware = { wrapGenerate: async ({ doGenerate, params }) => { const cacheKey = JSON.stringify(params); const cached = (await redis.get(cacheKey)) as Awaited< ReturnType<LanguageModelV1['doGenerate']> > | null; if (cached !== null) { return { ...cached, response: { ...cached.response, timestamp: cached?.response?.timestamp ? new Date(cached?.response?.timestamp) : undefined, }, }; } const result = await doGenerate(); redis.set(cacheKey, result); return result; }, wrapStream: async ({ doStream, params }) => { const cacheKey = JSON.stringify(params); // Check if the result is in the cache const cached = await redis.get(cacheKey); // If cached, return a simulated ReadableStream that yields the cached result if (cached !== null) { // 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, }), }; } // 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 redis.set(cacheKey, fullResponse); }, }); return { stream: stream.pipeThrough(transformStream), ...rest, }; }, }; ``` <Note> This example uses `@upstash/redis` to store and retrieve the assistant's responses but you can use any KV storage provider you would like. </Note> ## Server Finally, you will create an API route for `api/chat` to handle the assistant's messages and responses. You can use your cache middleware by wrapping the model with `wrapLanguageModel` and passing the middleware as an argument. ```tsx filename='app/api/chat/route.ts' import { cacheMiddleware } from '@/ai/middleware'; import { openai } from '@ai-sdk/openai'; import { wrapLanguageModel, streamText, tool } from 'ai'; import { z } from 'zod'; const wrappedModel = wrapLanguageModel({ model: openai('gpt-4o-mini'), middleware: cacheMiddleware, }); export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: wrappedModel, messages, tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, }); return result.toUIMessageStreamResponse(); } ``` --- File: /ai/content/cookbook/01-next/20-stream-text.mdx --- --- title: Stream Text description: Learn how to stream text using the AI SDK and Next.js tags: ['next', 'streaming'] --- # Stream Text Text generation can sometimes take a long time to complete, especially when you're generating a couple of paragraphs. In such cases, it is useful to stream the text generation process to the client in real-time. This allows the client to display the generated text as it is being generated, rather than have users wait for it to complete before displaying the result. <Browser> <TextGeneration stream /> </Browser> ## Client Let's create a simple React component that imports the `useCompletion` hook from the `@ai-sdk/react` module. The `useCompletion` hook will call the `/api/completion` endpoint when a button is clicked. The endpoint will generate text based on the input prompt and stream it to the client. ```tsx filename="app/page.tsx" 'use client'; import { useCompletion } from '@ai-sdk/react'; export default function Page() { const { completion, complete } = useCompletion({ api: '/api/completion', }); return ( <div> <div onClick={async () => { await complete('Why is the sky blue?'); }} > Generate </div> {completion} </div> ); } ``` ## Server Let's create a route handler for `/api/completion` that will generate text based on the input prompt. 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. ```typescript filename='app/api/completion/route.ts' import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; export async function POST(req: Request) { const { prompt }: { prompt: string } = await req.json(); const result = streamText({ model: openai('gpt-4'), system: 'You are a helpful assistant.', prompt, }); return result.toUIMessageStreamResponse(); } ``` --- <GithubLink link="https://github.com/vercel/ai/blob/main/examples/next-openai-pages/pages/basics/stream-text/index.tsx" /> --- File: /ai/content/cookbook/01-next/21-stream-text-with-chat-prompt.mdx --- --- title: Stream Text with Chat Prompt description: Learn how to generate text using the AI SDK and Next.js tags: ['next', 'streaming', 'chat'] --- # Stream Text with Chat Prompt Chat completion can sometimes take a long time to finish, especially when the response is big. In such cases, it is useful to stream the chat completion to the client in real-time. This allows the client to display the new message as it is being generated by the model, rather than have users wait for it to finish. <Browser> <ChatGeneration stream history={[ { role: 'User', content: 'How is it going?' }, { role: 'Assistant', content: 'All good, how may I help you?' }, ]} inputMessage={{ role: 'User', content: 'Why is the sky blue?' }} outputMessage={{ role: 'Assistant', content: 'The sky is blue because of rayleigh scattering.', }} /> </Browser> ## Client Let's create a React component that imports the `useChat` hook from the `@ai-sdk/react` module. The `useChat` hook will call the `/api/chat` endpoint when the user sends a message. The endpoint will generate the assistant's response based on the conversation history and stream it to the client. ```tsx filename='app/page.tsx' 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', }), }); return ( <div> <input value={input} onChange={event => { setInput(event.target.value); }} onKeyDown={async event => { if (event.key === 'Enter') { sendMessage({ parts: [{ type: 'text', text: input }], }); } }} /> {messages.map((message, index) => ( <div key={index}> {message.parts.map(part => { if (part.type === 'text') { return <div key={`${message.id}-text`}>{part.text}</div>; } })} </div> ))} </div> ); } ``` ## Server Next, let's create the `/api/chat` endpoint that generates the assistant's response based on the conversation history. ```typescript filename='app/api/chat/route.ts' import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, type UIMessage } from 'ai'; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), system: 'You are a helpful assistant.', messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } ``` --- <GithubLink link="https://github.com/vercel/ai/blob/main/examples/next-openai-pages/pages/chat/stream-chat/index.tsx" /> --- File: /ai/content/cookbook/01-next/22-stream-text-with-image-prompt.mdx --- --- title: Stream Text with Image Prompt description: Learn how to stream text with an image prompt using the AI SDK and Next.js tags: ['next', 'streaming', 'multimodal'] --- # Stream Text with Image Prompt Vision models such as GPT-4o can process both text and images. In this example, we will show you how to send an image URL along with the user's message to the model with `useChat`. ## Using Image URLs ### Server The server route uses `convertToModelMessages` to handle the conversion from `UIMessage`s to model messages, which automatically handles multimodal content including images. ```tsx filename='app/api/chat/route.ts' highlight="8,9,23" import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; export const maxDuration = 60; export async function POST(req: Request) { const { messages } = await req.json(); // Call the language model const result = streamText({ model: openai('gpt-4.1'), messages: convertToModelMessages(messages), }); // Respond with the stream return result.toUIMessageStreamResponse(); } ``` ### Client On the client side, we use the new `useChat` hook and send multimodal messages using the `parts` array. ```typescript filename='app/page.tsx' 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export default function Chat() { const [input, setInput] = useState(''); const [imageUrl, setImageUrl] = useState( 'https://science.nasa.gov/wp-content/uploads/2023/09/web-first-images-release.png', ); const { messages, sendMessage } = useChat(); const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); sendMessage({ role: 'user', parts: [ // check if imageUrl is defined, if so, add it to the message ...(imageUrl.trim().length > 0 ? [ { type: 'file' as const, mediaType: 'image/png', url: imageUrl, }, ] : []), { type: 'text' as const, text: input }, ], }); setInput(''); setImageUrl(''); }; return ( <div> <div> {messages.map(m => ( <div key={m.id}> <span>{m.role === 'user' ? 'User: ' : 'AI: '}</span> <div> {m.parts.map((part, i) => { switch (part.type) { case 'text': return part.text; case 'file': return ( <img key={(part.filename || 'image') + i} src={part.url} alt={part.filename ?? 'image'} /> ); default: return null; } })} </div> </div> ))} </div> <form onSubmit={handleSubmit}> <div> <label htmlFor="image-url">Image URL:</label> <input id="image-url" value={imageUrl} placeholder="Enter image URL..." onChange={e => setImageUrl(e.currentTarget.value)} /> </div> <div> <label htmlFor="image-description">Prompt:</label> <input id="image-description" value={input} placeholder="What does the image show..." onChange={e => setInput(e.currentTarget.value)} /> </div> <button type="submit">Send Message</button> </form> </div> ); } ``` --- File: /ai/content/cookbook/01-next/23-chat-with-pdf.mdx --- --- title: Chat with PDFs description: Learn how to build a chatbot that can understand PDFs using the AI SDK and Next.js tags: ['next', 'pdf', 'multimodal'] --- # Chat with PDFs Some language models like Anthropic's Claude Sonnet 3.5 and Google's Gemini 2.0 can understand PDFs and respond to questions about their contents. In this example, we'll show you how to build a chat interface that accepts PDF uploads. <Note> This example requires a provider that supports PDFs, such as Anthropic's 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. </Note> ## Implementation ### Server Create a route handler that will use Anthropic's Claude model to process messages and PDFs: ```tsx filename="app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, type 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(); } ``` ### Client Create a chat interface that allows uploading PDFs alongside messages: ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useRef, useState } from 'react'; async function convertFilesToDataURLs( files: FileList, ): Promise< { type: 'file'; filename: string; mediaType: string; url: string }[] > { return Promise.all( Array.from(files).map( file => new Promise<{ type: 'file'; filename: string; mediaType: string; url: string; }>((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { resolve({ type: 'file', filename: file.name, mediaType: file.type, url: reader.result as string, // Data URL }); }; reader.onerror = reject; reader.readAsDataURL(file); }), ), ); } export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', }), }); const [files, setFiles] = useState<FileList | undefined>(undefined); const fileInputRef = useRef<HTMLInputElement>(null); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map(part => { if (part.type === 'text') { return <div key={`${message.id}-text`}>{part.text}</div>; } })} <div></div> </div> ))} <form className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl space-y-2" onSubmit={async event => { event.preventDefault(); const fileParts = files && files.length > 0 ? await convertFilesToDataURLs(files) : []; sendMessage({ role: 'user', parts: [{ type: 'text', text: input }, ...fileParts], }); setFiles(undefined); setInput(''); if (fileInputRef.current) { fileInputRef.current.value = ''; } }} > <input type="file" onChange={event => { if (event.target.files) { setFiles(event.target.files); } }} multiple ref={fileInputRef} /> <input className="w-full p-2" value={input} placeholder="Say something..." onChange={event => { setInput(event.target.value); }} /> </form> </div> ); } ``` The code uses the `useChat` hook which handles the file upload and message streaming. The `experimental_attachments` option allows you to send files alongside messages. Make sure to set up your environment variables with your Anthropic API key: ```env filename=".env.local" ANTHROPIC_API_KEY=xxxxxxxxx ``` Now you can upload PDFs and ask questions about their contents. The LLM will analyze the PDF and provide relevant responses based on the document's content. --- File: /ai/content/cookbook/01-next/24-stream-text-multistep.mdx --- --- title: streamText Multi-Step Cookbook description: Learn how to create several streamText steps with different settings tags: ['next', 'streaming'] --- # streamText Multi-Step Agent You may want to have different steps in your stream where each step has different settings, e.g. models, tools, or system prompts. With `createUIMessageStream` and `sendFinish` / `sendStart` options when merging into the `UIMessageStream`, you can control when the finish and start events are sent to the client, allowing you to have different steps in a single assistant UI message. ## Server ```typescript filename='app/api/chat/route.ts' import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse, streamText, tool, } from 'ai'; import { z } from 'zod'; export async function POST(req: Request) { const { messages } = await req.json(); const stream = createUIMessageStream({ execute: async ({ writer }) => { // step 1 example: forced tool call const result1 = streamText({ model: openai('gpt-4o-mini'), system: 'Extract the user goal from the conversation.', messages, toolChoice: 'required', // force the model to call a tool tools: { extractGoal: tool({ inputSchema: z.object({ goal: z.string() }), execute: async ({ goal }) => goal, // no-op extract tool }), }, }); // forward the initial result to the client without the finish event: writer.merge(result1.toUIMessageStream({ sendFinish: false })); // note: you can use any programming construct here, e.g. if-else, loops, etc. // workflow programming is normal programming with this approach. // example: continue stream with forced tool call from previous step const result2 = streamText({ // different system prompt, different model, no tools: model: openai('gpt-4o'), 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: [ ...convertToModelMessages(messages), ...(await result1.response).messages, ], }); // forward the 2nd result to the client (incl. the finish event): writer.merge(result2.toUIMessageStream({ sendStart: false })); }, }); return createUIMessageStreamResponse({ stream }); } ``` ## Client ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat(); return ( <div> {messages?.map(message => ( <div key={message.id}> <strong>{`${message.role}: `}</strong> {message.parts.map((part, index) => { switch (part.type) { case 'text': return <span key={index}>{part.text}</span>; case 'tool-extractGoal': { return <pre key={index}>{JSON.stringify(part, null, 2)}</pre>; } } })} </div> ))} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input value={input} onChange={e => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` --- File: /ai/content/cookbook/01-next/25-markdown-chatbot-with-memoization.mdx --- --- title: Markdown Chatbot with Memoization description: Build a chatbot that renders and memoizes Markdown responses with Next.js and the AI SDK. tags: ['next', 'streaming', 'chatbot', 'markdown'] --- # Markdown Chatbot with Memoization When building a chatbot with Next.js and the AI SDK, you'll likely want to render the model's responses in Markdown format using a library like `react-markdown`. However, this can have negative performance implications as the Markdown is re-rendered on each new token received from the streaming response. As conversations get longer and more complex, this performance impact becomes exponentially worse since the entire conversation history is re-rendered with each new token. This recipe uses memoization - a performance optimization technique where the results of expensive function calls are cached and reused to avoid unnecessary re-computation. In this case, parsed Markdown blocks are memoized to prevent them from being re-parsed and re-rendered on each token update, which means that once a block is fully parsed, it's cached and reused rather than being regenerated. This approach significantly improves rendering performance for long conversations by eliminating redundant parsing and rendering operations. ## Installation First, install the required dependencies for Markdown rendering and parsing: ```bash npm install react-markdown marked ``` ## Server On the server, you use a simple route handler that streams the response from the language model. ```tsx filename='app/api/chat/route.ts' import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, type UIMessage } from 'ai'; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ system: 'You are a helpful assistant. Respond to the user in Markdown format.', model: openai('gpt-4o'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } ``` ## Memoized Markdown Component Next, create a memoized markdown component that will take in raw Markdown text into blocks and only updates when the content actually changes. This component splits Markdown content into blocks using the `marked` library to identify discrete Markdown elements, then uses React's memoization features to optimize re-rendering by only updating blocks that have actually changed. ```tsx filename='components/memoized-markdown.tsx' import { marked } from 'marked'; import { memo, useMemo } from 'react'; import ReactMarkdown from 'react-markdown'; function parseMarkdownIntoBlocks(markdown: string): string[] { const tokens = marked.lexer(markdown); return tokens.map(token => token.raw); } const MemoizedMarkdownBlock = memo( ({ content }: { content: string }) => { return <ReactMarkdown>{content}</ReactMarkdown>; }, (prevProps, nextProps) => { if (prevProps.content !== nextProps.content) return false; return true; }, ); MemoizedMarkdownBlock.displayName = 'MemoizedMarkdownBlock'; export const MemoizedMarkdown = memo( ({ content, id }: { content: string; id: string }) => { const blocks = useMemo(() => parseMarkdownIntoBlocks(content), [content]); return blocks.map((block, index) => ( <MemoizedMarkdownBlock content={block} key={`${id}-block_${index}`} /> )); }, ); MemoizedMarkdown.displayName = 'MemoizedMarkdown'; ``` ## Client Finally, on the client, use the `useChat` hook to manage the chat state and render the chat interface. You can use the `MemoizedMarkdown` component to render the message contents in Markdown format without compromising on performance. Additionally, you can render the form in its own component so as to not trigger unnecessary re-renders of the chat messages. You can also use the `experimental_throttle` option that will throttle data updates to a specified interval, helping to manage rendering performance. ```typescript filename='app/page.tsx' "use client"; import { Chat, useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; import { useState } from "react"; import { MemoizedMarkdown } from "@/components/memoized-markdown"; const chat = new Chat({ transport: new DefaultChatTransport({ api: "/api/chat", }), }); export default function Page() { const { messages } = useChat({ chat, experimental_throttle: 50 }); return ( <div className="flex flex-col w-full max-w-xl py-24 mx-auto stretch"> <div className="space-y-8 mb-4"> {messages.map((message) => ( <div key={message.id}> <div className="font-bold mb-2"> {message.role === "user" ? "You" : "Assistant"} </div> <div className="prose space-y-2"> {message.parts.map((part) => { if (part.type === "text") { return ( <MemoizedMarkdown key={`${message.id}-text`} id={message.id} content={part.text} /> ); } })} </div> </div> ))} </div> <MessageInput /> </div> ); } const MessageInput = () => { const [input, setInput] = useState(""); const { sendMessage } = useChat({ chat }); return ( <form onSubmit={(event) => { event.preventDefault(); sendMessage({ text: input, }); setInput(""); }} > <input className="fixed bottom-0 w-full max-w-xl p-2 mb-8 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl" placeholder="Say something..." value={input} onChange={(event) => { setInput(event.target.value); }} /> </form> ); }; ``` <Note> The chat state is shared between both components by using the same `Chat` instance. This allows you to split the form and chat messages into separate components while maintaining synchronized state. </Note> --- File: /ai/content/cookbook/01-next/30-generate-object.mdx --- --- title: Generate Object description: Learn how to generate object using the AI SDK and Next.js tags: ['next', 'structured data'] --- # Generate Object Earlier functions like `generateText` and `streamText` gave us the ability to generate unstructured text. However, if you want to generate structured data like JSON, you can provide a schema that describes the structure of your desired object to the `generateObject` function. The function requires you to provide a schema using [zod](https://zod.dev), a library for defining schemas for JavaScript objects. By using zod, you can also use it to validate the generated object and ensure that it conforms to the specified structure. <Browser> <ObjectGeneration object={{ notifications: [ { name: 'Jamie Roberts', message: "Hey! How's the study grind going? Need a coffee boost?", minutesAgo: 15, }, { name: 'Prof. Morgan', message: 'Reminder: Your term paper is due promptly at 8 AM tomorrow. Please ensure it meets the submission guidelines outlined.', minutesAgo: 46, }, { name: 'Alex Chen', message: "Dude, urgent! Borrow your notes for tomorrow's exam? I swear mine got eaten by my dog!", minutesAgo: 30, }, ], }} /> </Browser> ## Client Let's create a simple React component that will make a POST request to the `/api/completion` endpoint when a button is clicked. The endpoint will return the generated object based on the input prompt and we'll display it. ```tsx filename='app/page.tsx' 'use client'; import { useState } from 'react'; export default function Page() { const [generation, setGeneration] = useState(); const [isLoading, setIsLoading] = useState(false); return ( <div> <div onClick={async () => { setIsLoading(true); await fetch('/api/completion', { method: 'POST', body: JSON.stringify({ prompt: 'Messages during finals week.', }), }).then(response => { response.json().then(json => { setGeneration(json.notifications); setIsLoading(false); }); }); }} > Generate </div> {isLoading ? ( 'Loading...' ) : ( <pre>{JSON.stringify(generation, null, 2)}</pre> )} </div> ); } ``` ## Server Let's create a route handler for `/api/completion` that will generate an object based on the input prompt. The route will call the `generateObject` function from the `ai` module, which will then generate an object based on the input prompt and return it. ```typescript filename='app/api/completion/route.ts' import { generateObject } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; export async function POST(req: Request) { const { prompt }: { prompt: string } = await req.json(); const result = await generateObject({ model: openai('gpt-4o'), system: 'You generate three notifications for a messages app.', prompt, schema: z.object({ notifications: z.array( z.object({ name: z.string().describe('Name of a fictional person.'), message: z.string().describe('Do not use emojis or links.'), minutesAgo: z.number(), }), ), }), }); return result.toJsonResponse(); } ``` --- <GithubLink link="https://github.com/vercel/ai/blob/main/examples/next-openai-pages/pages/basics/generate-object/index.tsx" /> --- File: /ai/content/cookbook/01-next/31-generate-object-with-file-prompt.mdx --- --- title: Generate Object with File Prompt through Form Submission description: Learn how to generate object with file prompt through form submission using the AI SDK and Next.js tags: ['next', 'multi-modal'] --- # Generate Object with File Prompt through Form Submission <Note> This feature is limited to models/providers that support PDF inputs ([Anthropic](/providers/ai-sdk-providers/anthropic#pdf-support), [OpenAI](/providers/ai-sdk-providers/openai#pdf-support), [Google Gemini](/providers/ai-sdk-providers/google-generative-ai#file-inputs), and [Google Vertex](/providers/ai-sdk-providers/google-vertex#file-inputs)). </Note> With select models, you can send PDFs (files) as part of your prompt. Let's create a simple Next.js application that allows a user to upload a PDF send it to an LLM for summarization. ## Client On the frontend, create a form that allows the user to upload a PDF. When the form is submitted, send the PDF to the `/api/analyze` route. ```tsx file="app/page.tsx" 'use client'; import { useState } from 'react'; export default function Page() { const [description, setDescription] = useState<string>(); const [loading, setLoading] = useState(false); return ( <div> <form action={async formData => { try { setLoading(true); const response = await fetch('/api/analyze', { method: 'POST', body: formData, }); setLoading(false); if (response.ok) { setDescription(await response.text()); } } catch (error) { console.error('Analysis failed:', error); } }} > <div> <label>Upload Image</label> <input name="pdf" type="file" accept="application/pdf" /> </div> <button type="submit" disabled={loading}> Submit{loading && 'ing...'} </button> </form> {description && ( <pre>{JSON.stringify(JSON.parse(description), null, 2)}</pre> )} </div> ); } ``` ## Server On the server, create an API route that receives the PDF, sends it to the LLM, and returns the result. This example uses the [ `generateObject` ](/docs/reference/ai-sdk-core/generate-object) function to generate the summary as part of a structured output. ```typescript file="app/api/analyze/route.ts" import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import { z } from 'zod'; export async function POST(request: Request) { const formData = await request.formData(); const file = formData.get('pdf') as File; // Convert the file's arrayBuffer to a Base64 data URL const arrayBuffer = await file.arrayBuffer(); const uint8Array = new Uint8Array(arrayBuffer); // Convert Uint8Array to an array of characters const charArray = Array.from(uint8Array, byte => String.fromCharCode(byte)); const binaryString = charArray.join(''); const base64Data = btoa(binaryString); const fileDataUrl = `data:application/pdf;base64,${base64Data}`; const result = await generateObject({ model: openai('gpt-4o'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Analyze the following PDF and generate a summary.', }, { type: 'file', data: fileDataUrl, mediaType: 'application/pdf', }, ], }, ], schema: z.object({ people: z .object({ name: z.string().describe('The name of the person.'), age: z.number().min(0).describe('The age of the person.'), }) .array() .describe('An array of people.'), }), }); return Response.json(result.object); } ``` --- File: /ai/content/cookbook/01-next/40-stream-object.mdx --- --- title: Stream Object description: Learn how to stream object using the AI SDK and Next.js tags: ['next', 'streaming', 'structured data'] --- # Stream Object Object generation can sometimes take a long time to complete, especially when you're generating a large schema. In such cases, it is useful to stream the object generation process to the client in real-time. This allows the client to display the generated object as it is being generated, rather than have users wait for it to complete before displaying the result. <Browser> <ObjectGeneration stream object={{ notifications: [ { name: 'Jamie Roberts', message: "Hey! How's the study grind going? Need a coffee boost?", minutesAgo: 15, }, { name: 'Prof. Morgan', message: 'Reminder: Your term paper is due promptly at 8 AM tomorrow. Please ensure it meets the submission guidelines outlined.', minutesAgo: 46, }, { name: 'Alex Chen', message: "Dude, urgent! Borrow your notes for tomorrow's exam? I swear mine got eaten by my dog!", minutesAgo: 30, }, ], }} /> </Browser> ## Object Mode The `streamObject` function allows you to specify different output strategies using the `output` parameter. By default, the output mode is set to `object`, which will generate exactly the structured object that you specify in the schema option. ### Schema It is helpful to set up the schema in a separate file that is imported on both the client and server. ```ts filename='app/api/use-object/schema.ts' import { z } from 'zod'; // define a schema for the notifications export const notificationSchema = z.object({ notifications: z.array( z.object({ name: z.string().describe('Name of a fictional person.'), message: z.string().describe('Message. Do not use emojis or links.'), }), ), }); ``` ### Client The client uses [`useObject`](/docs/reference/ai-sdk-ui/use-object) to stream the object generation process. The results are partial and are displayed as they are received. Please note the code for handling `undefined` values in the JSX. ```tsx filename='app/page.tsx' 'use client'; import { experimental_useObject as useObject } from '@ai-sdk/react'; import { notificationSchema } from './api/use-object/schema'; export default function Page() { const { object, submit } = useObject({ api: '/api/use-object', schema: notificationSchema, }); return ( <div> <button onClick={() => submit('Messages during finals week.')}> Generate notifications </button> {object?.notifications?.map((notification, index) => ( <div key={index}> <p>{notification?.name}</p> <p>{notification?.message}</p> </div> ))} </div> ); } ``` ### Server On the server, we use [`streamObject`](/docs/reference/ai-sdk-core/stream-object) to stream the object generation process. ```typescript filename='app/api/use-object/route.ts' import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import { notificationSchema } from './schema'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const context = await req.json(); const result = streamObject({ model: openai('gpt-4.1'), schema: notificationSchema, prompt: `Generate 3 notifications for a messages app in this context:` + context, }); return result.toTextStreamResponse(); } ``` ## Loading State and Stopping the Stream You can use the `loading` state to display a loading indicator while the object is being generated. You can also use the `stop` function to stop the object generation process. ```tsx filename='app/page.tsx' highlight="7,16,21,24" 'use client'; import { experimental_useObject as useObject } from '@ai-sdk/react'; import { notificationSchema } from './api/use-object/schema'; export default function Page() { const { object, submit, isLoading, stop } = useObject({ api: '/api/use-object', schema: notificationSchema, }); return ( <div> <button onClick={() => submit('Messages during finals week.')} disabled={isLoading} > Generate notifications </button> {isLoading && ( <div> <div>Loading...</div> <button type="button" onClick={() => stop()}> Stop </button> </div> )} {object?.notifications?.map((notification, index) => ( <div key={index}> <p>{notification?.name}</p> <p>{notification?.message}</p> </div> ))} </div> ); } ``` ## Array Mode The "array" output mode allows you to stream an array of objects one element at a time. This is particularly useful when generating lists of items. ### Schema First, update the schema to generate a single object (remove the `z.array()`). ```ts filename='app/api/use-object/schema.ts' import { z } from 'zod'; // define a schema for a single notification export const notificationSchema = z.object({ name: z.string().describe('Name of a fictional person.'), message: z.string().describe('Message. Do not use emojis or links.'), }); ``` ### Client On the client, you wrap the schema in `z.array()` to generate an array of objects. ```tsx filename='app/page.tsx' 'use client'; import { experimental_useObject as useObject } from '@ai-sdk/react'; import { notificationSchema } from '../api/use-object/schema'; import z from 'zod'; export default function Page() { const { object, submit, isLoading, stop } = useObject({ api: '/api/use-object', schema: z.array(notificationSchema), }); return ( <div> <button onClick={() => submit('Messages during finals week.')} disabled={isLoading} > Generate notifications </button> {isLoading && ( <div> <div>Loading...</div> <button type="button" onClick={() => stop()}> Stop </button> </div> )} {object?.map((notification, index) => ( <div key={index}> <p>{notification?.name}</p> <p>{notification?.message}</p> </div> ))} </div> ); } ``` ### Server On the server, specify `output: 'array'` to generate an array of objects. ```typescript filename='app/api/use-object/route.ts' import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import { notificationSchema } from './schema'; export const maxDuration = 30; export async function POST(req: Request) { const context = await req.json(); const result = streamObject({ model: openai('gpt-4.1'), output: 'array', schema: notificationSchema, prompt: `Generate 3 notifications for a messages app in this context:` + context, }); return result.toTextStreamResponse(); } ``` ## No Schema Mode The "no-schema" output mode can be used when you don't want to specify a schema, for example when the data structure is defined by a dynamic user request. When using this mode, omit the schema parameter and set `output: 'no-schema'`. The model will still attempt to generate JSON data based on the prompt. ### Client On the client, you wrap the schema in `z.array()` to generate an array of objects. ```tsx filename='app/page.tsx' 'use client'; import { experimental_useObject as useObject } from '@ai-sdk/react'; import { z } from 'zod'; export default function Page() { const { object, submit, isLoading, stop } = useObject({ api: '/api/use-object', schema: z.unknown(), }); return ( <div> <button onClick={() => submit('Messages during finals week.')} disabled={isLoading} > Generate notifications </button> {isLoading && ( <div> <div>Loading...</div> <button type="button" onClick={() => stop()}> Stop </button> </div> )} {JSON.stringify(object, null, 2)} </div> ); } ``` ### Server On the server, specify `output: 'no-schema'`. ```typescript filename='app/api/use-object/route.ts' import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; export const maxDuration = 30; export async function POST(req: Request) { const context = await req.json(); const result = streamObject({ model: openai('gpt-4o'), output: 'no-schema', prompt: `Generate 3 notifications (in JSON) for a messages app in this context:` + context, }); return result.toTextStreamResponse(); } ``` --- File: /ai/content/cookbook/01-next/70-call-tools.mdx --- --- title: Call Tools description: Learn how to call tools using the AI SDK and Next.js tags: ['next', 'tool use'] --- # Call Tools Some models allow developers to provide a list of tools that can be called at any time during a generation. This is useful for extending the capabilities of a language model to either use logic or data to interact with systems external to the model. <Browser> <ChatGeneration history={[ { role: 'User', content: 'How is it going?' }, { role: 'Assistant', content: 'All good, how may I help you?' }, ]} inputMessage={{ role: 'User', content: 'What is the weather in Paris and New York?', }} outputMessage={{ role: 'Assistant', content: 'The weather is 24°C in New York and 25°C in Paris. It is sunny in both cities.', }} /> </Browser> ## Client Let's create a React component that imports the `useChat` hook from the `@ai-sdk/react` module. The `useChat` hook will call the `/api/chat` endpoint when the user sends a message. The endpoint will generate the assistant's response based on the conversation history and stream it to the client. If the assistant responds with a tool call, the hook will automatically display them as well. ```tsx filename='app/page.tsx' 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; import type { ChatMessage } from './api/chat/route'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat<ChatMessage>({ transport: new DefaultChatTransport({ api: '/api/chat', }), }); return ( <div> <input className="border" value={input} onChange={event => { setInput(event.target.value); }} onKeyDown={async event => { if (event.key === 'Enter') { sendMessage({ text: input, }); setInput(''); } }} /> {messages.map((message, index) => ( <div key={index}> {message.parts.map(part => { switch (part.type) { case 'text': return <div key={`${message.id}-text`}>{part.text}</div>; case 'tool-getWeather': return ( <div key={`${message.id}-weather`}> {JSON.stringify(part, null, 2)} </div> ); } })} </div> ))} </div> ); } ``` ## Server You will create a new route at `/api/chat` that will use the `streamText` function from the `ai` module to generate the assistant's response based on the conversation history. You will use the [`tools`](/docs/reference/ai-sdk-core/generate-text#tools) parameter to specify a tool called `celsiusToFahrenheit` that will convert a user given value in celsius to fahrenheit. You will also use zod to specify the schema for the `celsiusToFahrenheit` function's parameters. ```tsx filename='app/api/chat/route.ts' import { openai } from '@ai-sdk/openai'; import { type InferUITools, type ToolSet, type UIDataTypes, type UIMessage, convertToModelMessages, stepCountIs, streamText, tool, } from 'ai'; import { z } from 'zod'; const tools: ToolSet = { getWeather: tool({ description: 'Get the weather for a location', inputSchema: z.object({ city: z.string().describe('The city to get the weather for'), unit: z .enum(['C', 'F']) .describe('The unit to display the temperature in'), }), execute: async ({ city, unit }) => { const weather = { value: 24, description: 'Sunny', }; return `It is currently ${weather.value}°${unit} and ${weather.description} in ${city}!`; }, }), }; export type ChatTools = InferUITools<typeof tools>; export type ChatMessage = UIMessage<never, UIDataTypes, ChatTools>; export async function POST(req: Request) { const { messages }: { messages: ChatMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), system: 'You are a helpful assistant.', messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), tools, }); return result.toUIMessageStreamResponse(); } ``` --- <GithubLink link="https://github.com/vercel/ai/blob/main/examples/next-openai-pages/pages/tools/call-tool/index.tsx" /> --- File: /ai/content/cookbook/01-next/72-call-tools-multiple-steps.mdx --- --- title: Call Tools in Multiple Steps description: Learn how to call tools in multiple steps using the AI SDK and Next.js tags: ['next', 'streaming', 'tool use'] --- # Call Tools in Multiple Steps Some language models are great at calling tools in multiple steps to achieve a more complex task. This is particularly useful when the tools are dependent on each other and need to be executed in sequence during the same generation step. ## Client Let's create a React component that imports the `useChat` hook from the `@ai-sdk/react` module. The `useChat` hook will call the `/api/chat` endpoint when the user sends a message. The endpoint will generate the assistant's response based on the conversation history and stream it to the client. If the assistant responds with a tool call, the hook will automatically display them as well. ```tsx filename='app/page.tsx' 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; import type { ChatMessage } from './api/chat/route'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat<ChatMessage>({ transport: new DefaultChatTransport({ api: '/api/chat', }), }); return ( <div> <input className="border" value={input} onChange={event => { setInput(event.target.value); }} onKeyDown={async event => { if (event.key === 'Enter') { sendMessage({ text: input, }); setInput(''); } }} /> {messages.map((message, index) => ( <div key={index}> {message.parts.map((part, i) => { switch (part.type) { case 'text': return <div key={`${message.id}-text`}>{part.text}</div>; case 'tool-getLocation': case 'tool-getWeather': return ( <div key={`${message.id}-weather-${i}`}> {JSON.stringify(part, null, 2)} </div> ); } })} </div> ))} </div> ); } ``` ## Server You will create a new route at `/api/chat` that will use the `streamText` function from the `ai` module to generate the assistant's response based on the conversation history. You will use the [`tools`](/docs/reference/ai-sdk-core/generate-text#tools) parameter to specify two tools called `getLocation` and `getWeather` that will first get the user's location and then use it to get the weather. You will add the two functions mentioned earlier and use zod to specify the schema for its parameters. To call tools in multiple steps, you can use the `stopWhen` option to specify the stopping conditions for when the model generates a tool call. In this example, you will set it to `stepCountIs(5)` to allow for multiple consecutive tool calls (steps). ```ts filename='app/api/chat/route.ts' import { openai } from '@ai-sdk/openai'; import { type InferUITools, type ToolSet, type UIDataTypes, type UIMessage, convertToModelMessages, stepCountIs, streamText, tool, } from 'ai'; import { z } from 'zod'; const tools: ToolSet = { getLocation: tool({ description: 'Get the location of the user', inputSchema: z.object({}), execute: async () => { const location = { lat: 37.7749, lon: -122.4194 }; return `Your location is at latitude ${location.lat} and longitude ${location.lon}`; }, }), getWeather: tool({ description: 'Get the weather for a location', inputSchema: z.object({ city: z.string().describe('The city to get the weather for'), unit: z .enum(['C', 'F']) .describe('The unit to display the temperature in'), }), execute: async ({ city, unit }) => { const weather = { value: 24, description: 'Sunny', }; return `It is currently ${weather.value}°${unit} and ${weather.description} in ${city}!`; }, }), }; export type ChatTools = InferUITools<typeof tools>; export type ChatMessage = UIMessage<never, UIDataTypes, ChatTools>; export async function POST(req: Request) { const { messages }: { messages: ChatMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), system: 'You are a helpful assistant.', messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), tools, }); return result.toUIMessageStreamResponse(); } ``` --- File: /ai/content/cookbook/01-next/73-mcp-tools.mdx --- --- title: Model Context Protocol (MCP) Tools description: Learn how to use MCP tools with the AI SDK and Next.js tags: ['next', 'tool use', 'agent', 'mcp'] --- # MCP Tools The AI SDK supports Model Context Protocol (MCP) tools by offering a lightweight client that exposes a `tools` method for retrieving tools from a MCP server. After use, the client should always be closed to release resources. ## Server 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: <Snippet text="pnpm install @modelcontextprotocol/sdk" /> ```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 { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp'; export async function POST(req: Request) { const { prompt }: { prompt: string } = await req.json(); try { // Initialize an MCP client to connect to a `stdio` MCP server: const transport = new StdioClientTransport({ command: 'node', args: ['src/stdio/dist/server.js'], }); const stdioClient = await experimental_createMCPClient({ transport, }); // You can also connect to StreamableHTTP MCP servers const httpTransport = new StreamableHTTPClientTransport( new URL('http://localhost:3000/mcp'), ); const httpClient = await experimental_createMCPClient({ transport: httpTransport, }); // Alternatively, you can connect to a Server-Sent Events (SSE) MCP server: const sseTransport = new SSEClientTransport( new URL('http://localhost:3000/sse'), ); const sseClient = await experimental_createMCPClient({ transport: sseTransport, }); const toolSetOne = await stdioClient.tools(); const toolSetTwo = await httpClient.tools(); const toolSetThree = await sseClient.tools(); const tools = { ...toolSetOne, ...toolSetTwo, ...toolSetThree, // note: this approach causes subsequent tool sets to override tools with the same name }; const response = await streamText({ model: openai('gpt-4o'), tools, prompt, // When streaming, the client should be closed after the response is finished: onFinish: async () => { await stdioClient.close(); await httpClient.close(); await sseClient.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 httpClient.close(); await sseClient.close(); }, }); return response.toDataStreamResponse(); } catch (error) { return new Response('Internal Server Error', { status: 500 }); } } ``` ## Client Let's create a simple React component that imports the `useCompletion` hook from the `@ai-sdk/react` module. The `useCompletion` hook will call the `/api/completion` endpoint when a button is clicked. The endpoint will generate text based on the input prompt and stream it to the client. ```tsx filename="app/page.tsx" 'use client'; import { useCompletion } from '@ai-sdk/react'; export default function Page() { const { completion, complete } = useCompletion({ api: '/api/completion', }); return ( <div> <div onClick={async () => { await complete( 'Please schedule a call with Sonny and Robby for tomorrow at 10am ET for me!', ); }} > Schedule a call </div> {completion} </div> ); } ``` --- File: /ai/content/cookbook/01-next/75-human-in-the-loop.mdx --- --- title: Human-in-the-Loop Agent with Next.js description: Add a human approval step to your agentic system with Next.js and the AI SDK tags: ['next', 'agents', 'tool use'] --- # Human-in-the-Loop with Next.js When building agentic systems, it's important to add human-in-the-loop (HITL) functionality to ensure that users can approve actions before the system executes them. This recipe will describe how to [build a low-level solution](#adding-a-confirmation-step) and then provide an [example abstraction](#building-your-own-abstraction) you could implement and customise based on your needs. ## Background To understand how to implement this functionality, let's look at how tool calling works in a simple Next.js chatbot application with the AI SDK. On the frontend, use the `useChat` hook to manage the message state and user interaction. ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; export default function Chat() { const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', }), }); const [input, setInput] = useState(''); return ( <div> <div> {messages?.map(m => ( <div key={m.id}> <strong>{`${m.role}: `}</strong> {m.parts?.map((part, i) => { switch (part.type) { case 'text': return <div key={i}>{part.text}</div>; } })} <br /> </div> ))} </div> <form onSubmit={e => { e.preventDefault(); if (input.trim()) { sendMessage({ text: input }); setInput(''); } }} > <input value={input} placeholder="Say something..." onChange={e => setInput(e.target.value)} /> </form> </div> ); } ``` On the backend, create a route handler (API Route) that returns a `UIMessageStreamResponse`. Within the execute function of `createUIMessageStream`, call `streamText` and pass in the converted `messages` (sent from the client). Finally, merge the resulting generation into the `UIMessageStream`. ```ts filename="api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { createUIMessageStreamResponse, createUIMessageStream, streamText, tool, convertToModelMessages, stepCountIs, UIMessage, } from 'ai'; import { z } from 'zod'; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const stream = createUIMessageStream({ execute: async ({ writer }) => { const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), tools: { getWeatherInformation: tool({ description: 'show the weather in a given city to the user', inputSchema: z.object({ city: z.string() }), outputSchema: z.string(), execute: async ({ city }) => { const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy']; return weatherOptions[ Math.floor(Math.random() * weatherOptions.length) ]; }, }), }, stopWhen: stepCountIs(5), }); writer.merge(result.toUIMessageStream({ originalMessages: messages })); }, }); return createUIMessageStreamResponse(stream); } ``` What happens if you ask the LLM for the weather in New York? The LLM has one tool available, `weather`, which requires a `location` to run. This tool will, as stated in the tool's `description`, "show the weather in a given city to the user". If the LLM decides that the `weather` tool could answer the user's query, it would generate a `ToolCall`, extracting the `location` from the context. The AI SDK would then run the associated `execute` function, passing in the `location` parameter, and finally returning a tool result. To introduce a HITL step you will add a confirmation step to this process in between the tool call and the tool result. ## Adding a Confirmation Step At a high level, you will: 1. Intercept tool calls before they are executed 2. Render a confirmation UI with Yes/No buttons 3. Send a temporary tool result indicating whether the user confirmed or declined 4. On the server, check for the confirmation state in the tool result: - If confirmed, execute the tool and update the result - If declined, update the result with an error message 5. Send the updated tool result back to the client to maintain state consistency ### Forward Tool Call To The Client To implement HITL functionality, you start by omitting the `execute` function from the tool definition. This allows the frontend to intercept the tool call and handle the responsibility of adding the final tool result to the tool call. ```ts filename="api/chat/route.ts" highlight="18" import { openai } from '@ai-sdk/openai'; import { createUIMessageStreamResponse, createUIMessageStream, streamText, tool, convertToModelMessages, stepCountIs, } from 'ai'; import { z } from 'zod'; export async function POST(req: Request) { const { messages } = await req.json(); const stream = createUIMessageStream({ execute: async ({ writer }) => { const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), tools: { getWeatherInformation: tool({ description: 'show the weather in a given city to the user', inputSchema: z.object({ city: z.string() }), outputSchema: z.string(), // execute function removed to stop automatic execution }), }, stopWhen: stepCountIs(5), }); writer.merge(result.toUIMessageStream({ originalMessages: messages })); // pass in original messages to avoid duplicate assistant messages }, }); return createUIMessageStreamResponse(stream); } ``` <Note type="warning"> Each tool call must have a corresponding tool result. If you do not add a tool result, all subsequent generations will fail. </Note> ### Intercept Tool Call On the frontend, you map through the messages, either rendering the message content or checking for tool invocations and rendering custom UI. You can check if the tool requiring confirmation has been called and, if so, present options to either confirm or deny the proposed tool call. This confirmation is done using the `addToolResult` function to create a tool result and append it to the associated tool call. ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, isToolUIPart, getToolName, lastAssistantMessageIsCompleteWithToolCalls, } from 'ai'; import { useState } from 'react'; export default function Chat() { const { messages, addToolResult, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', }), sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, }); const [input, setInput] = useState(''); return ( <div> <div> {messages?.map(m => ( <div key={m.id}> <strong>{`${m.role}: `}</strong> {m.parts?.map((part, i) => { if (part.type === 'text') { return <div key={i}>{part.text}</div>; } if (isToolUIPart(part)) { const toolName = getToolName(part); const toolCallId = part.toolCallId; // render confirmation tool (client-side tool with user interaction) if ( toolName === 'getWeatherInformation' && part.state === 'input-available' ) { return ( <div key={toolCallId}> Get weather information for {part.input.city}? <div> <button onClick={async () => { await addToolResult({ toolCallId, tool: toolName, output: 'Yes, confirmed.', }); sendMessage(); }} > Yes </button> <button onClick={async () => { await addToolResult({ toolCallId, tool: toolName, output: 'No, denied.', }); sendMessage(); }} > No </button> </div> </div> ); } } })} <br /> </div> ))} </div> <form onSubmit={e => { e.preventDefault(); if (input.trim()) { sendMessage({ text: input }); setInput(''); } }} > <input value={input} placeholder="Say something..." onChange={e => setInput(e.target.value)} /> </form> </div> ); } ``` <Note> The `sendMessage()` function after `addToolResult` will trigger a call to your route handler. In this example, `sendMessage()` is called automatically when tool calls are complete because we set `sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls`. Without this option, you would need to manually call `sendMessage()` after `await addToolResult()`. </Note> ### Handle Confirmation Response Adding a tool result and sending the message will trigger another call to your route handler. Before sending the new messages to the language model, you pull out the last message and map through the message parts to see if the tool requiring confirmation was called and whether it's in a "result" state. If those conditions are met, you check the confirmation state (the tool result state that you set on the frontend with the `addToolResult` function). ```ts filename="api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { createUIMessageStreamResponse, createUIMessageStream, streamText, tool, convertToModelMessages, stepCountIs, isToolUIPart, getToolName, UIMessage, } from 'ai'; import { z } from 'zod'; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const stream = createUIMessageStream({ execute: async ({ writer }) => { // pull out last message const lastMessage = messages[messages.length - 1]; lastMessage.parts = await Promise.all( // map through all message parts lastMessage.parts?.map(async part => { if (!isToolUIPart(part)) { return part; } const toolName = getToolName(part); // return if tool isn't weather tool or in a output-available state if ( toolName !== 'getWeatherInformation' || part.state !== 'output-available' ) { return part; } // switch through tool output states (set on the frontend) switch (part.output) { case 'Yes, confirmed.': { const result = await executeWeatherTool(part.input); // forward updated tool result to the client: writer.write({ type: 'tool-output-available', toolCallId: part.toolCallId, output: result, }); // update the message part: return { ...part, output: result }; } case 'No, denied.': { const result = 'Error: User denied access to weather information'; // forward updated tool result to the client: writer.write({ type: 'tool-output-available', toolCallId: part.toolCallId, output: result, }); // update the message part: return { ...part, output: result }; } default: return part; } }) ?? [], ); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), tools: { getWeatherInformation: tool({ description: 'show the weather in a given city to the user', inputSchema: z.object({ city: z.string() }), outputSchema: z.string(), }), }, stopWhen: stepCountIs(5), }); writer.merge(result.toUIMessageStream({ originalMessages: messages })); }, }); return createUIMessageStreamResponse(stream); } async function executeWeatherTool({ city }: { city: string }) { const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy']; return weatherOptions[Math.floor(Math.random() * weatherOptions.length)]; } ``` In this implementation, you use simple strings like "Yes, the user confirmed" or "No, the user declined" as states. If confirmed, you execute the tool. If declined, you do not execute the tool. In both cases, you update the tool result from the arbitrary data you sent with the `addToolResult` function to either the result of the execute function or an "Execution declined" statement. You send the updated tool result back to the frontend to maintain state synchronization. After handling the tool result, your API route continues. This triggers another generation with the updated tool result, allowing the LLM to continue attempting to solve the query. ## Building your own abstraction The solution above is low-level and not very friendly to use in a production environment. You can build your own abstraction using these concepts ## Move tool declarations to their own file First, you will need to move tool declarations to their own file: ```ts filename="tools.ts" import { tool, ToolSet } from 'ai'; import { z } from 'zod'; const getWeatherInformation = tool({ description: 'show the weather in a given city to the user', inputSchema: z.object({ city: z.string() }), outputSchema: z.string(), // must define outputSchema // no execute function, we want human in the loop }); const getLocalTime = tool({ description: 'get the local time for a specified location', inputSchema: z.object({ location: z.string() }), outputSchema: z.string(), // including execute function -> no confirmation required execute: async ({ location }) => { console.log(`Getting local time for ${location}`); return '10am'; }, }); export const tools = { getWeatherInformation, getLocalTime, } satisfies ToolSet; ``` In this file, you have two tools, `getWeatherInformation` (requires confirmation to run) and `getLocalTime`. ### Create Type Definitions Create a types file to define a custom message type: ```ts filename="types.ts" import { InferUITools, UIDataTypes, UIMessage } from 'ai'; import { tools } from './tools'; export type MyTools = InferUITools<typeof tools>; // Define custom message type export type HumanInTheLoopUIMessage = UIMessage< never, // metadata type UIDataTypes, // data parts type MyTools // tools type >; ``` ### Create Utility Functions ```ts filename="utils.ts" import { convertToModelMessages, Tool, ToolCallOptions, ToolSet, UIMessageStreamWriter, getToolName, isToolUIPart, } from 'ai'; import { HumanInTheLoopUIMessage } from './types'; // Approval string to be shared across frontend and backend export const APPROVAL = { YES: 'Yes, confirmed.', NO: 'No, denied.', } as const; function isValidToolName<K extends PropertyKey, T extends object>( key: K, obj: T, ): key is K & keyof T { return key in obj; } /** * Processes tool invocations where human input is required, executing tools when authorized. * * @param options - The function options * @param options.tools - Map of tool names to Tool instances that may expose execute functions * @param options.writer - UIMessageStream writer for sending results back to the client * @param options.messages - Array of messages to process * @param executionFunctions - Map of tool names to execute functions * @returns Promise resolving to the processed messages */ export async function processToolCalls< Tools extends ToolSet, ExecutableTools extends { [Tool in keyof Tools as Tools[Tool] extends { execute: Function } ? never : Tool]: Tools[Tool]; }, >( { writer, messages, }: { tools: Tools; // used for type inference writer: UIMessageStreamWriter; messages: HumanInTheLoopUIMessage[]; }, executeFunctions: { [K in keyof Tools & keyof ExecutableTools]?: ( args: any, context: ToolCallOptions, ) => Promise<any>; }, ): Promise<HumanInTheLoopUIMessage[]> { const lastMessage = messages[messages.length - 1]; const parts = lastMessage.parts; if (!parts) return messages; const processedParts = await Promise.all( parts.map(async part => { // Only process tool invocations parts if (!isToolUIPart(part)) return part; const toolName = getToolName(part); // Only continue if we have an execute function for the tool (meaning it requires confirmation) and it's in a 'output-available' state if (!(toolName in executeFunctions) || part.state !== 'output-available') return part; let result; if (part.output === APPROVAL.YES) { // Get the tool and check if the tool has an execute function. if ( !isValidToolName(toolName, executeFunctions) || part.state !== 'output-available' ) { return part; } const toolInstance = executeFunctions[toolName] as Tool['execute']; if (toolInstance) { result = await toolInstance(part.input, { messages: convertToModelMessages(messages), toolCallId: part.toolCallId, }); } else { result = 'Error: No execute function found on tool'; } } else if (part.output === APPROVAL.NO) { result = 'Error: User denied access to tool execution'; } else { // For any unhandled responses, return the original part. return part; } // Forward updated tool result to the client. writer.write({ type: 'tool-output-available', toolCallId: part.toolCallId, output: result, }); // Return updated toolInvocation with the actual result. return { ...part, output: result, }; }), ); // Finally return the processed messages return [...messages.slice(0, -1), { ...lastMessage, parts: processedParts }]; } export function getToolsRequiringConfirmation<T extends ToolSet>( tools: T, ): string[] { return (Object.keys(tools) as (keyof T)[]).filter(key => { const maybeTool = tools[key]; return typeof maybeTool.execute !== 'function'; }) as string[]; } ``` In this file, you first declare the confirmation strings as constants so we can share them across the frontend and backend (reducing possible errors). Next, we create function called `processToolCalls` which takes in the `messages`, `tools`, and the `writer`. It also takes in a second parameter, `executeFunction`, which is an object that maps `toolName` to the functions that will be run upon human confirmation. This function is strongly typed so: - it autocompletes `executableTools` - these are tools without an execute function - provides full type-safety for arguments and options available within the `execute` function Unlike the low-level example, this will return a modified array of `messages` that can be passed directly to the LLM. Finally, you declare a function called `getToolsRequiringConfirmation` that takes your tools as an argument and then will return the names of your tools without execute functions (in an array of strings). This avoids the need to manually write out and check for `toolName`'s on the frontend. ### Update Route Handler Update your route handler to use the `processToolCalls` utility function. ```ts filename="app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { createUIMessageStreamResponse, createUIMessageStream, streamText, convertToModelMessages, stepCountIs, } from 'ai'; import { processToolCalls } from './utils'; import { tools } from './tools'; import { HumanInTheLoopUIMessage } from './types'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: HumanInTheLoopUIMessage[] } = await req.json(); const stream = createUIMessageStream({ execute: async ({ writer }) => { // Utility function to handle tools that require human confirmation // Checks for confirmation in last message and then runs associated tool const processedMessages = await processToolCalls( { messages, writer, tools, }, { // type-safe object for tools without an execute function getWeatherInformation: async ({ city }) => { const conditions = ['sunny', 'cloudy', 'rainy', 'snowy']; return `The weather in ${city} is ${ conditions[Math.floor(Math.random() * conditions.length)] }.`; }, }, ); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(processedMessages), tools, stopWhen: stepCountIs(5), }); writer.merge( result.toUIMessageStream({ originalMessages: processedMessages }), ); }, }); return createUIMessageStreamResponse({ stream }); } ``` ### Update Frontend Finally, update the frontend to use the new `getToolsRequiringConfirmation` function and the `APPROVAL` values: ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, getToolName, isToolUIPart, lastAssistantMessageIsCompleteWithToolCalls, } from 'ai'; import { tools } from '../api/chat/tools'; import { APPROVAL, getToolsRequiringConfirmation } from '../api/chat/utils'; import { useState } from 'react'; import { HumanInTheLoopUIMessage, MyTools } from '../api/chat/types'; export default function Chat() { const { messages, addToolResult, sendMessage } = useChat<HumanInTheLoopUIMessage>({ transport: new DefaultChatTransport({ api: '/api/chat', }), sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, }); const [input, setInput] = useState(''); const toolsRequiringConfirmation = getToolsRequiringConfirmation(tools); // used to disable input while confirmation is pending const pendingToolCallConfirmation = messages.some(m => m.parts?.some( part => isToolUIPart(part) && part.state === 'input-available' && toolsRequiringConfirmation.includes(getToolName(part)), ), ); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages?.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> <strong>{`${m.role}: `}</strong> {m.parts?.map((part, i) => { if (part.type === 'text') { return <div key={i}>{part.text}</div>; } if (isToolUIPart<MyTools>(part)) { const toolName = getToolName(part); const toolCallId = part.toolCallId; const dynamicInfoStyles = 'font-mono bg-zinc-100 p-1 text-sm'; // render confirmation tool (client-side tool with user interaction) if ( toolsRequiringConfirmation.includes(toolName) && part.state === 'input-available' ) { return ( <div key={toolCallId}> Run <span className={dynamicInfoStyles}>{toolName}</span>{' '} with args: <br /> <span className={dynamicInfoStyles}> {JSON.stringify(part.input, null, 2)} </span> <div className="flex gap-2 pt-2"> <button className="px-4 py-2 font-bold text-white bg-blue-500 rounded hover:bg-blue-700" onClick={async () => { await addToolResult({ toolCallId, tool: toolName, output: APPROVAL.YES, }); sendMessage(); }} > Yes </button> <button className="px-4 py-2 font-bold text-white bg-red-500 rounded hover:bg-red-700" onClick={async () => { await addToolResult({ toolCallId, tool: toolName, output: APPROVAL.NO, }); sendMessage(); }} > No </button> </div> </div> ); } } })} <br /> </div> ))} <form onSubmit={e => { e.preventDefault(); if (input.trim()) { sendMessage({ text: input }); setInput(''); } }} > <input disabled={pendingToolCallConfirmation} className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 rounded shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.target.value)} /> </form> </div> ); } ``` ## Full Example To see this code in action, check out the [`next-openai` example](https://github.com/vercel/ai/tree/main/examples/next-openai) in the AI SDK repository. Navigate to the `/use-chat-human-in-the-loop` page and associated route handler. --- File: /ai/content/cookbook/01-next/80-send-custom-body-from-use-chat.mdx --- --- title: Send Custom Body from useChat description: Learn how to send a custom body from the useChat hook using the AI SDK and Next.js tags: ['next', 'chat'] --- # Send Custom Body from useChat By default, `useChat` sends all messages as well as information from the request to the server. However, it is often desirable to control the body content that is sent to the server, e.g. to: - only send the last message - send additional data along with the message - change the structure of the request body The `prepareSendMessagesRequest` option allows you to customize the body content that is sent to the server. The function receives the message list, the request data, and the request body from the append call. It should return the body content that will be sent to the server. ## Example This example shows how to only send the text of the last message to the server. This can be useful if you want to reduce the amount of data sent to the server. ### Client ```typescript filename='app/page.tsx' highlight="3,7-14" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ prepareSendMessagesRequest: ({ id, messages }) => { return { body: { id, message: messages[messages.length - 1], }, }; }, }), }); return ( <div> {messages.map((message, index) => ( <div key={index}> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part) => { switch (part.type) { case "text": return <div key={`${message.id}-text`}>{part.text}</div>; } })} </div> ))} <form onSubmit={(e) => { e.preventDefault(); sendMessage({text: input}); setInput(''); }}> <input value={input} onChange={(e) => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` ### Server We need to adjust the server to receive the custom request format with the chat ID and last message. The rest of the message history can be loaded from storage. ```tsx filename='app/api/chat/route.ts' highlight="8,11,12,16" import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { id, message } = await req.json(); // Load existing messages and add the new one const messages = await loadMessages(id); messages.push(message); // Call the language model const result = streamText({ model: openai('gpt-4.1'), messages: convertToModelMessages(messages), }); // Respond with the stream return result.toUIMessageStreamResponse({ originalMessages: messages, onFinish: ({ messages: newMessages }) => { saveMessages(id, newMessages); }, }); } ``` --- File: /ai/content/cookbook/01-next/90-render-visual-interface-in-chat.mdx --- --- title: Render Visual Interface in Chat description: Learn how to render visual interfaces in chat using the AI SDK and Next.js tags: ['next', 'generative user interface'] --- # Render Visual Interface in Chat An interesting consequence of language models that can call [tools](/docs/ai-sdk-core/tools-and-tool-calling) is that this ability can be used to render visual interfaces by streaming React components to the client. <Browser> <ChatGeneration history={[ { role: 'User', content: 'How is it going?' }, { role: 'Assistant', content: 'All good, how may I help you?' }, ]} inputMessage={{ role: 'User', content: 'What is the weather in San Francisco?', }} outputMessage={{ role: 'Assistant', content: 'The weather is 24°C and sunny in San Francisco.', display: ( <div className="py-4"> <WeatherCard content={{ weather: { temperature: 24, condition: 'Sunny', }, }} /> </div> ), }} /> </Browser> ## Client Let's build an assistant that gets the weather for any city by calling the `getWeatherInformation` tool. Instead of returning text during the tool call, you will render a React component that displays the weather information on the client. ```tsx filename='app/page.tsx' 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls, } from 'ai'; import { useState } from 'react'; import { ChatMessage } from './api/chat/route'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage, addToolResult } = useChat<ChatMessage>({ transport: new DefaultChatTransport({ api: '/api/chat', }), sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, // run client-side tools that are automatically executed: async onToolCall({ toolCall }) { if (toolCall.toolName === 'getLocation') { const cities = ['New York', 'Los Angeles', 'Chicago', 'San Francisco']; // No await - avoids potential deadlocks addToolResult({ tool: 'getLocation', toolCallId: toolCall.toolCallId, output: cities[Math.floor(Math.random() * cities.length)], }); } }, }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch gap-4"> {messages?.map(m => ( <div key={m.id} className="whitespace-pre-wrap flex flex-col gap-1"> <strong>{`${m.role}: `}</strong> {m.parts?.map((part, i) => { switch (part.type) { case 'text': return <div key={m.id + i}>{part.text}</div>; // render confirmation tool (client-side tool with user interaction) case 'tool-askForConfirmation': return ( <div key={part.toolCallId} className="text-gray-500 flex flex-col gap-2" > <div className="flex gap-2"> {part.state === 'output-available' ? ( <b>{part.output}</b> ) : ( <> <button className="px-4 py-2 font-bold text-white bg-blue-500 rounded hover:bg-blue-700" onClick={() => addToolResult({ tool: 'askForConfirmation', toolCallId: part.toolCallId, output: 'Yes, confirmed.', }) } > Yes </button> <button className="px-4 py-2 font-bold text-white bg-red-500 rounded hover:bg-red-700" onClick={() => addToolResult({ tool: 'askForConfirmation', toolCallId: part.toolCallId, output: 'No, denied', }) } > No </button> </> )} </div> </div> ); // other tools: case 'tool-getWeatherInformation': if (part.state === 'output-available') { return ( <div key={part.toolCallId} className="flex flex-col gap-2 p-4 bg-blue-400 rounded-lg" > <div className="flex flex-row justify-between items-center"> <div className="text-4xl text-blue-50 font-medium"> {part.output.value}° {part.output.unit === 'celsius' ? 'C' : 'F'} </div> <div className="h-9 w-9 bg-amber-400 rounded-full flex-shrink-0" /> </div> <div className="flex flex-row gap-2 text-blue-50 justify-between"> {part.output.weeklyForecast.map(forecast => ( <div key={forecast.day} className="flex flex-col items-center" > <div className="text-xs">{forecast.day}</div> <div>{forecast.value}°</div> </div> ))} </div> </div> ); } break; case 'tool-getLocation': if (part.state === 'output-available') { return ( <div key={part.toolCallId} className="text-gray-500 bg-gray-100 rounded-lg p-4" > User is in {part.output}. </div> ); } else { return ( <div key={part.toolCallId} className="text-gray-500"> Calling getLocation... </div> ); } default: break; } })} </div> ))} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` ## Server ```tsx filename='api/chat.ts' import { openai } from '@ai-sdk/openai'; import { type InferUITools, type ToolSet, type UIDataTypes, type UIMessage, convertToModelMessages, stepCountIs, streamText, tool, } from 'ai'; import { z } from 'zod'; const tools: ToolSet = { getWeatherInformation: tool({ description: 'show the weather in a given city to the user', inputSchema: z.object({ city: z.string() }), execute: async ({}: { city: string }) => { return { value: 24, unit: 'celsius', weeklyForecast: [ { day: 'Monday', value: 24 }, { day: 'Tuesday', value: 25 }, { day: 'Wednesday', value: 26 }, { day: 'Thursday', value: 27 }, { day: 'Friday', value: 28 }, { day: 'Saturday', value: 29 }, { day: 'Sunday', value: 30 }, ], }; }, }), // client-side tool that starts user interaction: askForConfirmation: tool({ description: 'Ask the user for confirmation.', inputSchema: z.object({ message: z.string().describe('The message to ask for confirmation.'), }), }), // client-side tool that is automatically executed on the client: getLocation: tool({ description: 'Get the user location. Always ask for confirmation before using this tool.', inputSchema: z.object({}), }), }; export type ChatTools = InferUITools<typeof tools>; export type ChatMessage = UIMessage<never, UIDataTypes, ChatTools>; export async function POST(request: Request) { const { messages }: { messages: ChatMessage[] } = await request.json(); const result = streamText({ model: openai('gpt-4.1'), messages: convertToModelMessages(messages), tools, stopWhen: stepCountIs(5), }); return result.toUIMessageStreamResponse(); } ``` --- File: /ai/content/cookbook/01-next/index.mdx --- --- title: Next.js --- --- File: /ai/content/cookbook/05-node/10-generate-text.mdx --- --- title: Generate Text description: Learn how to generate text using the AI SDK and Node tags: ['node'] --- # Generate Text The most basic LLM use case is generating text based on a prompt. For example, you may want to generate a response to a question or summarize a body of text. The `generateText` function can be used to generate text based on the input prompt. ```ts file='index.ts' import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = await generateText({ model: openai('gpt-4o'), prompt: 'Why is the sky blue?', }); console.log(result); ``` --- File: /ai/content/cookbook/05-node/100-retrieval-augmented-generation.mdx --- --- title: Retrieval Augmented Generation description: Learn how to use retrieval augmented generation using the AI SDK and Node tags: ['node'] --- # Retrieval Augmented Generation Retrieval Augmented Generation (RAG) is a technique that enhances the capabilities of language models by providing them with relevant information from external sources during the generation process. This approach allows the model to access and incorporate up-to-date or specific knowledge that may not be present in its original training data. This example uses [the following essay](https://raw.githubusercontent.com/run-llama/llama_index/main/docs/docs/examples/data/paul_graham/paul_graham_essay.txt) as an input (`essay.txt`). This example uses a simple in-memory vector database to store and retrieve relevant information. For a more in-depth guide, check out the [RAG Chatbot Guide](/docs/guides/rag-chatbot) which will show you how to build a RAG chatbot with [Next.js](https://nextjs.org), [Drizzle ORM](https://orm.drizzle.team/) and [Postgres](https://postgresql.org). ```ts import fs from 'fs'; import path from 'path'; import dotenv from 'dotenv'; import { openai } from '@ai-sdk/openai'; import { cosineSimilarity, embed, embedMany, generateText } from 'ai'; dotenv.config(); async function main() { const db: { embedding: number[]; value: string }[] = []; const essay = fs.readFileSync(path.join(__dirname, 'essay.txt'), 'utf8'); const chunks = essay .split('.') .map(chunk => chunk.trim()) .filter(chunk => chunk.length > 0 && chunk !== '\n'); const { embeddings } = await embedMany({ model: openai.textEmbeddingModel('text-embedding-3-small'), values: chunks, }); embeddings.forEach((e, i) => { db.push({ embedding: e, value: chunks[i], }); }); const input = 'What were the two main things the author worked on before college?'; const { embedding } = await embed({ model: openai.textEmbeddingModel('text-embedding-3-small'), value: input, }); const context = db .map(item => ({ document: item, similarity: cosineSimilarity(embedding, item.embedding), })) .sort((a, b) => b.similarity - a.similarity) .slice(0, 3) .map(r => r.document.value) .join('\n'); const { text } = await generateText({ model: openai('gpt-4o'), prompt: `Answer the following question based only on the provided context: ${context} Question: ${input}`, }); console.log(text); } main().catch(console.error); ``` --- File: /ai/content/cookbook/05-node/11-generate-text-with-chat-prompt.mdx --- --- title: Generate Text with Chat Prompt description: Learn how to generate text with chat prompt using the AI SDK and Node tags: ['node', 'chat'] --- # Generate Text with Chat Prompt Previously, we were able to generate text and objects using either a single message prompt, a system prompt, or a combination of both of them. However, there may be times when you want to generate text based on a series of messages. A chat completion allows you to generate text based on a series of messages. This series of messages can be any series of interactions between any number of systems, but the most popular and relatable use case has been a series of messages that represent a conversation between a user and a model. ```ts file='index.ts' import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = await generateText({ model: openai('gpt-4o'), maxOutputTokens: 1024, system: 'You are a helpful chatbot.', messages: [ { role: 'user', content: [{ type: 'text', text: 'Hello!' }], }, { role: 'assistant', content: [{ type: 'text', text: 'Hello! How can I help you today?' }], }, { role: 'user', content: [{ type: 'text', text: 'I need help with my computer.' }], }, ], }); console.log(result.text); ``` --- File: /ai/content/cookbook/05-node/12-generate-text-with-image-prompt.mdx --- --- title: Generate Text with Image Prompt description: Learn how to generate text with image prompt using the AI SDK and Node tags: ['node', 'multimodal'] --- # Generate Text with Image Prompt Some language models that support vision capabilities accept images as part of the prompt. Here are some of the different [formats](/docs/reference/ai-sdk-core/generate-text#content-image) you can use to include images as input. ## URL ```ts file='index.ts' import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = await generateText({ model: openai('gpt-4.1'), maxOutputTokens: 512, messages: [ { role: 'user', content: [ { type: 'text', text: 'what are the red things in this image?', }, { type: 'image', image: new URL( 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/3e/2024_Solar_Eclipse_Prominences.jpg/720px-2024_Solar_Eclipse_Prominences.jpg', ), }, ], }, ], }); console.log(result); ``` ## File Buffer ```ts file='index.ts' import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; import fs from 'fs'; const result = await generateText({ model: openai('gpt-4.1'), maxOutputTokens: 512, messages: [ { role: 'user', content: [ { type: 'text', text: 'what are the red things in this image?', }, { type: 'image', image: fs.readFileSync('./node/attachments/eclipse.jpg', { encoding: 'base64', }), }, ], }, ], }); console.log(result); ``` --- File: /ai/content/cookbook/05-node/20-stream-text.mdx --- --- title: Stream Text description: Learn how to stream text using the AI SDK and Node tags: ['node', 'streaming'] --- # Stream Text Text generation can sometimes take a long time to complete, especially when you're generating a couple of paragraphs. In such cases, it is useful to stream the text to the client in real-time. This allows the client to display the generated text as it is being generated, rather than have users wait for it to complete before displaying the result. ```txt Introducing "Joyful Hearts Day" - a holiday dedicated to spreading love, joy, and kindness to others. On Joyful Hearts Day, people exchange handmade cards, gifts, and acts of kindness to show appreciation and love for their friends, family, and community members. It is a day to focus on positivity and gratitude, spreading happiness and warmth to those around us. Traditions include decorating homes and public spaces with hearts and bright colors, hosting community events such as charity drives, volunteer projects, and festive gatherings. People also participate in random acts of kindness, such as paying for someone's coffee, leaving encouraging notes for strangers, or simply offering a helping hand to those in need. One of the main traditions of Joyful Hearts Day is the "Heart Exchange" where people write heartfelt messages to loved ones and exchange them in person or through mail. These messages can be words of encouragement, expressions of gratitude, or simply a reminder of how much they are loved. Overall, Joyful Hearts Day is a day to celebrate love, kindness, and positivity, and to spread joy and happiness to all those around us. It is a reminder to appreciate the people in our lives and to make the world a brighter and more loving place. ``` ## Without reader ```ts file='index.ts' import { streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = streamText({ model: openai('gpt-4o'), maxOutputTokens: 512, prompt: 'Invent a new holiday and describe its traditions.', }); for await (const textPart of result.textStream) { console.log(textPart); } ``` ## With reader ```ts file='index.ts' import { streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = streamText({ model: openai('gpt-4o'), maxOutputTokens: 512, prompt: 'Invent a new holiday and describe its traditions.', }); const reader = result.textStream.getReader(); while (true) { const { done, value } = await reader.read(); if (done) { break; } process.stdout.write(value); } ``` --- File: /ai/content/cookbook/05-node/21-stream-text-with-chat-prompt.mdx --- --- title: Stream Text with Chat Prompt description: Learn how to stream text with chat prompt using the AI SDK and Node tags: ['node', 'streaming', 'chat'] --- # Stream Text with Chat Prompt Text generation can sometimes take a long time to finish, especially when the response is big. In such cases, it is useful to stream the chat completion to the client in real-time. This allows the client to display the new message as it is being generated by the model, rather than have users wait for it to finish. ```ts file='index.ts' import { streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = streamText({ model: openai('gpt-4o'), maxOutputTokens: 1024, system: 'You are a helpful chatbot.', messages: [ { role: 'user', content: [{ type: 'text', text: 'Hello!' }], }, { role: 'assistant', content: [{ type: 'text', text: 'Hello! How can I help you today?' }], }, { role: 'user', content: [{ type: 'text', text: 'I need help with my computer.' }], }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } ``` --- File: /ai/content/cookbook/05-node/22-stream-text-with-image-prompt.mdx --- --- title: Stream Text with Image Prompt description: Learn how to stream text with image prompt using the AI SDK and Node tags: ['node', 'streaming', 'multimodal'] --- # Stream Text with Image Prompt Vision-language models can analyze images alongside text prompts to generate responses about visual content. This multimodal approach allows for rich interactions where you can ask questions about images, request descriptions, or analyze visual details. The combination of image and text inputs enables more sophisticated AI applications like visual question answering and image analysis. ```ts import { anthropic } from '@ai-sdk/anthropic'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = streamText({ model: anthropic('claude-3-5-sonnet-20240620'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png') }, ], }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); ``` --- File: /ai/content/cookbook/05-node/23-stream-text-with-file-prompt.mdx --- --- title: Stream Text with File Prompt description: Learn how to stream text with file prompt using the AI SDK and Node tags: ['node', 'streaming', 'multimodal'] --- # Stream Text with File Prompt Working with files in AI applications often requires analyzing documents, processing structured data, or extracting information from various file formats. File prompts allow you to send file content directly to the model, enabling tasks like document analysis, data extraction, or generating responses based on file contents. ```ts import { anthropic } from '@ai-sdk/anthropic'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = streamText({ model: anthropic('claude-3-5-sonnet-20241022'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', }, ], }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); ``` --- File: /ai/content/cookbook/05-node/30-generate-object-reasoning.mdx --- --- title: Generate Object with a Reasoning Model description: Learn how to generate structured data with a reasoning model using the AI SDK and Node tags: ['node', 'structured data', 'reasoning'] --- # Generate Object with a Reasoning Model Reasoning models, like [DeepSeek's](/providers/ai-sdk-providers/deepseek) R1, are gaining popularity due to their ability to understand and generate better responses to complex queries than non-reasoning models. You may want to use these models to generate structured data. However, most (like R1 and [OpenAI's](/providers/ai-sdk-providers/openai) o1) do not support tool-calling or structured outputs. One solution is to pass the output from a reasoning model through a smaller model that can output structured data (like gpt-4o-mini). These lightweight models can efficiently extract the structured data while adding very little overhead in terms of speed and cost. ```ts import { deepseek } from '@ai-sdk/deepseek'; import { openai } from '@ai-sdk/openai'; import { generateObject, generateText } from 'ai'; import 'dotenv/config'; import { z } from 'zod'; async function main() { const { text: rawOutput } = await generateText({ model: deepseek('deepseek-reasoner'), prompt: 'Predict the top 3 largest city by 2050. For each, return the name, the country, the reason why it will on the list, and the estimated population in millions.', }); const { object } = await generateObject({ model: openai('gpt-4o-mini'), prompt: 'Extract the desired information from this text: \n' + rawOutput, schema: z.object({ name: z.string().describe('the name of the city'), country: z.string().describe('the name of the country'), reason: z .string() .describe( 'the reason why the city will be one of the largest cities by 2050', ), estimatedPopulation: z.number(), }), output: 'array', }); console.log(object); } main().catch(console.error); ``` --- File: /ai/content/cookbook/05-node/30-generate-object.mdx --- --- title: Generate Object description: Learn how to generate structured data using the AI SDK and Node tags: ['node', 'structured data'] --- # Generate Object Earlier functions like `generateText` and `streamText` gave us the ability to generate unstructured text. However, if you want to generate structured data like JSON, you can provide a schema that describes the structure of your desired object to the `generateObject` function. The function requires you to provide a schema using [zod](https://zod.dev), a library for defining schemas for JavaScript objects. By using zod, you can also use it to validate the generated object and ensure that it conforms to the specified structure. ```ts file='index.ts' import { generateObject } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const result = await generateObject({ model: openai('gpt-4.1'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object.recipe, null, 2)); ``` --- File: /ai/content/cookbook/05-node/40-stream-object.mdx --- --- title: Stream Object description: Learn how to stream structured data using the AI SDK and Node tags: ['node', 'streaming', 'structured data'] --- # Stream Object Object generation can sometimes take a long time to complete, especially when you're generating a large schema. In Generative UI use cases, it is useful to stream the object to the client in real-time to render UIs as the object is being generated. You can use the [`streamObject`](/docs/reference/ai-sdk-core/stream-object) function to generate partial object streams. ```ts file='index.ts' import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import { z } from 'zod'; const { partialObjectStream } = streamObject({ model: openai('gpt-4.1'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.string()), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); for await (const partialObject of partialObjectStream) { console.clear(); console.log(partialObject); } ``` --- File: /ai/content/cookbook/05-node/41-stream-object-with-image-prompt.mdx --- --- title: Stream Object with Image Prompt description: Learn how to stream structured data with an image prompt using the AI SDK and Node tags: ['node', 'streaming', 'structured data', 'multimodal'] --- # Stream Object with Image Prompt Some language models that support vision capabilities accept images as part of the prompt. Here are some of the different [formats](/docs/reference/ai-sdk-core/generate-text#content-image) you can use to include images as input. ## URL ```ts file='index.ts' import { streamObject } from 'ai'; import { openai } from '@ai-sdk/openai'; import dotenv from 'dotenv'; import { z } from 'zod'; dotenv.config(); async function main() { const { partialObjectStream } = streamObject({ model: openai('gpt-4.1'), maxOutputTokens: 512, schema: z.object({ stamps: z.array( z.object({ country: z.string(), date: z.string(), }), ), }), messages: [ { role: 'user', content: [ { type: 'text', text: 'list all the stamps in these passport pages?', }, { type: 'image', image: new URL( 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/WW2_Spanish_official_passport.jpg/1498px-WW2_Spanish_official_passport.jpg', ), }, ], }, ], }); for await (const partialObject of partialObjectStream) { console.clear(); console.log(partialObject); } } main(); ``` ## File Buffer ```ts file='index.ts' import { streamObject } from 'ai'; import { openai } from '@ai-sdk/openai'; import dotenv from 'dotenv'; import { z } from 'zod'; import fs from 'fs'; dotenv.config(); async function main() { const { partialObjectStream } = streamObject({ model: openai('gpt-4.1'), maxOutputTokens: 512, schema: z.object({ stamps: z.array( z.object({ country: z.string(), date: z.string(), }), ), }), messages: [ { role: 'user', content: [ { type: 'text', text: 'list all the stamps in these passport pages?', }, { type: 'image', image: fs.readFileSync('./data/passport.png', { encoding: 'base64', }), }, ], }, ], }); for await (const partialObject of partialObjectStream) { console.clear(); console.log(partialObject); } } main(); ``` --- File: /ai/content/cookbook/05-node/45-stream-object-record-token-usage.mdx --- --- title: Record Token Usage After Streaming Object description: Learn how to record token usage when streaming structured data using the AI SDK and Node tags: ['node', 'streaming', 'structured data', 'observability'] --- # Record Token Usage After Streaming Object When you're streaming structured data with [`streamObject`](/docs/reference/ai-sdk-core/stream-object), you may want to record the token usage for billing purposes. ## `onFinish` Callback You can use the `onFinish` callback to record token usage. It is called when the stream is finished. ```ts file='index.ts' highlight={"15-17"} import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import { z } from 'zod'; const result = streamObject({ model: openai('gpt-4.1'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.string()), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', onFinish({ usage }) { console.log('Token usage:', usage); }, }); ``` ## `usage` Promise The [`streamObject`](/docs/reference/ai-sdk-core/stream-object) result contains a `usage` promise that resolves to the total token usage. ```ts file='index.ts' highlight={"29,32"} import { openai } from '@ai-sdk/openai'; import { streamObject, TokenUsage } from 'ai'; import { z } from 'zod'; const result = streamObject({ model: openai('gpt-4.1'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.string()), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); // your custom function to record token usage: function recordTokenUsage({ inputTokens, outputTokens, totalTokens, }: TokenUsage) { console.log('Prompt tokens:', inputTokens); console.log('Completion tokens:', outputTokens); console.log('Total tokens:', totalTokens); } // use as promise: result.usage.then(recordTokenUsage); // use with async/await: recordTokenUsage(await result.usage); // note: the stream needs to be consumed because of backpressure for await (const partialObject of result.partialObjectStream) { } ``` --- File: /ai/content/cookbook/05-node/46-stream-object-record-final-object.mdx --- --- title: Record Final Object after Streaming Object description: Learn how to record the final object after streaming an object using the AI SDK and Node tags: ['node', 'streaming', 'structured data'] --- # Record Final Object after Streaming Object When you're streaming structured data, you may want to record the final object for logging or other purposes. ## `onFinish` Callback You can use the `onFinish` callback to record the final object. It is called when the stream is finished. The `object` parameter contains the final object, or `undefined` if the type validation fails. There is also an `error` parameter that contains error when e.g. the object does not match the schema. ```ts file='index.ts' highlight={"15-23"} import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import { z } from 'zod'; const result = streamObject({ model: openai('gpt-4.1'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.string()), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', onFinish({ object, error }) { // handle type validation failure (when the object does not match the schema): if (object === undefined) { console.error('Error:', error); return; } console.log('Final object:', JSON.stringify(object, null, 2)); }, }); ``` ## `object` Promise The [`streamObject`](/docs/reference/ai-sdk-core/stream-object) result contains an `object` promise that resolves to the final object. The object is fully typed. When the type validation according to the schema fails, the promise will be rejected with a `TypeValidationError`. ```ts file='index.ts' highlight={"17-26"} import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import { z } from 'zod'; const result = streamObject({ model: openai('gpt-4.1'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.string()), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); result.object .then(({ recipe }) => { // do something with the fully typed, final object: console.log('Recipe:', JSON.stringify(recipe, null, 2)); }) .catch(error => { // handle type validation failure // (when the object does not match the schema): console.error(error); }); // note: the stream needs to be consumed because of backpressure for await (const partialObject of result.partialObjectStream) { } ``` --- File: /ai/content/cookbook/05-node/50-call-tools.mdx --- --- title: Call Tools description: Learn how to call tools using the AI SDK and Node tags: ['node', 'tool use'] --- # Call Tools Some models allow developers to provide a list of tools that can be called at any time during a generation. This is useful for extending the capabilities of a language model to either use logic or data to interact with systems external to the model. ```ts import { generateText, tool } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const result = await generateText({ model: openai('gpt-4.1'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); ``` ## Accessing Tool Calls and Tool Results If the model decides to call a tool, it will generate a tool call. You can access the tool call by checking the `toolCalls` property on the result. ```ts highlight="31-44" import { openai } from '@ai-sdk/openai'; import { generateText, tool } from 'ai'; import dotenv from 'dotenv'; import { z } from 'zod'; dotenv.config(); async function main() { const result = await generateText({ model: openai('gpt-4o'), maxOutputTokens: 512, tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool calls: for (const toolCall of result.toolCalls) { switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; // string break; } case 'weather': { toolCall.input.location; // string break; } } } console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); ``` ## Accessing Tool Results You can access the result of a tool call by checking the `toolResults` property on the result. ```ts highlight="31-41" import { openai } from '@ai-sdk/openai'; import { generateText, tool } from 'ai'; import dotenv from 'dotenv'; import { z } from 'zod'; dotenv.config(); async function main() { const result = await generateText({ model: openai('gpt-4o'), maxOutputTokens: 512, tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { switch (toolResult.toolName) { case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); ``` <Note> `toolResults` will only be available if the tool has an `execute` function. </Note> ## Model Response When using tools, it's important to note that the model won't respond with the tool call results by default. This is because the model has technically already generated its response to the prompt: the tool call. Many use cases will require the model to summarise the results of the tool call within the context of the original prompt automatically. You can achieve this by [using `stopWhen`](/examples/node/tools/call-tools-multiple-steps) which will automatically send toolResults to the model to trigger another generation. --- File: /ai/content/cookbook/05-node/52-call-tools-with-image-prompt.mdx --- --- title: Call Tools with Image Prompt description: Learn how to call tools with image prompt using the AI SDK and Node tags: ['node', 'tool use', 'multimodal'] --- # Call Tools with Image Prompt Some language models that support vision capabilities accept images as part of the prompt. Here are some of the different [formats](/docs/reference/ai-sdk-core/generate-text#content-image) you can use to include images as input. ```ts import { generateText, tool } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const result = await generateText({ model: openai('gpt-4.1'), messages: [ { role: 'user', content: [ { type: 'text', text: 'can you log this meal for me?' }, { type: 'image', image: new URL( 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e4/Cheeseburger_%2817237580619%29.jpg/640px-Cheeseburger_%2817237580619%29.jpg', ), }, ], }, ], tools: { logFood: tool({ description: 'Log a food item', inputSchema: z.object({ name: z.string(), calories: z.number(), }), execute({ name, calories }) { storeInDatabase({ name, calories }); // your implementation here }, }), }, }); ``` --- File: /ai/content/cookbook/05-node/53-call-tools-multiple-steps.mdx --- --- title: Call Tools in Multiple Steps description: Learn how to call tools with multiple steps using the AI SDK and Node tags: ['node', 'tool use', 'agent'] --- # Call Tools in Multiple Steps Models call tools to gather information or perform actions that are not directly available to the model. When tool results are available, the model can use them to generate another response. You can enable multi-step tool calls in `generateText` by defining stopping conditions with `stopWhen`. This allows you to define the conditions for which your agent should stop when the model generates a tool call. ```ts highlight={"7"} import { generateText, tool, stepCountIs } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const { text, steps } = await generateText({ model: openai('gpt-4.1'), stopWhen: stepCountIs(5), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }: { location: string }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, prompt: 'What is the weather in San Francisco?', }); ``` --- File: /ai/content/cookbook/05-node/54-mcp-tools.mdx --- --- title: Model Context Protocol (MCP) Tools description: Learn how to use MCP tools with the AI SDK and Node tags: ['node', 'tool use', 'agent', 'mcp'] --- # MCP Tools The AI SDK supports Model Context Protocol (MCP) tools by offering a lightweight client that exposes a `tools` method for retrieving tools from a MCP server. After use, the client should always be closed to release resources. Use the official Model Context Protocol Typescript SDK to create the connection to the MCP server. <Snippet text="pnpm install @modelcontextprotocol/sdk" /> ```ts import { experimental_createMCPClient, generateText, stepCountIs } from 'ai'; import { Experimental_StdioMCPTransport } from 'ai/mcp-stdio'; import { openai } from '@ai-sdk/openai'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp'; let clientOne; let clientTwo; let clientThree; try { // Initialize an MCP client to connect to a `stdio` MCP server: const transport = new StdioClientTransport({ command: 'node', args: ['src/stdio/dist/server.js'], }); const clientOne = await experimental_createMCPClient({ transport, }); // You can also connect to StreamableHTTP MCP servers const httpTransport = new StreamableHTTPClientTransport( new URL('http://localhost:3000/mcp'), ); const clientTwo = await experimental_createMCPClient({ transport: httpTransport, }); // Alternatively, you can connect to a Server-Sent Events (SSE) MCP server: const sseTransport = new SSEClientTransport( new URL('http://localhost:3000/sse'), ); const clientThree = await experimental_createMCPClient({ transport: sseTransport, }); const toolSetOne = await clientOne.tools(); const toolSetTwo = await clientTwo.tools(); const toolSetThree = await clientThree.tools(); const tools = { ...toolSetOne, ...toolSetTwo, ...toolSetThree, // note: this approach causes subsequent tool sets to override tools with the same name }; const response = await generateText({ model: openai('gpt-4o'), tools, stopWhen: stepCountIs(5), messages: [ { role: 'user', content: [{ type: 'text', text: 'Find products under $100' }], }, ], }); console.log(response.text); } catch (error) { console.error(error); } finally { await Promise.all([ clientOne.close(), clientTwo.close(), clientThree.close(), ]); } ``` --- File: /ai/content/cookbook/05-node/55-manual-agent-loop.mdx --- --- title: Manual Agent Loop description: Learn how to create your own agentic loop with full control over tool execution tags: ['node', 'agent'] --- # Manual Agent Loop When you need complete control over the agentic loop and tool execution, you can manage the agent flow yourself rather than using `prepareStep` and `stopWhen`. This approach gives you full flexibility over when and how tools are executed, message history management, and loop termination conditions. This pattern is useful when you want to: - Implement custom logic between tool calls - Handle tool execution errors in specific ways - Add custom logging or monitoring - Integrate with external systems during the loop - Have complete control over the conversation history ## Example ```ts import { openai } from '@ai-sdk/openai'; import { ModelMessage, streamText, tool } from 'ai'; import 'dotenv/config'; import z from 'zod'; const getWeather = async ({ location }: { location: string }) => { return `The weather in ${location} is ${Math.floor(Math.random() * 100)} degrees.`; }; const messages: ModelMessage[] = [ { role: 'user', content: 'Get the weather in New York and San Francisco', }, ]; async function main() { while (true) { const result = streamText({ model: openai('gpt-4o'), messages, tools: { getWeather: tool({ description: 'Get the current weather in a given location', inputSchema: z.object({ location: z.string(), }), }), // add more tools here, omitting the execute function so you handle it yourself }, }); // Stream the response (only necessary for providing updates to the user) for await (const chunk of result.fullStream) { if (chunk.type === 'text-delta') { process.stdout.write(chunk.text); } if (chunk.type === 'tool-call') { console.log('\\nCalling tool:', chunk.toolName); } } // Add LLM generated messages to the message history const responseMessages = (await result.response).messages; messages.push(...responseMessages); const finishReason = await result.finishReason; if (finishReason === 'tool-calls') { const toolCalls = await result.toolCalls; // Handle all tool call execution here for (const toolCall of toolCalls) { if (toolCall.toolName === 'getWeather') { const toolOutput = await getWeather(toolCall.input); messages.push({ role: 'tool', content: [ { toolName: toolCall.toolName, toolCallId: toolCall.toolCallId, type: 'tool-result', output: { type: 'text', value: toolOutput }, // update depending on the tool's output format }, ], }); } // Handle other tool calls } } else { // Exit the loop when the model doesn't request to use any more tools console.log('\\n\\nFinal message history:'); console.dir(messages, { depth: null }); break; } } } main().catch(console.error); ``` ## Key Concepts ### Message Management The example maintains a `messages` array that tracks the entire conversation history. After each model response, the generated messages are added to this history: ```ts const responseMessages = (await result.response).messages; messages.push(...responseMessages); ``` ### Tool Execution Control Tool execution is handled manually in the loop. When the model requests tool calls, you process each one individually: ```ts if (finishReason === 'tool-calls') { const toolCalls = await result.toolCalls; for (const toolCall of toolCalls) { if (toolCall.toolName === 'getWeather') { const toolOutput = await getWeather(toolCall.input); // Add tool result to message history messages.push({ role: 'tool', content: [ { toolName: toolCall.toolName, toolCallId: toolCall.toolCallId, type: 'tool-result', output: { type: 'text', value: toolOutput }, }, ], }); } } } ``` ### Loop Termination The loop continues until the model stops requesting tool calls. You can customize this logic to implement your own termination conditions: ```ts if (finishReason === 'tool-calls') { // Continue the loop } else { // Exit the loop break; } ``` ## Extending This Example ### Custom Loop Control Implement maximum iterations or time limits: ```ts let iterations = 0; const MAX_ITERATIONS = 10; while (iterations < MAX_ITERATIONS) { iterations++; // ... rest of the loop } ``` ### Parallel Tool Execution Execute multiple tools in parallel for better performance: ```ts const toolPromises = toolCalls.map(async toolCall => { if (toolCall.toolName === 'getWeather') { const toolOutput = await getWeather(toolCall.input); return { role: 'tool' as const, content: [ { toolName: toolCall.toolName, toolCallId: toolCall.toolCallId, type: 'tool-result' as const, output: { type: 'text' as const, value: toolOutput }, }, ], }; } // Handle other tools }); const toolResults = await Promise.all(toolPromises); messages.push(...toolResults.filter(Boolean)); ``` This manual approach gives you complete control over the agentic loop while still leveraging the AI SDK's powerful streaming and tool calling capabilities. --- File: /ai/content/cookbook/05-node/56-web-search-agent.mdx --- --- title: Web Search Agent description: Learn how to build an agent that has access to web with the AI SDK and Node tags: ['node', 'tool use', 'agent', 'web'] --- # Web Search Agent There are two approaches you can take to building a web search agent with the AI SDK: 1. Use a model that has native web-searching capabilities 2. Create a tool to access the web and return search results. Both approaches have their advantages and disadvantages. Models with native search capabilities tend to be faster and there is no additional cost to make the search. The disadvantage is that you have less control over what is being searched, and the functionality is limited to models that support it. instead, by creating a tool, you can achieve more flexibility and greater control over your search queries. It allows you to customize your search strategy, specify search parameters, and you can use it with any LLM that supports tool calling. This approach will incur additional costs for the search API you use, but gives you complete control over the search experience. ## Using native web-search There are several models that offer native web-searching capabilities (Perplexity, OpenAI, Gemini). Let's look at how you could build a Web Search Agent across providers. ### OpenAI Responses API OpenAI's Responses API has a built-in web search tool that can be used to search the web and return search results. This tool is called `web_search_preview` and is accessed via the `openai` provider. ```ts import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; const { text, sources } = await generateText({ model: openai.responses('gpt-4o-mini'), prompt: 'What happened in San Francisco last week?', tools: { web_search_preview: openai.tools.webSearchPreview({}), }, }); console.log(text); console.log(sources); ``` ### Perplexity Perplexity's Sonar models combines real-time web search with natural language processing. Each response is grounded in current web data and includes detailed citations. ```ts import { perplexity } from '@ai-sdk/perplexity'; import { generateText } from 'ai'; const { text, sources } = await generateText({ model: perplexity('sonar-pro'), prompt: 'What are the latest developments in quantum computing?', }); console.log(text); console.log(sources); ``` ### Gemini With compatible Gemini models, you can enable search grounding to give the model access to the latest information using Google search. ```ts import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; const { text, sources, providerMetadata } = await generateText({ model: google('gemini-2.5-flash'), tools: { google_search: google.tools.googleSearch({}), }, prompt: 'List the top 5 San Francisco news from the past week.' + 'You must include the date of each article.', }); console.log(text); console.log(sources); // access the grounding metadata. const metadata = providerMetadata?.google; const groundingMetadata = metadata?.groundingMetadata; const safetyRatings = metadata?.safetyRatings; ``` ## Building a web search tool Let's look at how you can build tools that search the web and return results. These tools can be used with any model that supports tool calling, giving you maximum flexibility and control over your search experience. We'll examine several search API options that can be integrated as tools in your agent. Unlike the native web search examples where searching is built into the model, using web search tools requires multiple steps. The language model will make two generations - the first to call the relevant web search tool (extracting search queries from the context), and the second to process the results and generate a response. This multi-step process is handled automatically when you set `stopWhen: stepCountIs()` to a value greater than 1. <Note> By using `stopWhen`, you can automatically send tool results back to the language model alongside the original question, enabling the model to respond with information relevant to the user's query based on the search results. This creates a seamless experience where the agent can search the web and incorporate those findings into its response. </Note> ### Exa [Exa](https://exa.ai/) is a search API designed for AI. Let's look at how you could implement a search tool using Exa: ```ts import { generateText, tool, stepCountIs } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; import Exa from 'exa-js'; export const exa = new Exa(process.env.EXA_API_KEY); export const webSearch = tool({ description: 'Search the web for up-to-date information', inputSchema: z.object({ query: z.string().min(1).max(100).describe('The search query'), }), execute: async ({ query }) => { const { results } = await exa.searchAndContents(query, { livecrawl: 'always', numResults: 3, }); return results.map(result => ({ title: result.title, url: result.url, content: result.text.slice(0, 1000), // take just the first 1000 characters publishedDate: result.publishedDate, })); }, }); const { text } = await generateText({ model: openai('gpt-4o-mini'), // can be any model that supports tools prompt: 'What happened in San Francisco last week?', tools: { webSearch, }, stopWhen: stepCountIs(5), }); ``` ### Firecrawl [Firecrawl](https://firecrawl.dev) provides an API for web scraping and crawling. Let's look at how you could implement a scraping tool using Firecrawl: ```ts import { generateText, tool, stepCountIs } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; import FirecrawlApp from '@mendable/firecrawl-js'; import 'dotenv/config'; const app = new FirecrawlApp({ apiKey: process.env.FIRECRAWL_API_KEY }); export const webSearch = tool({ description: 'Search the web for up-to-date information', inputSchema: z.object({ urlToCrawl: z .string() .url() .min(1) .max(100) .describe('The URL to crawl (including http:// or https://)'), }), execute: async ({ urlToCrawl }) => { const crawlResponse = await app.crawlUrl(urlToCrawl, { limit: 1, scrapeOptions: { formats: ['markdown', 'html'], }, }); if (!crawlResponse.success) { throw new Error(`Failed to crawl: ${crawlResponse.error}`); } return crawlResponse.data; }, }); const main = async () => { const { text } = await generateText({ model: openai('gpt-4o-mini'), // can be any model that supports tools prompt: 'Get the latest blog post from vercel.com/blog', tools: { webSearch, }, stopWhen: stepCountIs(5), }); console.log(text); }; main(); ``` --- File: /ai/content/cookbook/05-node/60-embed-text.mdx --- --- title: Embed Text description: Learn how to embed text using the AI SDK and Node tags: ['node', 'embedding'] --- # Embed Text Text embeddings are numerical representations of text that capture semantic meaning, allowing machines to understand and process language in a mathematical way. These vector representations are crucial for many AI applications, as they enable tasks like semantic search, document similarity comparison, and content recommendation. This example demonstrates how to convert text into embeddings using a text embedding model. The resulting embedding is a high-dimensional vector that represents the semantic meaning of the input text. For a more practical application of embeddings, check out our [RAG example](/cookbook/node/retrieval-augmented-generation) which shows how embeddings can be used for document retrieval. ```ts import { openai } from '@ai-sdk/openai'; import { embed } from 'ai'; import 'dotenv/config'; async function main() { const { embedding, usage } = await embed({ model: openai.textEmbeddingModel('text-embedding-3-small'), value: 'sunny day at the beach', }); console.log(embedding); console.log(usage); } main().catch(console.error); ``` --- File: /ai/content/cookbook/05-node/61-embed-text-batch.mdx --- --- title: Embed Text in Batch description: Learn how to embed multiple text using the AI SDK and Node tags: ['node', 'embedding'] --- # Embed Text in Batch When working with large datasets or multiple pieces of text, processing embeddings one at a time can be inefficient. Batch embedding allows you to convert multiple text inputs into embeddings simultaneously, significantly improving performance and reducing API calls. This is particularly useful when processing documents, chat messages, or any collection of text that needs to be vectorized. This example shows how to embed multiple text inputs in a single operation using the AI SDK. For single text embedding, see our [Embed Text](/cookbook/node/embed-text) example, or for a practical application, check out our [RAG example](/cookbook/node/retrieval-augmented-generation) which demonstrates how batch embeddings can be used in a document retrieval system. ```ts import { openai } from '@ai-sdk/openai'; import { embedMany } from 'ai'; import 'dotenv/config'; async function main() { const { embeddings, usage } = await embedMany({ model: openai.textEmbeddingModel('text-embedding-3-small'), values: [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ], }); console.log(embeddings); console.log(usage); } main().catch(console.error); ``` --- File: /ai/content/cookbook/05-node/70-intercept-fetch-requests.mdx --- --- title: Intercepting Fetch Requests description: Learn how to intercept fetch requests using the AI SDK and Node tags: ['node'] --- # Intercepting Fetch Requests Many providers support setting a custom `fetch` function using the `fetch` argument in their factory function. A custom `fetch` function can be used to intercept and modify requests before they are sent to the provider's API, and to intercept and modify responses before they are returned to the caller. Use cases for intercepting requests include: - Logging requests and responses - Adding authentication headers - Modifying request bodies - Caching responses - Using a custom HTTP client ## Example ```ts file='index.ts' highlight="5-13" import { generateText } from 'ai'; import { createOpenAI } from '@ai-sdk/openai'; const openai = createOpenAI({ // example fetch wrapper that logs the input to the API call: fetch: async (url, options) => { console.log('URL', url); console.log('Headers', JSON.stringify(options!.headers, null, 2)); console.log( `Body ${JSON.stringify(JSON.parse(options!.body! as string), null, 2)}`, ); return await fetch(url, options); }, }); const { text } = await generateText({ model: openai('gpt-4o'), prompt: 'Why is the sky blue?', }); ``` --- File: /ai/content/cookbook/05-node/80-local-caching-middleware.mdx --- --- title: Local Caching Middleware description: Learn how to create a caching middleware for local development. tags: ['streaming', 'caching', 'middleware'] --- # Local Caching Middleware <Note type="warning">This example is not yet updated to v5.</Note> 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. <Note> Make sure to add the path of your local cache to your `.gitignore` so you do not commit it. </Note> ### 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 LanguageModelV2Middleware, 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: LanguageModelV2Middleware = { 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<LanguageModelV1['doGenerate']> > | 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, }), }; } 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')), maxOutputTokens: 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. --- File: /ai/content/cookbook/05-node/index.mdx --- --- title: Node --- --- File: /ai/content/cookbook/15-api-servers/10-node-http-server.mdx --- --- title: Node.js HTTP Server description: Learn how to use the AI SDK in a Node.js HTTP server tags: ['api servers', 'streaming'] --- # Node.js HTTP Server You can use the AI SDK in a Node.js HTTP server to generate text and stream it to the client. ## Examples The examples start a simple HTTP server that listens on port 8080. You can e.g. test it using `curl`: ```bash curl -X POST http://localhost:8080 ``` <Note> The examples use the OpenAI `gpt-4o` model. Ensure that the OpenAI API key is set in the `OPENAI_API_KEY` environment variable. </Note> **Full example**: [github.com/vercel/ai/examples/node-http-server](https://github.com/vercel/ai/tree/main/examples/node-http-server) ### Data Stream You can use the `pipeDataStreamToResponse` method to pipe the stream data to the server response. ```ts filename='index.ts' import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import { createServer } from 'http'; createServer(async (req, res) => { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.pipeDataStreamToResponse(res); }).listen(8080); ``` ### Sending Custom Data `pipeDataStreamToResponse` can be used to send custom data to the client. ```ts filename='index.ts' highlight="6-9,16" import { openai } from '@ai-sdk/openai'; import { pipeDataStreamToResponse, streamText } from 'ai'; import { createServer } from 'http'; createServer(async (req, res) => { // immediately start streaming the response pipeDataStreamToResponse(res, { execute: async dataStreamWriter => { dataStreamWriter.writeData('initialized call'); const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.mergeIntoDataStream(dataStreamWriter); }, onError: error => { // Error messages are masked by default for security reasons. // If you want to expose the error message to the client, you can do so here: return error instanceof Error ? error.message : String(error); }, }); }).listen(8080); ``` ### Text Stream You can send a text stream to the client using `pipeTextStreamToResponse`. ```ts filename='index.ts' import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import { createServer } from 'http'; createServer(async (req, res) => { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.pipeTextStreamToResponse(res); }).listen(8080); ``` ## Troubleshooting - Streaming not working when [proxied](/docs/troubleshooting/streaming-not-working-when-proxied) --- File: /ai/content/cookbook/15-api-servers/20-express.mdx --- --- title: Express description: Learn how to use the AI SDK in an Express server tags: ['api servers', 'streaming'] --- # Express You can use the AI SDK in an [Express](https://expressjs.com/) server to generate and stream text and objects to the client. ## Examples The examples start a simple HTTP server that listens on port 8080. You can e.g. test it using `curl`: ```bash curl -X POST http://localhost:8080 ``` <Note> The examples use the OpenAI `gpt-4o` model. Ensure that the OpenAI API key is set in the `OPENAI_API_KEY` environment variable. </Note> **Full example**: [github.com/vercel/ai/examples/express](https://github.com/vercel/ai/tree/main/examples/express) ### Data Stream You can use the `pipeDataStreamToResponse` method to pipe the stream data to the server response. ```ts filename='index.ts' import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import express, { Request, Response } from 'express'; const app = express(); app.post('/', async (req: Request, res: Response) => { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.pipeDataStreamToResponse(res); }); app.listen(8080, () => { console.log(`Example app listening on port ${8080}`); }); ``` ### Sending Custom Data `pipeDataStreamToResponse` can be used to send custom data to the client. ```ts filename='index.ts' highlight="8-11,18" import { openai } from '@ai-sdk/openai'; import { pipeDataStreamToResponse, streamText } from 'ai'; import express, { Request, Response } from 'express'; const app = express(); app.post('/stream-data', async (req: Request, res: Response) => { // immediately start streaming the response pipeDataStreamToResponse(res, { execute: async dataStreamWriter => { dataStreamWriter.writeData('initialized call'); const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.mergeIntoDataStream(dataStreamWriter); }, onError: error => { // Error messages are masked by default for security reasons. // If you want to expose the error message to the client, you can do so here: return error instanceof Error ? error.message : String(error); }, }); }); app.listen(8080, () => { console.log(`Example app listening on port ${8080}`); }); ``` ### Text Stream You can send a text stream to the client using `pipeTextStreamToResponse`. ```ts filename='index.ts' highlight="13" import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import express, { Request, Response } from 'express'; const app = express(); app.post('/', async (req: Request, res: Response) => { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.pipeTextStreamToResponse(res); }); app.listen(8080, () => { console.log(`Example app listening on port ${8080}`); }); ``` ## Troubleshooting - Streaming not working when [proxied](/docs/troubleshooting/streaming-not-working-when-proxied) --- File: /ai/content/cookbook/15-api-servers/30-hono.mdx --- --- title: Hono description: Example of using the AI SDK in a Hono server. tags: ['api servers', 'streaming'] --- # Hono You can use the AI SDK in a [Hono](https://hono.dev/) server to generate and stream text and objects to the client. ## Examples The examples start a simple HTTP server that listens on port 8080. You can e.g. test it using `curl`: ```bash curl -X POST http://localhost:8080 ``` <Note> The examples use the OpenAI `gpt-4o` model. Ensure that the OpenAI API key is set in the `OPENAI_API_KEY` environment variable. </Note> **Full example**: [github.com/vercel/ai/examples/hono](https://github.com/vercel/ai/tree/main/examples/hono) ### UI Message Stream You can use the `toUIMessageStreamResponse` method to create a properly formatted streaming response. ```ts filename='index.ts' import { openai } from '@ai-sdk/openai'; import { serve } from '@hono/node-server'; import { streamText } from 'ai'; import { Hono } from 'hono'; const app = new Hono(); app.post('/', async c => { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); return result.toUIMessageStreamResponse(); }); serve({ fetch: app.fetch, port: 8080 }); ``` ### Text Stream You can use the `textStream` property to get a text stream from the result and then pipe it to the response. ```ts filename='index.ts' highlight="17" import { openai } from '@ai-sdk/openai'; import { serve } from '@hono/node-server'; import { streamText } from 'ai'; import { Hono } from 'hono'; import { stream } from 'hono/streaming'; const app = new Hono(); app.post('/', async c => { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); c.header('Content-Type', 'text/plain; charset=utf-8'); return stream(c, stream => stream.pipe(result.textStream)); }); serve({ fetch: app.fetch, port: 8080 }); ``` ### Sending Custom Data `createDataStream` can be used to send custom data to the client. ```ts filename='index.ts' highlight="10-13,20" import { openai } from '@ai-sdk/openai'; import { serve } from '@hono/node-server'; import { createDataStream, streamText } from 'ai'; import { Hono } from 'hono'; import { stream } from 'hono/streaming'; const app = new Hono(); app.post('/stream-data', async c => { // immediately start streaming the response const dataStream = createDataStream({ execute: async dataStreamWriter => { dataStreamWriter.writeData('initialized call'); const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.mergeIntoDataStream(dataStreamWriter); }, onError: error => { // Error messages are masked by default for security reasons. // If you want to expose the error message to the client, you can do so here: return error instanceof Error ? error.message : String(error); }, }); // Mark the response as a v1 data stream: c.header('X-Vercel-AI-Data-Stream', 'v1'); c.header('Content-Type', 'text/plain; charset=utf-8'); return stream(c, stream => stream.pipe(dataStream.pipeThrough(new TextEncoderStream())), ); }); serve({ fetch: app.fetch, port: 8080 }); ``` ## Troubleshooting - Streaming not working when [proxied](/docs/troubleshooting/streaming-not-working-when-proxied) --- File: /ai/content/cookbook/15-api-servers/40-fastify.mdx --- --- title: Fastify description: Learn how to use the AI SDK in a Fastify server tags: ['api servers', 'streaming'] --- # Fastify You can use the AI SDK in a [Fastify](https://fastify.dev/) server to generate and stream text and objects to the client. ## Examples The examples start a simple HTTP server that listens on port 8080. You can e.g. test it using `curl`: ```bash curl -X POST http://localhost:8080 ``` <Note> The examples use the OpenAI `gpt-4o` model. Ensure that the OpenAI API key is set in the `OPENAI_API_KEY` environment variable. </Note> **Full example**: [github.com/vercel/ai/examples/fastify](https://github.com/vercel/ai/tree/main/examples/fastify) ### Data Stream You can use the `toDataStream` method to get a data stream from the result and then pipe it to the response. ```ts filename='index.ts' import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import Fastify from 'fastify'; const fastify = Fastify({ logger: true }); fastify.post('/', async function (request, reply) { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); // Mark the response as a v1 data stream: reply.header('X-Vercel-AI-Data-Stream', 'v1'); reply.header('Content-Type', 'text/plain; charset=utf-8'); return reply.send(result.toDataStream({ data })); }); fastify.listen({ port: 8080 }); ``` ### Sending Custom Data `createDataStream` can be used to send custom data to the client. ```ts filename='index.ts' highlight="8-11,18" import { openai } from '@ai-sdk/openai'; import { createDataStream, streamText } from 'ai'; import Fastify from 'fastify'; const fastify = Fastify({ logger: true }); fastify.post('/stream-data', async function (request, reply) { // immediately start streaming the response const dataStream = createDataStream({ execute: async dataStreamWriter => { dataStreamWriter.writeData('initialized call'); const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.mergeIntoDataStream(dataStreamWriter); }, onError: error => { // Error messages are masked by default for security reasons. // If you want to expose the error message to the client, you can do so here: return error instanceof Error ? error.message : String(error); }, }); // Mark the response as a v1 data stream: reply.header('X-Vercel-AI-Data-Stream', 'v1'); reply.header('Content-Type', 'text/plain; charset=utf-8'); return reply.send(dataStream); }); fastify.listen({ port: 8080 }); ``` ### Text Stream You can use the `textStream` property to get a text stream from the result and then pipe it to the response. ```ts filename='index.ts' highlight="15" import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import Fastify from 'fastify'; const fastify = Fastify({ logger: true }); fastify.post('/', async function (request, reply) { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); reply.header('Content-Type', 'text/plain; charset=utf-8'); return reply.send(result.textStream); }); fastify.listen({ port: 8080 }); ``` ## Troubleshooting - Streaming not working when [proxied](/docs/troubleshooting/streaming-not-working-when-proxied) --- File: /ai/content/cookbook/15-api-servers/50-nest.mdx --- --- title: Nest.js description: Learn how to use the AI SDK in a Nest.js server tags: ['api servers', 'streaming'] --- # Nest.js You can use the AI SDK in a [Nest.js](https://nestjs.com/) server to generate and stream text and objects to the client. ## Examples The examples show how to implement a Nest.js controller that uses the AI SDK to stream text and objects to the client. **Full example**: [github.com/vercel/ai/examples/nest](https://github.com/vercel/ai/tree/main/examples/nest) ### Data Stream You can use the `pipeDataStreamToResponse` method to get a data stream from the result and then pipe it to the response. ```ts filename='app.controller.ts' import { Controller, Post, Res } from '@nestjs/common'; import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import { Response } from 'express'; @Controller() export class AppController { @Post() async example(@Res() res: Response) { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.pipeDataStreamToResponse(res); } } ``` ### Sending Custom Data `pipeDataStreamToResponse` can be used to send custom data to the client. ```ts filename='app.controller.ts' highlight="10-12,19" import { Controller, Post, Res } from '@nestjs/common'; import { openai } from '@ai-sdk/openai'; import { pipeDataStreamToResponse, streamText } from 'ai'; import { Response } from 'express'; @Controller() export class AppController { @Post('/stream-data') async streamData(@Res() res: Response) { pipeDataStreamToResponse(res, { execute: async dataStreamWriter => { dataStreamWriter.writeData('initialized call'); const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.mergeIntoDataStream(dataStreamWriter); }, onError: error => { // Error messages are masked by default for security reasons. // If you want to expose the error message to the client, you can do so here: return error instanceof Error ? error.message : String(error); }, }); } } ``` ### Text Stream You can use the `pipeTextStreamToResponse` method to get a text stream from the result and then pipe it to the response. ```ts filename='app.controller.ts' highlight="15" import { Controller, Post, Res } from '@nestjs/common'; import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import { Response } from 'express'; @Controller() export class AppController { @Post() async example(@Res() res: Response) { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.pipeTextStreamToResponse(res); } } ``` ## Troubleshooting - Streaming not working when [proxied](/docs/troubleshooting/streaming-not-working-when-proxied) --- File: /ai/content/cookbook/15-api-servers/index.mdx --- --- title: API Servers --- --- File: /ai/content/cookbook/20-rsc/10-generate-text.mdx --- --- title: Generate Text description: Learn how to generate text using the AI SDK and React Server Components. tags: ['rsc'] --- # Generate Text <Note> This example uses React Server Components (RSC). If you want to client side rendering and hooks instead, check out the ["generate text" example with useState](/examples/next-pages/basics/generating-text). </Note> A situation may arise when you need to generate text based on a prompt. For example, you may want to generate a response to a question or summarize a body of text. The `generateText` function can be used to generate text based on the input prompt. <Browser> <TextGeneration /> </Browser> ## Client Let's create a simple React component that will call the `getAnswer` function when a button is clicked. The `getAnswer` function will call the `generateText` function from the `ai` module, which will then generate text based on the input prompt. ```tsx filename="app/page.tsx" 'use client'; import { useState } from 'react'; import { getAnswer } from './actions'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export default function Home() { const [generation, setGeneration] = useState<string>(''); return ( <div> <button onClick={async () => { const { text } = await getAnswer('Why is the sky blue?'); setGeneration(text); }} > Answer </button> <div>{generation}</div> </div> ); } ``` ## Server On the server side, we need to implement the `getAnswer` function, which will call the `generateText` function from the `ai` module. The `generateText` function will generate text based on the input prompt. ```typescript filename='app/actions.ts' 'use server'; import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; export async function getAnswer(question: string) { const { text, finishReason, usage } = await generateText({ model: openai('gpt-3.5-turbo'), prompt: question, }); return { text, finishReason, usage }; } ``` --- File: /ai/content/cookbook/20-rsc/11-generate-text-with-chat-prompt.mdx --- --- title: Generate Text with Chat Prompt description: Learn how to generate text with chat prompt using the AI SDK and React Server Components. tags: ['rsc', 'chat'] --- # Generate Text with Chat Prompt Previously, we were able to generate text and objects using either a single message prompt, a system prompt, or a combination of both of them. However, there may be times when you want to generate text based on a series of messages. A chat completion allows you to generate text based on a series of messages. This series of messages can be any series of interactions between any number of systems, but the most popular and relatable use case has been a series of messages that represent a conversation between a user and a model. <Browser> <ChatGeneration history={[ { role: 'User', content: 'How is it going?' }, { role: 'Assistant', content: 'All good, how may I help you?' }, ]} inputMessage={{ role: 'User', content: 'Why is the sky blue?' }} outputMessage={{ role: 'Assistant', content: 'The sky is blue because of rayleigh scattering.', }} /> </Browser> ## Client Let's create a simple conversation between a user and a model, and place a button that will call `continueConversation`. ```tsx filename='app/page.tsx' 'use client'; import { useState } from 'react'; import { Message, continueConversation } from './actions'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export default function Home() { const [conversation, setConversation] = useState<Message[]>([]); const [input, setInput] = useState<string>(''); return ( <div> <div> {conversation.map((message, index) => ( <div key={index}> {message.role}: {message.content} </div> ))} </div> <div> <input type="text" value={input} onChange={event => { setInput(event.target.value); }} /> <button onClick={async () => { const { messages } = await continueConversation([ ...conversation, { role: 'user', content: input }, ]); setConversation(messages); }} > Send Message </button> </div> </div> ); } ``` ## Server Now, let's implement the `continueConversation` function that will insert the user's message into the conversation and generate a response. ```typescript filename='app/actions.ts' 'use server'; import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; export interface Message { role: 'user' | 'assistant'; content: string; } export async function continueConversation(history: Message[]) { 'use server'; const { text } = await generateText({ model: openai('gpt-3.5-turbo'), system: 'You are a friendly assistant!', messages: history, }); return { messages: [ ...history, { role: 'assistant' as const, content: text, }, ], }; } ``` --- File: /ai/content/cookbook/20-rsc/20-stream-text.mdx --- --- title: Stream Text description: Learn how to stream text using the AI SDK and React Server Components. tags: ['rsc', 'streaming'] --- # Stream Text <Note> This example uses React Server Components (RSC). If you want to client side rendering and hooks instead, check out the ["stream text" example with useCompletion](/examples/next-pages/basics/streaming-text-generation). </Note> Text generation can sometimes take a long time to complete, especially when you're generating a couple of paragraphs. In such cases, it is useful to stream the text generation process to the client in real-time. This allows the client to display the generated text as it is being generated, rather than have users wait for it to complete before displaying the result. <Browser> <TextGeneration stream /> </Browser> ## Client Let's create a simple React component that will call the `generate` function when a button is clicked. The `generate` function will call the `streamText` function, which will then generate text based on the input prompt. To consume the stream of text in the client, we will use the `readStreamableValue` function from the `@ai-sdk/rsc` module. ```tsx filename="app/page.tsx" 'use client'; import { useState } from 'react'; import { generate } from './actions'; import { readStreamableValue } from '@ai-sdk/rsc'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export default function Home() { const [generation, setGeneration] = useState<string>(''); return ( <div> <button onClick={async () => { const { output } = await generate('Why is the sky blue?'); for await (const delta of readStreamableValue(output)) { setGeneration(currentGeneration => `${currentGeneration}${delta}`); } }} > Ask </button> <div>{generation}</div> </div> ); } ``` ## Server On the server side, we need to implement the `generate` function, which will call the `streamText` function. The `streamText` function will generate text based on the input prompt. In order to stream the text generation to the client, we will use `createStreamableValue` that can wrap any changeable value and stream it to the client. Using DevTools, we can see the text generation being streamed to the client in real-time. ```typescript filename='app/actions.ts' 'use server'; import { streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; import { createStreamableValue } from '@ai-sdk/rsc'; export async function generate(input: string) { const stream = createStreamableValue(''); (async () => { const { textStream } = streamText({ model: openai('gpt-3.5-turbo'), prompt: input, }); for await (const delta of textStream) { stream.update(delta); } stream.done(); })(); return { output: stream.value }; } ``` --- File: /ai/content/cookbook/20-rsc/21-stream-text-with-chat-prompt.mdx --- --- title: Stream Text with Chat Prompt description: Learn how to stream text with chat prompt using the AI SDK and React Server Components. tags: ['rsc', 'chat'] --- # Stream Text with Chat Prompt Chat completion can sometimes take a long time to finish, especially when the response is big. In such cases, it is useful to stream the chat completion to the client in real-time. This allows the client to display the new message as it is being generated by the model, rather than have users wait for it to finish. <Browser> <ChatGeneration stream history={[ { role: 'User', content: 'How is it going?' }, { role: 'Assistant', content: 'All good, how may I help you?' }, ]} inputMessage={{ role: 'User', content: 'Why is the sky blue?' }} outputMessage={{ role: 'Assistant', content: 'The sky is blue because of rayleigh scattering.', }} /> </Browser> ## Client Let's create a simple conversation between a user and a model, and place a button that will call `continueConversation`. ```tsx filename='app/page.tsx' 'use client'; import { useState } from 'react'; import { Message, continueConversation } from './actions'; import { readStreamableValue } from '@ai-sdk/rsc'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export default function Home() { const [conversation, setConversation] = useState<Message[]>([]); const [input, setInput] = useState<string>(''); return ( <div> <div> {conversation.map((message, index) => ( <div key={index}> {message.role}: {message.content} </div> ))} </div> <div> <input type="text" value={input} onChange={event => { setInput(event.target.value); }} /> <button onClick={async () => { const { messages, newMessage } = await continueConversation([ ...conversation, { role: 'user', content: input }, ]); let textContent = ''; for await (const delta of readStreamableValue(newMessage)) { textContent = `${textContent}${delta}`; setConversation([ ...messages, { role: 'assistant', content: textContent }, ]); } }} > Send Message </button> </div> </div> ); } ``` ## Server Now, let's implement the `continueConversation` function that will insert the user's message into the conversation and stream back the new message. ```typescript filename='app/actions.ts' 'use server'; import { streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; import { createStreamableValue } from '@ai-sdk/rsc'; export interface Message { role: 'user' | 'assistant'; content: string; } export async function continueConversation(history: Message[]) { 'use server'; const stream = createStreamableValue(); (async () => { const { textStream } = streamText({ model: openai('gpt-3.5-turbo'), system: "You are a dude that doesn't drop character until the DVD commentary.", messages: history, }); for await (const text of textStream) { stream.update(text); } stream.done(); })(); return { messages: history, newMessage: stream.value, }; } ``` --- File: /ai/content/cookbook/20-rsc/30-generate-object.mdx --- --- title: Generate Object description: Learn how to generate object using the AI SDK and React Server Components. tags: ['rsc', 'structured data'] --- # Generate Object <Note> This example uses React Server Components (RSC). If you want to client side rendering and hooks instead, check out the ["generate object" example with useState](/examples/next-pages/basics/generating-object). </Note> Earlier functions like `generateText` and `streamText` gave us the ability to generate unstructured text. However, if you want to generate structured data like JSON, you can provide a schema that describes the structure of your desired object to the `generateObject` function. The function requires you to provide a schema using [zod](https://zod.dev), a library for defining schemas for JavaScript objects. By using zod, you can also use it to validate the generated object and ensure that it conforms to the specified structure. <Browser> <ObjectGeneration object={{ notifications: [ { name: 'Jamie Roberts', message: "Hey! How's the study grind going? Need a coffee boost?", minutesAgo: 15, }, { name: 'Prof. Morgan', message: 'Reminder: Your term paper is due promptly at 8 AM tomorrow. Please ensure it meets the submission guidelines outlined.', minutesAgo: 46, }, { name: 'Alex Chen', message: "Dude, urgent! Borrow your notes for tomorrow's exam? I swear mine got eaten by my dog!", minutesAgo: 30, }, ], }} /> </Browser> ## Client Let's create a simple React component that will call the `getNotifications` function when a button is clicked. The function will generate a list of notifications as described in the schema. ```tsx filename='app/page.tsx' 'use client'; import { useState } from 'react'; import { getNotifications } from './actions'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export default function Home() { const [generation, setGeneration] = useState<string>(''); return ( <div> <button onClick={async () => { const { notifications } = await getNotifications( 'Messages during finals week.', ); setGeneration(JSON.stringify(notifications, null, 2)); }} > View Notifications </button> <pre>{generation}</pre> </div> ); } ``` ## Server Now let's implement the `getNotifications` function. We'll use the `generateObject` function to generate the list of notifications based on the schema we defined earlier. ```typescript filename='app/actions.ts' 'use server'; import { generateObject } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; export async function getNotifications(input: string) { 'use server'; const { object: notifications } = await generateObject({ model: openai('gpt-4.1'), system: 'You generate three notifications for a messages app.', prompt: input, schema: z.object({ notifications: z.array( z.object({ name: z.string().describe('Name of a fictional person.'), message: z.string().describe('Do not use emojis or links.'), minutesAgo: z.number(), }), ), }), }); return { notifications }; } ``` --- File: /ai/content/cookbook/20-rsc/40-stream-object.mdx --- --- title: Stream Object description: Learn how to stream object using the AI SDK and React Server Components. tags: ['rsc', 'streaming', 'structured data'] --- # Stream Object <Note> This example uses React Server Components (RSC). If you want to client side rendering and hooks instead, check out the ["streaming object generation" example with useObject](/examples/next-pages/basics/streaming-object-generation). </Note> Object generation can sometimes take a long time to complete, especially when you're generating a large schema. In such cases, it is useful to stream the object generation process to the client in real-time. This allows the client to display the generated object as it is being generated, rather than have users wait for it to complete before displaying the result. <Browser> <ObjectGeneration stream object={{ notifications: [ { name: 'Jamie Roberts', message: "Hey! How's the study grind going? Need a coffee boost?", minutesAgo: 15, }, { name: 'Prof. Morgan', message: 'Reminder: Your term paper is due promptly at 8 AM tomorrow. Please ensure it meets the submission guidelines outlined.', minutesAgo: 46, }, { name: 'Alex Chen', message: "Dude, urgent! Borrow your notes for tomorrow's exam? I swear mine got eaten by my dog!", minutesAgo: 30, }, ], }} /> </Browser> ## Client Let's create a simple React component that will call the `getNotifications` function when a button is clicked. The function will generate a list of notifications as described in the schema. ```tsx filename='app/page.tsx' 'use client'; import { useState } from 'react'; import { generate } from './actions'; import { readStreamableValue } from '@ai-sdk/rsc'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export default function Home() { const [generation, setGeneration] = useState<string>(''); return ( <div> <button onClick={async () => { const { object } = await generate('Messages during finals week.'); for await (const partialObject of readStreamableValue(object)) { if (partialObject) { setGeneration( JSON.stringify(partialObject.notifications, null, 2), ); } } }} > Ask </button> <pre>{generation}</pre> </div> ); } ``` ## Server Now let's implement the `getNotifications` function. We'll use the `generateObject` function to generate the list of fictional notifications based on the schema we defined earlier. ```typescript filename='app/actions.ts' 'use server'; import { streamObject } from 'ai'; import { openai } from '@ai-sdk/openai'; import { createStreamableValue } from '@ai-sdk/rsc'; import { z } from 'zod'; export async function generate(input: string) { 'use server'; const stream = createStreamableValue(); (async () => { const { partialObjectStream } = streamObject({ model: openai('gpt-4.1'), system: 'You generate three notifications for a messages app.', prompt: input, schema: z.object({ notifications: z.array( z.object({ name: z.string().describe('Name of a fictional person.'), message: z.string().describe('Do not use emojis or links.'), minutesAgo: z.number(), }), ), }), }); for await (const partialObject of partialObjectStream) { stream.update(partialObject); } stream.done(); })(); return { object: stream.value }; } ``` --- File: /ai/content/cookbook/20-rsc/50-call-tools.mdx --- --- title: Call Tools description: Learn how to call tools using the AI SDK and React Server Components. tags: ['rsc', 'tool use'] --- # Call Tools Some models allow developers to provide a list of tools that can be called at any time during a generation. This is useful for extending the capabilities of a language model to either use logic or data to interact with systems external to the model. <Browser> <ChatGeneration history={[ { role: 'User', content: 'How is it going?' }, { role: 'Assistant', content: 'All good, how may I help you?' }, ]} inputMessage={{ role: 'User', content: 'What is 24 celsius in fahrenheit?', }} outputMessage={{ role: 'Assistant', content: '24°C is 75.20°F', }} /> </Browser> ## Client Let's create a simple conversation between a user and model and place a button that will call `continueConversation`. ```tsx filename='app/page.tsx' 'use client'; import { useState } from 'react'; import { Message, continueConversation } from './actions'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export default function Home() { const [conversation, setConversation] = useState<Message[]>([]); const [input, setInput] = useState<string>(''); return ( <div> <div> {conversation.map((message, index) => ( <div key={index}> {message.role}: {message.content} </div> ))} </div> <div> <input type="text" value={input} onChange={event => { setInput(event.target.value); }} /> <button onClick={async () => { const { messages } = await continueConversation([ ...conversation, { role: 'user', content: input }, ]); setConversation(messages); }} > Send Message </button> </div> </div> ); } ``` ## Server Now, let's implement the `continueConversation` action that uses `generateText` to generate a response to the user's question. We will use the [`tools`](/docs/reference/ai-sdk-core/generate-text#tools) parameter to specify our own function called `celsiusToFahrenheit` that will convert a user given value in celsius to fahrenheit. We will use zod to specify the schema for the `celsiusToFahrenheit` function's parameters. ```tsx filename='app/actions.ts' 'use server'; import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; export interface Message { role: 'user' | 'assistant'; content: string; } export async function continueConversation(history: Message[]) { 'use server'; const { text, toolResults } = await generateText({ model: openai('gpt-3.5-turbo'), system: 'You are a friendly assistant!', messages: history, tools: { celsiusToFahrenheit: { description: 'Converts celsius to fahrenheit', inputSchema: z.object({ value: z.string().describe('The value in celsius'), }), execute: async ({ value }) => { const celsius = parseFloat(value); const fahrenheit = celsius * (9 / 5) + 32; return `${celsius}°C is ${fahrenheit.toFixed(2)}°F`; }, }, }, }); return { messages: [ ...history, { role: 'assistant' as const, content: text || toolResults.map(toolResult => toolResult.result).join('\n'), }, ], }; } ``` --- File: /ai/content/cookbook/20-rsc/51-call-tools-in-parallel.mdx --- --- title: Call Tools in Parallel description: Learn how to tools in parallel text using the AI SDK and React Server Components. tags: ['rsc', 'tool use'] --- # Call Tools in Parallel Some language models support calling tools in parallel. This is particularly useful when multiple tools are independent of each other and can be executed in parallel during the same generation step. <Browser> <ChatGeneration history={[ { role: 'User', content: 'How is it going?' }, { role: 'Assistant', content: 'All good, how may I help you?' }, ]} inputMessage={{ role: 'User', content: 'What is the weather in Paris and New York?', }} outputMessage={{ role: 'Assistant', content: 'The weather is 24°C in New York and 25°C in Paris. It is sunny in both cities.', }} /> </Browser> ## Client Let's modify our previous example to call `getWeather` tool for multiple cities in parallel. ```tsx filename='app/page.tsx' 'use client'; import { useState } from 'react'; import { Message, continueConversation } from './actions'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export default function Home() { const [conversation, setConversation] = useState<Message[]>([]); const [input, setInput] = useState<string>(''); return ( <div> <div> {conversation.map((message, index) => ( <div key={index}> {message.role}: {message.content} </div> ))} </div> <div> <input type="text" value={input} onChange={event => { setInput(event.target.value); }} /> <button onClick={async () => { const { messages } = await continueConversation([ ...conversation, { role: 'user', content: input }, ]); setConversation(messages); }} > Send Message </button> </div> </div> ); } ``` ## Server Let's update the tools object to now use the `getWeather` function instead. ```ts filename='app/actions.ts' 'use server'; import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; export interface Message { role: 'user' | 'assistant'; content: string; } function getWeather({ city, unit }) { // This function would normally make an // API request to get the weather. return { value: 25, description: 'Sunny' }; } export async function continueConversation(history: Message[]) { 'use server'; const { text, toolResults } = await generateText({ model: openai('gpt-3.5-turbo'), system: 'You are a friendly weather assistant!', messages: history, tools: { getWeather: { description: 'Get the weather for a location', inputSchema: z.object({ city: z.string().describe('The city to get the weather for'), unit: z .enum(['C', 'F']) .describe('The unit to display the temperature in'), }), execute: async ({ city, unit }) => { const weather = getWeather({ city, unit }); return `It is currently ${weather.value}°${unit} and ${weather.description} in ${city}!`; }, }, }, }); return { messages: [ ...history, { role: 'assistant' as const, content: text || toolResults.map(toolResult => toolResult.result).join('\n'), }, ], }; } ``` --- File: /ai/content/cookbook/20-rsc/60-save-messages-to-database.mdx --- --- title: Save Messages To Database description: Learn how to save messages to an external database using the AI SDK and React Server Components tags: ['rsc', 'tool use'] --- # Save Messages To Database Sometimes conversations with language models can get interesting and you might want to save the state of so you can revisit it or continue the conversation later. `createAI` has an experimental callback function called `onSetAIState` that gets called whenever the AI state changes. You can use this to save the AI state to a file or a database. ## Client ```tsx filename='app/layout.tsx' import { ServerMessage } from './actions'; import { AI } from './ai'; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { // get chat history from database const history: ServerMessage[] = getChat(); return ( <html lang="en"> <body> <AI initialAIState={history} initialUIState={[]}> {children} </AI> </body> </html> ); } ``` ```tsx filename='app/page.tsx' 'use client'; import { useState } from 'react'; import { ClientMessage } from './actions'; import { useActions, useUIState } from '@ai-sdk/rsc'; import { generateId } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export default function Home() { const [input, setInput] = useState<string>(''); const [conversation, setConversation] = useUIState(); const { continueConversation } = useActions(); return ( <div> <div> {conversation.map((message: ClientMessage) => ( <div key={message.id}> {message.role}: {message.display} </div> ))} </div> <div> <input type="text" value={input} onChange={event => { setInput(event.target.value); }} /> <button onClick={async () => { setConversation((currentConversation: ClientMessage[]) => [ ...currentConversation, { id: generateId(), role: 'user', display: input }, ]); const message = await continueConversation(input); setConversation((currentConversation: ClientMessage[]) => [ ...currentConversation, message, ]); }} > Send Message </button> </div> </div> ); } ``` ## Server We will use the callback function to listen to state changes and save the conversation once we receive a `done` event. ```tsx filename='app/actions.tsx' 'use server'; import { getAIState, getMutableAIState, streamUI } from '@ai-sdk/rsc'; import { openai } from '@ai-sdk/openai'; import { ReactNode } from 'react'; import { z } from 'zod'; import { generateId } from 'ai'; import { Stock } from '@ai-studio/components/stock'; export interface ServerMessage { role: 'user' | 'assistant' | 'function'; content: string; } export interface ClientMessage { id: string; role: 'user' | 'assistant' | 'function'; display: ReactNode; } export async function continueConversation( input: string, ): Promise<ClientMessage> { 'use server'; const history = getMutableAIState(); const result = await streamUI({ model: openai('gpt-3.5-turbo'), messages: [...history.get(), { role: 'user', content: input }], text: ({ content, done }) => { if (done) { history.done([ ...history.get(), { role: 'user', content: input }, { role: 'assistant', content }, ]); } return <div>{content}</div>; }, tools: { showStockInformation: { description: 'Get stock information for symbol for the last numOfMonths months', inputSchema: z.object({ symbol: z .string() .describe('The stock symbol to get information for'), numOfMonths: z .number() .describe('The number of months to get historical information for'), }), generate: async ({ symbol, numOfMonths }) => { history.done([ ...history.get(), { role: 'function', name: 'showStockInformation', content: JSON.stringify({ symbol, numOfMonths }), }, ]); return <Stock symbol={symbol} numOfMonths={numOfMonths} />; }, }, }, }); return { id: generateId(), role: 'assistant', display: result.value, }; } ``` ```ts filename='app/ai.ts' import { createAI } from '@ai-sdk/rsc'; import { ServerMessage, ClientMessage, continueConversation } from './actions'; export const AI = createAI<ServerMessage[], ClientMessage[]>({ actions: { continueConversation, }, onSetAIState: async ({ state, done }) => { 'use server'; if (done) { saveChat(state); } }, onGetUIState: async () => { 'use server'; const history: ServerMessage[] = getAIState(); return history.map(({ role, content }) => ({ id: generateId(), role, display: role === 'function' ? <Stock {...JSON.parse(content)} /> : content, })); }, }); ``` --- File: /ai/content/cookbook/20-rsc/61-restore-messages-from-database.mdx --- --- title: Restore Messages From Database description: Learn how to restore messages from an external database using the AI SDK and React Server Components tags: ['rsc', 'tool use'] --- # Restore Messages from Database When building AI applications, you might want to restore previous conversations from a database to allow users to continue their conversations or review past interactions. The AI SDK provides mechanisms to restore conversation state through `initialAIState` and `onGetUIState`. ## Client ```tsx filename='app/layout.tsx' import { ServerMessage } from './actions'; import { AI } from './ai'; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { // Fetch stored messages from your database const savedMessages: ServerMessage[] = getSavedMessages(); return ( <html lang="en"> <body> <AI initialAIState={savedMessages} initialUIState={[]}> {children} </AI> </body> </html> ); } ``` ```tsx filename='app/page.tsx' 'use client'; import { useState, useEffect } from 'react'; import { ClientMessage } from './actions'; import { useActions, useUIState } from '@ai-sdk/rsc'; import { generateId } from 'ai'; export default function Home() { const [conversation, setConversation] = useUIState(); const [input, setInput] = useState<string>(''); const { continueConversation } = useActions(); return ( <div> <div className="conversation-history"> {conversation.map((message: ClientMessage) => ( <div key={message.id} className={`message ${message.role}`}> {message.role}: {message.display} </div> ))} </div> <div className="input-area"> <input type="text" value={input} onChange={e => setInput(e.target.value)} placeholder="Type your message..." /> <button onClick={async () => { // Add user message to UI setConversation((currentConversation: ClientMessage[]) => [ ...currentConversation, { id: generateId(), role: 'user', display: input }, ]); // Get AI response const message = await continueConversation(input); // Add AI response to UI setConversation((currentConversation: ClientMessage[]) => [ ...currentConversation, message, ]); setInput(''); }} > Send </button> </div> </div> ); } ``` ## Server The server-side implementation handles the restoration of messages and their transformation into the appropriate format for display. ```tsx filename='app/ai.ts' import { createAI } from '@ai-sdk/rsc'; import { ServerMessage, ClientMessage, continueConversation } from './actions'; import { Stock } from '@ai-studio/components/stock'; import { generateId } from 'ai'; export const AI = createAI<ServerMessage[], ClientMessage[]>({ actions: { continueConversation, }, onGetUIState: async () => { 'use server'; // Get the current AI state (stored messages) const history: ServerMessage[] = getAIState(); // Transform server messages into client messages return history.map(({ role, content }) => ({ id: generateId(), role, display: role === 'function' ? <Stock {...JSON.parse(content)} /> : content, })); }, }); ``` ```tsx filename='app/actions.tsx' 'use server'; import { getAIState } from '@ai-sdk/rsc'; export interface ServerMessage { role: 'user' | 'assistant' | 'function'; content: string; } export interface ClientMessage { id: string; role: 'user' | 'assistant' | 'function'; display: ReactNode; } // Function to get saved messages from database export async function getSavedMessages(): Promise<ServerMessage[]> { 'use server'; // Implement your database fetching logic here return await fetchMessagesFromDatabase(); } ``` --- File: /ai/content/cookbook/20-rsc/90-render-visual-interface-in-chat.mdx --- --- title: Render Visual Interface in Chat description: Learn how to generate text using the AI SDK and React Server Components. tags: ['rsc', 'generative user interface'] --- # Render Visual Interface in Chat We've now seen how a language model can call a function and render a component based on a conversation with the user. When we define multiple functions in [`tools`](/docs/reference/ai-sdk-core/generate-text#tools), it is possible for the model to reason out the right functions to call based on whatever the user's intent is. This means that you can write a bunch of functions without the burden of implementing complex routing logic to run them. ## Client ```tsx filename='app/page.tsx' 'use client'; import { useState } from 'react'; import { ClientMessage } from './actions'; import { useActions, useUIState } from '@ai-sdk/rsc'; import { generateId } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export default function Home() { const [input, setInput] = useState<string>(''); const [conversation, setConversation] = useUIState(); const { continueConversation } = useActions(); return ( <div> <div> {conversation.map((message: ClientMessage) => ( <div key={message.id}> {message.role}: {message.display} </div> ))} </div> <div> <input type="text" value={input} onChange={event => { setInput(event.target.value); }} /> <button onClick={async () => { setConversation((currentConversation: ClientMessage[]) => [ ...currentConversation, { id: generateId(), role: 'user', display: input }, ]); const message = await continueConversation(input); setConversation((currentConversation: ClientMessage[]) => [ ...currentConversation, message, ]); }} > Send Message </button> </div> </div> ); } ``` ```tsx filename='components/stock.tsx' export async function Stock({ symbol, numOfMonths }) { const data = await fetch( `https://api.example.com/stock/${symbol}/${numOfMonths}`, ); return ( <div> <div>{symbol}</div> <div> {data.timeline.map(data => ( <div> <div>{data.date}</div> <div>{data.value}</div> </div> ))} </div> </div> ); } ``` ```tsx filename='components/flight.tsx' export async function Flight({ flightNumber }) { const data = await fetch(`https://api.example.com/flight/${flightNumber}`); return ( <div> <div>{flightNumber}</div> <div>{data.status}</div> <div>{data.source}</div> <div>{data.destination}</div> </div> ); } ``` ## Server ```tsx filename='app/actions.tsx' 'use server'; import { getMutableAIState, streamUI } from '@ai-sdk/rsc'; import { openai } from '@ai-sdk/openai'; import { ReactNode } from 'react'; import { z } from 'zod'; import { generateId } from 'ai'; import { Stock } from '@/components/stock'; import { Flight } from '@/components/flight'; export interface ServerMessage { role: 'user' | 'assistant'; content: string; } export interface ClientMessage { id: string; role: 'user' | 'assistant'; display: ReactNode; } export async function continueConversation( input: string, ): Promise<ClientMessage> { 'use server'; const history = getMutableAIState(); const result = await streamUI({ model: openai('gpt-3.5-turbo'), messages: [...history.get(), { role: 'user', content: input }], text: ({ content, done }) => { if (done) { history.done((messages: ServerMessage[]) => [ ...messages, { role: 'assistant', content }, ]); } return <div>{content}</div>; }, tools: { showStockInformation: { description: 'Get stock information for symbol for the last numOfMonths months', inputSchema: z.object({ symbol: z .string() .describe('The stock symbol to get information for'), numOfMonths: z .number() .describe('The number of months to get historical information for'), }), generate: async ({ symbol, numOfMonths }) => { history.done((messages: ServerMessage[]) => [ ...messages, { role: 'assistant', content: `Showing stock information for ${symbol}`, }, ]); return <Stock symbol={symbol} numOfMonths={numOfMonths} />; }, }, showFlightStatus: { description: 'Get the status of a flight', inputSchema: z.object({ flightNumber: z .string() .describe('The flight number to get status for'), }), generate: async ({ flightNumber }) => { history.done((messages: ServerMessage[]) => [ ...messages, { role: 'assistant', content: `Showing flight status for ${flightNumber}`, }, ]); return <Flight flightNumber={flightNumber} />; }, }, }, }); return { id: generateId(), role: 'assistant', display: result.value, }; } ``` ```typescript filename='app/ai.ts' import { createAI } from '@ai-sdk/rsc'; import { ServerMessage, ClientMessage, continueConversation } from './actions'; export const AI = createAI<ServerMessage[], ClientMessage[]>({ actions: { continueConversation, }, initialAIState: [], initialUIState: [], }); ``` --- File: /ai/content/cookbook/20-rsc/91-stream-updates-to-visual-interfaces.mdx --- --- title: Stream Updates to Visual Interfaces description: Learn how to generate text using the AI SDK and React Server Components. tags: ['rsc', 'streaming', 'generative user interface'] --- # Stream Updates to Visual Interfaces In our previous example we've been streaming react components from the server to the client. By streaming the components, we open up the possibility to update these components based on state changes that occur in the server. ## Client ```tsx filename='app/page.tsx' 'use client'; import { useState } from 'react'; import { ClientMessage } from './actions'; import { useActions, useUIState } from '@ai-sdk/rsc'; import { generateId } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export default function Home() { const [input, setInput] = useState<string>(''); const [conversation, setConversation] = useUIState(); const { continueConversation } = useActions(); return ( <div> <div> {conversation.map((message: ClientMessage) => ( <div key={message.id}> {message.role}: {message.display} </div> ))} </div> <div> <input type="text" value={input} onChange={event => { setInput(event.target.value); }} /> <button onClick={async () => { setConversation((currentConversation: ClientMessage[]) => [ ...currentConversation, { id: generateId(), role: 'user', display: input }, ]); const message = await continueConversation(input); setConversation((currentConversation: ClientMessage[]) => [ ...currentConversation, message, ]); }} > Send Message </button> </div> </div> ); } ``` ## Server ```tsx filename='app/actions.tsx' 'use server'; import { getMutableAIState, streamUI } from '@ai-sdk/rsc'; import { openai } from '@ai-sdk/openai'; import { ReactNode } from 'react'; import { z } from 'zod'; import { generateId } from 'ai'; export interface ServerMessage { role: 'user' | 'assistant'; content: string; } export interface ClientMessage { id: string; role: 'user' | 'assistant'; display: ReactNode; } export async function continueConversation( input: string, ): Promise<ClientMessage> { 'use server'; const history = getMutableAIState(); const result = await streamUI({ model: openai('gpt-3.5-turbo'), messages: [...history.get(), { role: 'user', content: input }], text: ({ content, done }) => { if (done) { history.done((messages: ServerMessage[]) => [ ...messages, { role: 'assistant', content }, ]); } return <div>{content}</div>; }, tools: { deploy: { description: 'Deploy repository to vercel', inputSchema: z.object({ repositoryName: z .string() .describe('The name of the repository, example: vercel/ai-chatbot'), }), generate: async function* ({ repositoryName }) { yield <div>Cloning repository {repositoryName}...</div>; // [!code highlight:5] await new Promise(resolve => setTimeout(resolve, 3000)); yield <div>Building repository {repositoryName}...</div>; await new Promise(resolve => setTimeout(resolve, 2000)); return <div>{repositoryName} deployed!</div>; }, }, }, }); return { id: generateId(), role: 'assistant', display: result.value, }; } ``` ```typescript filename='app/ai.ts' import { createAI } from '@ai-sdk/rsc'; import { ServerMessage, ClientMessage, continueConversation } from './actions'; export const AI = createAI<ServerMessage[], ClientMessage[]>({ actions: { continueConversation, }, initialAIState: [], initialUIState: [], }); ``` --- File: /ai/content/cookbook/20-rsc/92-stream-ui-record-token-usage.mdx --- --- title: Record Token Usage after Streaming User Interfaces description: Learn how to record token usage after streaming user interfaces using the AI SDK and React Server Components tags: ['rsc', 'usage'] --- # Record Token Usage after Streaming User Interfaces When you're streaming structured data with [`streamUI`](/docs/reference/ai-sdk-rsc/stream-ui), you may want to record the token usage for billing purposes. ## `onFinish` Callback You can use the `onFinish` callback to record token usage. It is called when the stream is finished. ```tsx filename='app/page.tsx' 'use client'; import { useState } from 'react'; import { ClientMessage } from './actions'; import { useActions, useUIState } from '@ai-sdk/rsc'; import { generateId } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export default function Home() { const [input, setInput] = useState<string>(''); const [conversation, setConversation] = useUIState(); const { continueConversation } = useActions(); return ( <div> <div> {conversation.map((message: ClientMessage) => ( <div key={message.id}> {message.role}: {message.display} </div> ))} </div> <div> <input type="text" value={input} onChange={event => { setInput(event.target.value); }} /> <button onClick={async () => { setConversation((currentConversation: ClientMessage[]) => [ ...currentConversation, { id: generateId(), role: 'user', display: input }, ]); const message = await continueConversation(input); setConversation((currentConversation: ClientMessage[]) => [ ...currentConversation, message, ]); }} > Send Message </button> </div> </div> ); } ``` ## Server ```tsx filename='app/actions.tsx' highlight={"57-63"} 'use server'; import { createAI, getMutableAIState, streamUI } from '@ai-sdk/rsc'; import { openai } from '@ai-sdk/openai'; import { ReactNode } from 'react'; import { z } from 'zod'; import { generateId } from 'ai'; export interface ServerMessage { role: 'user' | 'assistant'; content: string; } export interface ClientMessage { id: string; role: 'user' | 'assistant'; display: ReactNode; } export async function continueConversation( input: string, ): Promise<ClientMessage> { 'use server'; const history = getMutableAIState(); const result = await streamUI({ model: openai('gpt-3.5-turbo'), messages: [...history.get(), { role: 'user', content: input }], text: ({ content, done }) => { if (done) { history.done((messages: ServerMessage[]) => [ ...messages, { role: 'assistant', content }, ]); } return <div>{content}</div>; }, tools: { deploy: { description: 'Deploy repository to vercel', inputSchema: z.object({ repositoryName: z .string() .describe('The name of the repository, example: vercel/ai-chatbot'), }), generate: async function* ({ repositoryName }) { yield <div>Cloning repository {repositoryName}...</div>; // [!code highlight:5] await new Promise(resolve => setTimeout(resolve, 3000)); yield <div>Building repository {repositoryName}...</div>; await new Promise(resolve => setTimeout(resolve, 2000)); return <div>{repositoryName} deployed!</div>; }, }, }, onFinish: ({ usage }) => { const { promptTokens, completionTokens, totalTokens } = usage; // your own logic, e.g. for saving the chat history or recording usage console.log('Prompt tokens:', promptTokens); console.log('Completion tokens:', completionTokens); console.log('Total tokens:', totalTokens); }, }); return { id: generateId(), role: 'assistant', display: result.value, }; } ``` ```typescript filename='app/ai.ts' import { createAI } from '@ai-sdk/rsc'; import { ServerMessage, ClientMessage, continueConversation } from './actions'; export const AI = createAI<ServerMessage[], ClientMessage[]>({ actions: { continueConversation, }, initialAIState: [], initialUIState: [], }); ``` --- File: /ai/content/cookbook/20-rsc/index.mdx --- --- title: React Server Components collapsed: true --- --- File: /ai/content/docs/00-introduction/index.mdx --- --- title: AI SDK by Vercel description: The AI SDK is the TypeScript toolkit for building AI applications and agents with React, Next.js, Vue, Svelte, Node.js, and more. --- # AI SDK The AI SDK is the TypeScript toolkit designed to help developers build AI-powered applications and agents with React, Next.js, Vue, Svelte, Node.js, and more. ## Why use the AI SDK? 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: <PreviewSwitchProviders /> 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. ## Model Providers The AI SDK supports [multiple model providers](/providers). <OfficialModelCards /> ## Templates We've built some [templates](https://vercel.com/templates?type=ai) that include AI SDK integrations for different use cases, providers, and frameworks. You can use these templates to get started with your AI-powered application. ### Starter Kits <Templates type="starter-kits" /> ### Feature Exploration <Templates type="feature-exploration" /> ### Frameworks <Templates type="frameworks" /> ### Generative UI <Templates type="generative-ui" /> ### Security <Templates type="security" /> ## Join our Community 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` (for Cursor, Windsurf, Copilot, Claude etc.) 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 [ai-sdk.dev/llms.txt](/llms.txt) 2. Use the following prompt format: ```prompt Documentation: {paste documentation here} --- Based on the above documentation, answer the following: {your question} ``` --- File: /ai/content/docs/02-foundations/01-overview.mdx --- --- title: Overview description: An overview of foundational concepts critical to understanding the AI SDK --- # Overview <Note> This page is a beginner-friendly introduction to high-level artificial intelligence (AI) concepts. To dive right into implementing the AI SDK, feel free to skip ahead to our [quickstarts](/docs/getting-started) or learn about our [supported models and providers](/docs/foundations/providers-and-models). </Note> 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: <PreviewSwitchProviders /> To effectively leverage the AI SDK, it helps to familiarize yourself with the following concepts: ## Generative Artificial Intelligence **Generative artificial intelligence** refers to models that predict and generate various types of outputs (such as text, images, or audio) based on what’s statistically likely, pulling from patterns they’ve learned from their training data. For example: - Given a photo, a generative model can generate a caption. - Given an audio file, a generative model can generate a transcription. - Given a text description, a generative model can generate an image. ## Large Language Models A **large language model (LLM)** is a subset of generative models focused primarily on **text**. An LLM takes a sequence of words as input and aims to predict the most likely sequence to follow. It assigns probabilities to potential next sequences and then selects one. The model continues to generate sequences until it meets a specified stopping criterion. LLMs learn by training on massive collections of written text, which means they will be better suited to some use cases than others. For example, a model trained on GitHub data would understand the probabilities of sequences in source code particularly well. However, it's crucial to understand LLMs' limitations. When asked about less known or absent information, like the birthday of a personal relative, LLMs might "hallucinate" or make up information. It's essential to consider how well-represented the information you need is in the model. ## Embedding Models An **embedding model** is used to convert complex data (like words or images) into a dense vector (a list of numbers) representation, known as an embedding. Unlike generative models, embedding models do not generate new text or data. Instead, they provide representations of semantic and syntactic relationships between entities that can be used as input for other models or other natural language processing tasks. In the next section, you will learn about the difference between models providers and models, and which ones are available in the AI SDK. --- File: /ai/content/docs/02-foundations/02-providers-and-models.mdx --- --- title: Providers and Models description: Learn about the providers and models available in the AI SDK. --- # Providers and Models Companies such as OpenAI and Anthropic (providers) offer access to a range of large language models (LLMs) with differing strengths and capabilities through their own APIs. Each provider typically has its own unique method for interfacing with their models, complicating the process of switching providers and increasing the risk of vendor lock-in. To solve these challenges, AI SDK Core offers a standardized approach to interacting with LLMs through a [language model specification](https://github.com/vercel/ai/tree/main/packages/provider/src/language-model/v2) that abstracts differences between providers. This unified interface allows you to switch between providers with ease while using the same API for all providers. Here is an overview of the AI SDK Provider Architecture: <MDXImage srcLight="/images/ai-sdk-diagram.png" srcDark="/images/ai-sdk-diagram-dark.png" width={800} height={800} /> ## AI SDK Providers The AI SDK comes with a wide range of providers that you can use to interact with different language models: - [xAI Grok Provider](/providers/ai-sdk-providers/xai) (`@ai-sdk/xai`) - [OpenAI Provider](/providers/ai-sdk-providers/openai) (`@ai-sdk/openai`) - [Azure OpenAI Provider](/providers/ai-sdk-providers/azure) (`@ai-sdk/azure`) - [Anthropic Provider](/providers/ai-sdk-providers/anthropic) (`@ai-sdk/anthropic`) - [Amazon Bedrock Provider](/providers/ai-sdk-providers/amazon-bedrock) (`@ai-sdk/amazon-bedrock`) - [Google Generative AI Provider](/providers/ai-sdk-providers/google-generative-ai) (`@ai-sdk/google`) - [Google Vertex Provider](/providers/ai-sdk-providers/google-vertex) (`@ai-sdk/google-vertex`) - [Mistral Provider](/providers/ai-sdk-providers/mistral) (`@ai-sdk/mistral`) - [Together.ai Provider](/providers/ai-sdk-providers/togetherai) (`@ai-sdk/togetherai`) - [Cohere Provider](/providers/ai-sdk-providers/cohere) (`@ai-sdk/cohere`) - [Fireworks Provider](/providers/ai-sdk-providers/fireworks) (`@ai-sdk/fireworks`) - [DeepInfra Provider](/providers/ai-sdk-providers/deepinfra) (`@ai-sdk/deepinfra`) - [DeepSeek Provider](/providers/ai-sdk-providers/deepseek) (`@ai-sdk/deepseek`) - [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`) - [LMNT Provider](/providers/ai-sdk-providers/lmnt) (`@ai-sdk/lmnt`) - [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: - [LM Studio](/providers/openai-compatible-providers/lmstudio) - [Baseten](/providers/openai-compatible-providers/baseten) Our [language model specification](https://github.com/vercel/ai/tree/main/packages/provider/src/language-model/v2) is published as an open-source package, which you can use to create [custom providers](/providers/community-providers/custom-providers). The open-source community has created the following providers: - [Ollama Provider](/providers/community-providers/ollama) (`ollama-ai-provider`) - [FriendliAI Provider](/providers/community-providers/friendliai) (`@friendliai/ai-provider`) - [Portkey Provider](/providers/community-providers/portkey) (`@portkey-ai/vercel-provider`) - [Cloudflare Workers AI Provider](/providers/community-providers/cloudflare-workers-ai) (`workers-ai-provider`) - [OpenRouter Provider](/providers/community-providers/openrouter) (`@openrouter/ai-sdk-provider`) - [Requesty Provider](/providers/community-providers/requesty) (`@requesty/ai-sdk`) - [Crosshatch Provider](/providers/community-providers/crosshatch) (`@crosshatch/ai-provider`) - [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`) - [Claude Code Provider](/providers/community-providers/claude-code) (`ai-sdk-provider-claude-code`) - [Built-in AI Provider](/providers/community-providers/built-in-ai) (`built-in-ai`) - [Gemini CLI Provider](/providers/community-providers/gemini-cli) (`ai-sdk-provider-gemini-cli`) - [A2A Provider](/providers/community-providers/a2a) (`a2a-ai-provider`) - [SAP-AI Provider](/providers/community-providers/sap-ai) (`@mymediset/sap-ai-provider`) ## Self-Hosted Models You can access self-hosted models with the following providers: - [Ollama Provider](/providers/community-providers/ollama) - [LM Studio](/providers/openai-compatible-providers/lmstudio) - [Baseten](/providers/openai-compatible-providers/baseten) Additionally, any self-hosted provider that supports the OpenAI specification can be used with the [OpenAI Compatible Provider](/providers/openai-compatible-providers). ## Model Capabilities 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-4` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-3` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-3-fast` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-3-mini` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-3-mini-fast` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-2-1212` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-2-vision-1212` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-beta` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-vision-beta` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | [Vercel](/providers/ai-sdk-providers/vercel) | `v0-1.0-md` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4.1` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4.1-mini` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4.1-nano` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4o` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4o-mini` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4.1` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `o3-mini` | <Cross size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `o3` | <Cross size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `o4-mini` | <Cross size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `o1` | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `o1-mini` | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `o1-preview` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-opus-4-20250514` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-sonnet-4-20250514` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-3-7-sonnet-20250219` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-3-5-sonnet-20241022` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-3-5-sonnet-20240620` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-3-5-haiku-20241022` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Mistral](/providers/ai-sdk-providers/mistral) | `pixtral-large-latest` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Mistral](/providers/ai-sdk-providers/mistral) | `mistral-large-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Mistral](/providers/ai-sdk-providers/mistral) | `mistral-medium-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Mistral](/providers/ai-sdk-providers/mistral) | `mistral-medium-2505` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Mistral](/providers/ai-sdk-providers/mistral) | `mistral-small-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Mistral](/providers/ai-sdk-providers/mistral) | `pixtral-12b-2409` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai) | `gemini-2.0-flash-exp` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai) | `gemini-1.5-flash` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai) | `gemini-1.5-pro` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Google Vertex](/providers/ai-sdk-providers/google-vertex) | `gemini-2.0-flash-exp` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Google Vertex](/providers/ai-sdk-providers/google-vertex) | `gemini-1.5-flash` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Google Vertex](/providers/ai-sdk-providers/google-vertex) | `gemini-1.5-pro` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [DeepSeek](/providers/ai-sdk-providers/deepseek) | `deepseek-chat` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [DeepSeek](/providers/ai-sdk-providers/deepseek) | `deepseek-reasoner` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | [Cerebras](/providers/ai-sdk-providers/cerebras) | `llama3.1-8b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Cerebras](/providers/ai-sdk-providers/cerebras) | `llama3.1-70b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Cerebras](/providers/ai-sdk-providers/cerebras) | `llama3.3-70b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Groq](/providers/ai-sdk-providers/groq) | `meta-llama/llama-4-scout-17b-16e-instruct` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Groq](/providers/ai-sdk-providers/groq) | `llama-3.3-70b-versatile` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Groq](/providers/ai-sdk-providers/groq) | `llama-3.1-8b-instant` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Groq](/providers/ai-sdk-providers/groq) | `mixtral-8x7b-32768` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Groq](/providers/ai-sdk-providers/groq) | `gemma2-9b-it` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Note> This table is not exhaustive. Additional models can be found in the provider documentation pages and on the provider websites. </Note> --- File: /ai/content/docs/02-foundations/03-prompts.mdx --- --- title: Prompts description: Learn about the Prompt structure used in the AI SDK. --- # Prompts Prompts are instructions that you give a [large language model (LLM)](/docs/foundations/overview#large-language-models) to tell it what to do. It's like when you ask someone for directions; the clearer your question, the better the directions you'll get. Many LLM providers offer complex interfaces for specifying prompts. They involve different roles and message types. While these interfaces are powerful, they can be hard to use and understand. In order to simplify prompting, the AI SDK supports text, message, and system prompts. ## Text Prompts Text prompts are strings. They are ideal for simple generation use cases, e.g. repeatedly generating content for variants of the same prompt text. You can set text prompts using the `prompt` property made available by AI SDK functions like [`streamText`](/docs/reference/ai-sdk-core/stream-text) or [`generateObject`](/docs/reference/ai-sdk-core/generate-object). You can structure the text in any way and inject variables, e.g. using a template literal. ```ts highlight="3" const result = await generateText({ model: 'openai/gpt-4.1', prompt: 'Invent a new holiday and describe its traditions.', }); ``` You can also use template literals to provide dynamic data to your prompt. ```ts highlight="3-5" const result = await generateText({ model: 'openai/gpt-4.1', prompt: `I am planning a trip to ${destination} for ${lengthOfStay} days. ` + `Please suggest the best tourist activities for me to do.`, }); ``` ## System Prompts System prompts are the initial set of instructions given to models that help guide and constrain the models' behaviors and responses. You can set system prompts using the `system` property. System prompts work with both the `prompt` and the `messages` properties. ```ts highlight="3-6" const result = await generateText({ model: 'openai/gpt-4.1', system: `You help planning travel itineraries. ` + `Respond to the users' request with a list ` + `of the best stops to make in their destination.`, prompt: `I am planning a trip to ${destination} for ${lengthOfStay} days. ` + `Please suggest the best tourist activities for me to do.`, }); ``` <Note> When you use a message prompt, you can also use system messages instead of a system prompt. </Note> ## Message Prompts A message prompt is an array of user, assistant, and tool messages. They are great for chat interfaces and more complex, multi-modal prompts. You can use the `messages` property to set message prompts. Each message has a `role` and a `content` property. The content can either be text (for user and assistant messages), or an array of relevant parts (data) for that message type. ```ts highlight="3-7" const result = await generateText({ model: 'openai/gpt-4.1', messages: [ { role: 'user', content: 'Hi!' }, { role: 'assistant', content: 'Hello, how can I help?' }, { role: 'user', content: 'Where can I buy the best Currywurst in Berlin?' }, ], }); ``` Instead of sending a text in the `content` property, you can send an array of parts that includes a mix of text and other content parts. <Note type="warning"> Not all language models support all message and content types. For example, some models might not be capable of handling multi-modal inputs or tool messages. [Learn more about the capabilities of select models](./providers-and-models#model-capabilities). </Note> ### 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 import { ModelMessage } from 'ai'; const messages: ModelMessage[] = [ { 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 import { ModelMessage } from 'ai'; const messages: ModelMessage[] = [ { 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' }, }, }, ], }, ]; ``` <Note type="warning"> 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 [`convertToModelMessages`](/docs/reference/ai-sdk-ui/convert-to-core-messages) function to convert `UIMessage` objects to [`ModelMessage`](/docs/reference/ai-sdk-core/model-message) objects before applying or appending message(s) or message parts with `providerOptions`. </Note> ### User Messages #### Text Parts Text content is the most common type of content. It is a string that is passed to the model. If you only need to send text content in a message, the `content` property can be a string, but you can also use it to send multiple content parts. ```ts highlight="7-10" const result = await generateText({ model: 'openai/gpt-4.1', messages: [ { role: 'user', content: [ { type: 'text', text: 'Where can I buy the best Currywurst in Berlin?', }, ], }, ], }); ``` #### Image Parts User messages can include image parts. An image can be one of the following: - base64-encoded image: - `string` with base-64 encoded content - data URL `string`, e.g. `data:image/png;base64,...` - binary image: - `ArrayBuffer` - `Uint8Array` - `Buffer` - URL: - http(s) URL `string`, e.g. `https://example.com/image.png` - `URL` object, e.g. `new URL('https://example.com/image.png')` ##### Example: Binary image (Buffer) ```ts highlight="8-11" const result = await generateText({ model, messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png'), }, ], }, ], }); ``` ##### Example: Base-64 encoded image (string) ```ts highlight="8-11" const result = await generateText({ model: 'openai/gpt-4.1', messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png').toString('base64'), }, ], }, ], }); ``` ##### Example: Image URL (string) ```ts highlight="8-12" const result = await generateText({ model: 'openai/gpt-4.1', messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/comic-cat.png?raw=true', }, ], }, ], }); ``` #### File Parts <Note type="warning"> Only a few providers and models currently support file parts: [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai), [Google Vertex AI](/providers/ai-sdk-providers/google-vertex), [OpenAI](/providers/ai-sdk-providers/openai) (for `wav` and `mp3` audio with `gpt-4o-audio-preview`), [Anthropic](/providers/ai-sdk-providers/anthropic), [OpenAI](/providers/ai-sdk-providers/openai) (for `pdf`). </Note> User messages can include file parts. A file can be one of the following: - base64-encoded file: - `string` with base-64 encoded content - data URL `string`, e.g. `data:image/png;base64,...` - binary data: - `ArrayBuffer` - `Uint8Array` - `Buffer` - URL: - http(s) URL `string`, e.g. `https://example.com/some.pdf` - `URL` object, e.g. `new URL('https://example.com/some.pdf')` You need to specify the MIME type of the file you are sending. ##### Example: PDF file from Buffer ```ts highlight="12-15" import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; const result = await generateText({ model: google('gemini-1.5-flash'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is the file about?' }, { type: 'file', mediaType: 'application/pdf', data: fs.readFileSync('./data/example.pdf'), filename: 'example.pdf', // optional, not used by all providers }, ], }, ], }); ``` ##### Example: mp3 audio file from Buffer ```ts highlight="12-14" import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; const result = await generateText({ model: openai('gpt-4o-audio-preview'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is the audio saying?' }, { type: 'file', mediaType: 'audio/mpeg', data: fs.readFileSync('./data/galileo.mp3'), }, ], }, ], }); ``` ### Assistant Messages Assistant messages are messages that have a role of `assistant`. They are typically previous responses from the assistant and can contain text, reasoning, and tool call parts. #### Example: Assistant message with text content ```ts highlight="5" const result = await generateText({ model: 'openai/gpt-4.1', messages: [ { role: 'user', content: 'Hi!' }, { role: 'assistant', content: 'Hello, how can I help?' }, ], }); ``` #### Example: Assistant message with text content in array ```ts highlight="7" const result = await generateText({ model: 'openai/gpt-4.1', messages: [ { role: 'user', content: 'Hi!' }, { role: 'assistant', content: [{ type: 'text', text: 'Hello, how can I help?' }], }, ], }); ``` #### Example: Assistant message with tool call content ```ts highlight="7-14" const result = await generateText({ model: 'openai/gpt-4.1', messages: [ { role: 'user', content: 'How many calories are in this block of cheese?' }, { role: 'assistant', content: [ { type: 'tool-call', toolCallId: '12345', toolName: 'get-nutrition-data', input: { cheese: 'Roquefort' }, }, ], }, ], }); ``` #### Example: Assistant message with file content <Note> This content part is for model-generated files. Only a few models support this, and only for file types that they can generate. </Note> ```ts highlight="9-11" const result = await generateText({ model: 'openai/gpt-4.1', messages: [ { role: 'user', content: 'Generate an image of a roquefort cheese!' }, { role: 'assistant', content: [ { type: 'file', mediaType: 'image/png', data: fs.readFileSync('./data/roquefort.jpg'), }, ], }, ], }); ``` ### Tool messages <Note> [Tools](/docs/foundations/tools) (also known as function calling) are programs that you can provide an LLM to extend its built-in functionality. This can be anything from calling an external API to calling functions within your UI. Learn more about Tools in [the next section](/docs/foundations/tools). </Note> For models that support [tool](/docs/foundations/tools) calls, assistant messages can contain tool call parts, and tool messages can contain tool output parts. A single assistant message can call multiple tools, and a single tool message can contain multiple tool results. ```ts highlight="14-42" const result = await generateText({ model: 'openai/gpt-4.1', messages: [ { role: 'user', content: [ { type: 'text', text: 'How many calories are in this block of cheese?', }, { type: 'image', image: fs.readFileSync('./data/roquefort.jpg') }, ], }, { role: 'assistant', content: [ { type: 'tool-call', toolCallId: '12345', toolName: 'get-nutrition-data', input: { cheese: 'Roquefort' }, }, // there could be more tool calls here (parallel calling) ], }, { role: 'tool', content: [ { type: 'tool-result', toolCallId: '12345', // needs to match the tool call id toolName: 'get-nutrition-data', output: { type: 'json', value: { name: 'Cheese, roquefort', calories: 369, fat: 31, protein: 22, }, }, }, // there could be more tool results here (parallel calling) ], }, ], }); ``` #### Multi-modal Tool Results <Note type="warning"> Multi-part tool results are experimental and only supported by Anthropic. </Note> Tool results can be multi-part and multi-modal, e.g. a text and an image. You can use the `experimental_content` property on tool parts to specify multi-part tool results. ```ts highlight="20-32" const result = await generateText({ model: 'openai/gpt-4.1', messages: [ // ... { role: 'tool', content: [ { type: 'tool-result', toolCallId: '12345', // needs to match the tool call id toolName: 'get-nutrition-data', // for models that do not support multi-part tool results, // you can include a regular output part: output: { type: 'json', value: { name: 'Cheese, roquefort', calories: 369, fat: 31, protein: 22, }, }, }, { type: 'tool-result', toolCallId: '12345', // needs to match the tool call id toolName: 'get-nutrition-data', // for models that support multi-part tool results, // you can include a multi-part content part: output: { type: 'content', value: [ { type: 'text', text: 'Here is an image of the nutrition data for the cheese:', }, { type: 'media', data: fs .readFileSync('./data/roquefort-nutrition-data.png') .toString('base64'), mediaType: 'image/png', }, ], }, }, ], }, ], }); ``` ### System Messages System messages are messages that are sent to the model before the user messages to guide the assistant's behavior. You can alternatively use the `system` property. ```ts highlight="4" const result = await generateText({ model: 'openai/gpt-4.1', messages: [ { role: 'system', content: 'You help planning travel itineraries.' }, { role: 'user', content: 'I am planning a trip to Berlin for 3 days. Please suggest the best tourist activities for me to do.', }, ], }); ``` --- File: /ai/content/docs/02-foundations/04-tools.mdx --- --- title: Tools description: Learn about tools with the AI SDK. --- # Tools While [large language models (LLMs)](/docs/foundations/overview#large-language-models) have incredible generation capabilities, they struggle with discrete tasks (e.g. mathematics) and interacting with the outside world (e.g. getting the weather). Tools are actions that an LLM can invoke. The results of these actions can be reported back to the LLM to be considered in the next response. For example, when you ask an LLM for the "weather in London", and there is a weather tool available, it could call a tool with London as the argument. The tool would then fetch the weather data and return it to the LLM. The LLM can then use this information in its response. ## What is a tool? A tool is an object that can be called by the model to perform a specific task. You can use tools with [`generateText`](/docs/reference/ai-sdk-core/generate-text) and [`streamText`](/docs/reference/ai-sdk-core/stream-text) by passing one or more tools to the `tools` parameter. A tool consists of three properties: - **`description`**: An optional description of the tool that can influence when the tool is picked. - **`inputSchema`**: A [Zod schema](/docs/foundations/tools#schema-specification-and-validation-with-zod) or a [JSON schema](/docs/reference/ai-sdk-core/json-schema) that defines the input required for the tool to run. The schema is consumed by the LLM, and also used to validate the LLM tool calls. - **`execute`**: An optional async function that is called with the arguments from the tool call. <Note> `streamUI` uses UI generator tools with a `generate` function that can return React components. </Note> If the LLM decides to use a tool, it will generate a tool call. Tools with an `execute` function are run automatically when these calls are generated. The output of the tool calls are returned using tool result objects. You can automatically pass tool results back to the LLM using [multi-step calls](/docs/ai-sdk-core/tools-and-tool-calling#multi-step-calls) with `streamText` and `generateText`. ## Schemas Schemas are used to define the parameters for tools and to validate the [tool calls](/docs/ai-sdk-core/tools-and-tool-calling). The AI SDK supports both raw JSON schemas (using the [`jsonSchema` function](/docs/reference/ai-sdk-core/json-schema)) and [Zod](https://zod.dev/) schemas (either directly or using the [`zodSchema` function](/docs/reference/ai-sdk-core/zod-schema)). [Zod](https://zod.dev/) is a popular TypeScript schema validation library. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add zod" dark /> </Tab> <Tab> <Snippet text="npm install zod" dark /> </Tab> <Tab> <Snippet text="yarn add zod" dark /> </Tab> </Tabs> You can then specify a Zod schema, for example: ```ts import z from 'zod'; const recipeSchema = z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }); ``` <Note> You can also use schemas for structured output generation with [`generateObject`](/docs/reference/ai-sdk-core/generate-object) and [`streamObject`](/docs/reference/ai-sdk-core/stream-object). </Note> ## Toolkits When you work with tools, you typically need a mix of application specific tools and general purpose tools. There are several providers that offer pre-built tools as **toolkits** that you can use out of the box: - **[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/introduction#vercel-ai-integration)** - 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 - **[Smithery](https://smithery.ai/docs/use/connect)** - Smithery provides an open marketplace of 6K+ MCPs, including [Browserbase](https://browserbase.com/) and [Exa](https://exa.ai/). - **[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. - **[Composio](https://docs.composio.dev/javascript/vercel)** - Composio provides 250+ tools like GitHub, Gmail, Salesforce and [more](https://composio.dev/tools). - **[Interlify](https://www.interlify.com/docs/integrate-with-vercel-ai)** - Convert APIs into tools so that AI can connect to your backend in minutes. - **[Freestyle](https://docs.freestyle.sh/integrations/vercel)** - Tool for your AI to execute JavaScript or TypeScript with arbitrary node modules. - **[JigsawStack](http://www.jigsawstack.com/docs/integration/vercel)** - JigsawStack provides over 30+ small custom fine tuned models available for specific uses. <Note> Do you have open source tools or tool libraries that are compatible with the AI SDK? Please [file a pull request](https://github.com/vercel/ai/pulls) to add them to this list. </Note> ## Learn more The AI SDK Core [Tool Calling](/docs/ai-sdk-core/tools-and-tool-calling) and [Agents](/docs/foundations/agents) documentation has more information about tools and tool calling. --- File: /ai/content/docs/02-foundations/05-streaming.mdx --- --- title: Streaming description: Why use streaming for AI applications? --- # Streaming Streaming conversational text UIs (like ChatGPT) have gained massive popularity over the past few months. This section explores the benefits and drawbacks of streaming and blocking interfaces. [Large language models (LLMs)](/docs/foundations/overview#large-language-models) are extremely powerful. However, when generating long outputs, they can be very slow compared to the latency you're likely used to. If you try to build a traditional blocking UI, your users might easily find themselves staring at loading spinners for 5, 10, even up to 40s waiting for the entire LLM response to be generated. This can lead to a poor user experience, especially in conversational applications like chatbots. Streaming UIs can help mitigate this issue by **displaying parts of the response as they become available**. <div className="grid lg:grid-cols-2 grid-cols-1 gap-4 mt-8"> <Card title="Blocking UI" description="Blocking responses wait until the full response is available before displaying it." > <BrowserIllustration highlight blocking /> </Card> <Card title="Streaming UI" description="Streaming responses can transmit parts of the response as they become available." > <BrowserIllustration highlight /> </Card> </div> ## Real-world Examples Here are 2 examples that illustrate how streaming UIs can improve user experiences in a real-world setting – the first uses a blocking UI, while the second uses a streaming UI. ### Blocking UI <InlinePrompt initialInput="Come up with the first 200 characters of the first book in the Harry Potter series." blocking /> ### Streaming UI <InlinePrompt initialInput="Come up with the first 200 characters of the first book in the Harry Potter series." /> As you can see, the streaming UI is able to start displaying the response much faster than the blocking UI. This is because the blocking UI has to wait for the entire response to be generated before it can display anything, while the streaming UI can display parts of the response as they become available. While streaming interfaces can greatly enhance user experiences, especially with larger language models, they aren't always necessary or beneficial. If you can achieve your desired functionality using a smaller, faster model without resorting to streaming, this route can often lead to simpler and more manageable development processes. However, regardless of the speed of your model, the AI SDK is designed to make implementing streaming UIs as simple as possible. In the example below, we stream text generation from OpenAI's `gpt-4.1` in under 10 lines of code using the SDK's [`streamText`](/docs/reference/ai-sdk-core/stream-text) function: ```ts import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; const { textStream } = streamText({ model: openai('gpt-4.1'), prompt: 'Write a poem about embedding models.', }); for await (const textPart of textStream) { console.log(textPart); } ``` For an introduction to streaming UIs and the AI SDK, check out our [Getting Started guides](/docs/getting-started). --- File: /ai/content/docs/02-foundations/06-agents.mdx --- --- title: Agents description: Learn how to build agents with AI SDK Core. --- # Agents When building AI applications, you often need **systems that can understand context and take meaningful actions**. When building these systems, the key consideration is finding the right balance between flexibility and control. Let's explore different approaches and patterns for building these systems, with a focus on helping you match capabilities to your needs. ## Building Blocks When building AI systems, you can combine these fundamental components: ### Single-Step LLM Generation The basic building block - one call to an LLM to get a response. Useful for straightforward tasks like classification or text generation. ### Tool Usage Enhanced capabilities through tools (like calculators, APIs, or databases) that the LLM can use to accomplish tasks. Tools provide a controlled way to extend what the LLM can do. When solving complex problems, **an LLM can make multiple tool calls across multiple steps without you explicitly specifying the order** - for example, looking up information in a database, using that to make calculations, and then storing results. The AI SDK makes this [multi-step tool usage](#multi-step-tool-usage) straightforward through the `stopWhen` parameter. ### Multi-Agent Systems Multiple LLMs working together, each specialized for different aspects of a complex task. This enables sophisticated behaviors while keeping individual components focused. ## Patterns These building blocks can be combined with workflow patterns that help manage complexity: - [Sequential Processing](#sequential-processing-chains) - Steps executed in order - [Parallel Processing](#parallel-processing) - Independent tasks run simultaneously - [Evaluation/Feedback Loops](#evaluator-optimizer) - Results checked and improved iteratively - [Orchestration](#orchestrator-worker) - Coordinating multiple components - [Routing](#routing) - Directing work based on context ## Choosing Your Approach The key factors to consider: - **Flexibility vs Control** - How much freedom does the LLM need vs how tightly must you constrain its actions? - **Error Tolerance** - What are the consequences of mistakes in your use case? - **Cost Considerations** - More complex systems typically mean more LLM calls and higher costs - **Maintenance** - Simpler architectures are easier to debug and modify **Start with the simplest approach that meets your needs**. Add complexity only when required by: 1. Breaking down tasks into clear steps 2. Adding tools for specific capabilities 3. Implementing feedback loops for quality control 4. Introducing multiple agents for complex workflows Let's look at examples of these patterns in action. ## Patterns with Examples The following patterns, adapted from [Anthropic's guide on building effective agents](https://www.anthropic.com/research/building-effective-agents), serve as building blocks that can be combined to create comprehensive workflows. Each pattern addresses specific aspects of task execution, and by combining them thoughtfully, you can build reliable solutions for complex problems. ### Sequential Processing (Chains) The simplest workflow pattern executes steps in a predefined order. Each step's output becomes input for the next step, creating a clear chain of operations. This pattern is ideal for tasks with well-defined sequences, like content generation pipelines or data transformation processes. ```ts import { openai } from '@ai-sdk/openai'; import { generateText, generateObject } from 'ai'; import { z } from 'zod'; async function generateMarketingCopy(input: string) { const model = openai('gpt-4o'); // First step: Generate marketing copy const { text: copy } = await generateText({ model, prompt: `Write persuasive marketing copy for: ${input}. Focus on benefits and emotional appeal.`, }); // Perform quality check on copy const { object: qualityMetrics } = await generateObject({ model, schema: z.object({ hasCallToAction: z.boolean(), emotionalAppeal: z.number().min(1).max(10), clarity: z.number().min(1).max(10), }), prompt: `Evaluate this marketing copy for: 1. Presence of call to action (true/false) 2. Emotional appeal (1-10) 3. Clarity (1-10) Copy to evaluate: ${copy}`, }); // If quality check fails, regenerate with more specific instructions if ( !qualityMetrics.hasCallToAction || qualityMetrics.emotionalAppeal < 7 || qualityMetrics.clarity < 7 ) { const { text: improvedCopy } = await generateText({ model, prompt: `Rewrite this marketing copy with: ${!qualityMetrics.hasCallToAction ? '- A clear call to action' : ''} ${qualityMetrics.emotionalAppeal < 7 ? '- Stronger emotional appeal' : ''} ${qualityMetrics.clarity < 7 ? '- Improved clarity and directness' : ''} Original copy: ${copy}`, }); return { copy: improvedCopy, qualityMetrics }; } return { copy, qualityMetrics }; } ``` ### Routing This pattern allows the model to make decisions about which path to take through a workflow based on context and intermediate results. The model acts as an intelligent router, directing the flow of execution between different branches of your workflow. This is particularly useful when handling varied inputs that require different processing approaches. In the example below, the results of the first LLM call change the properties of the second LLM call like model size and system prompt. ```ts import { openai } from '@ai-sdk/openai'; import { generateObject, generateText } from 'ai'; import { z } from 'zod'; async function handleCustomerQuery(query: string) { const model = openai('gpt-4o'); // First step: Classify the query type const { object: classification } = await generateObject({ model, schema: z.object({ reasoning: z.string(), type: z.enum(['general', 'refund', 'technical']), complexity: z.enum(['simple', 'complex']), }), prompt: `Classify this customer query: ${query} Determine: 1. Query type (general, refund, or technical) 2. Complexity (simple or complex) 3. Brief reasoning for classification`, }); // Route based on classification // Set model and system prompt based on query type and complexity const { text: response } = await generateText({ model: classification.complexity === 'simple' ? openai('gpt-4o-mini') : openai('o3-mini'), system: { general: 'You are an expert customer service agent handling general inquiries.', refund: 'You are a customer service agent specializing in refund requests. Follow company policy and collect necessary information.', technical: 'You are a technical support specialist with deep product knowledge. Focus on clear step-by-step troubleshooting.', }[classification.type], prompt: query, }); return { response, classification }; } ``` ### Parallel Processing Some tasks can be broken down into independent subtasks that can be executed simultaneously. This pattern takes advantage of parallel execution to improve efficiency while maintaining the benefits of structured workflows. For example, analyzing multiple documents or processing different aspects of a single input concurrently (like code review). ```ts import { openai } from '@ai-sdk/openai'; import { generateText, generateObject } from 'ai'; import { z } from 'zod'; // Example: Parallel code review with multiple specialized reviewers async function parallelCodeReview(code: string) { const model = openai('gpt-4o'); // Run parallel reviews const [securityReview, performanceReview, maintainabilityReview] = await Promise.all([ generateObject({ model, system: 'You are an expert in code security. Focus on identifying security vulnerabilities, injection risks, and authentication issues.', schema: z.object({ vulnerabilities: z.array(z.string()), riskLevel: z.enum(['low', 'medium', 'high']), suggestions: z.array(z.string()), }), prompt: `Review this code: ${code}`, }), generateObject({ model, system: 'You are an expert in code performance. Focus on identifying performance bottlenecks, memory leaks, and optimization opportunities.', schema: z.object({ issues: z.array(z.string()), impact: z.enum(['low', 'medium', 'high']), optimizations: z.array(z.string()), }), prompt: `Review this code: ${code}`, }), generateObject({ model, system: 'You are an expert in code quality. Focus on code structure, readability, and adherence to best practices.', schema: z.object({ concerns: z.array(z.string()), qualityScore: z.number().min(1).max(10), recommendations: z.array(z.string()), }), prompt: `Review this code: ${code}`, }), ]); const reviews = [ { ...securityReview.object, type: 'security' }, { ...performanceReview.object, type: 'performance' }, { ...maintainabilityReview.object, type: 'maintainability' }, ]; // Aggregate results using another model instance const { text: summary } = await generateText({ model, system: 'You are a technical lead summarizing multiple code reviews.', prompt: `Synthesize these code review results into a concise summary with key actions: ${JSON.stringify(reviews, null, 2)}`, }); return { reviews, summary }; } ``` ### Orchestrator-Worker In this pattern, a primary model (orchestrator) coordinates the execution of specialized workers. Each worker is optimized for a specific subtask, while the orchestrator maintains overall context and ensures coherent results. This pattern excels at complex tasks requiring different types of expertise or processing. ```ts import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import { z } from 'zod'; async function implementFeature(featureRequest: string) { // Orchestrator: Plan the implementation const { object: implementationPlan } = await generateObject({ model: openai('o3-mini'), schema: z.object({ files: z.array( z.object({ purpose: z.string(), filePath: z.string(), changeType: z.enum(['create', 'modify', 'delete']), }), ), estimatedComplexity: z.enum(['low', 'medium', 'high']), }), system: 'You are a senior software architect planning feature implementations.', prompt: `Analyze this feature request and create an implementation plan: ${featureRequest}`, }); // Workers: Execute the planned changes const fileChanges = await Promise.all( implementationPlan.files.map(async file => { // Each worker is specialized for the type of change const workerSystemPrompt = { create: 'You are an expert at implementing new files following best practices and project patterns.', modify: 'You are an expert at modifying existing code while maintaining consistency and avoiding regressions.', delete: 'You are an expert at safely removing code while ensuring no breaking changes.', }[file.changeType]; const { object: change } = await generateObject({ model: openai('gpt-4o'), schema: z.object({ explanation: z.string(), code: z.string(), }), system: workerSystemPrompt, prompt: `Implement the changes for ${file.filePath} to support: ${file.purpose} Consider the overall feature context: ${featureRequest}`, }); return { file, implementation: change, }; }), ); return { plan: implementationPlan, changes: fileChanges, }; } ``` ### Evaluator-Optimizer This pattern introduces quality control into workflows by having dedicated evaluation steps that assess intermediate results. Based on the evaluation, the workflow can either proceed, retry with adjusted parameters, or take corrective action. This creates more robust workflows capable of self-improvement and error recovery. ```ts import { openai } from '@ai-sdk/openai'; import { generateText, generateObject } from 'ai'; import { z } from 'zod'; async function translateWithFeedback(text: string, targetLanguage: string) { let currentTranslation = ''; let iterations = 0; const MAX_ITERATIONS = 3; // Initial translation const { text: translation } = await generateText({ model: openai('gpt-4o-mini'), // use small model for first attempt system: 'You are an expert literary translator.', prompt: `Translate this text to ${targetLanguage}, preserving tone and cultural nuances: ${text}`, }); currentTranslation = translation; // Evaluation-optimization loop while (iterations < MAX_ITERATIONS) { // Evaluate current translation const { object: evaluation } = await generateObject({ model: openai('gpt-4o'), // use a larger model to evaluate schema: z.object({ qualityScore: z.number().min(1).max(10), preservesTone: z.boolean(), preservesNuance: z.boolean(), culturallyAccurate: z.boolean(), specificIssues: z.array(z.string()), improvementSuggestions: z.array(z.string()), }), system: 'You are an expert in evaluating literary translations.', prompt: `Evaluate this translation: Original: ${text} Translation: ${currentTranslation} Consider: 1. Overall quality 2. Preservation of tone 3. Preservation of nuance 4. Cultural accuracy`, }); // Check if quality meets threshold if ( evaluation.qualityScore >= 8 && evaluation.preservesTone && evaluation.preservesNuance && evaluation.culturallyAccurate ) { break; } // Generate improved translation based on feedback const { text: improvedTranslation } = await generateText({ model: openai('gpt-4o'), // use a larger model system: 'You are an expert literary translator.', prompt: `Improve this translation based on the following feedback: ${evaluation.specificIssues.join('\n')} ${evaluation.improvementSuggestions.join('\n')} Original: ${text} Current Translation: ${currentTranslation}`, }); currentTranslation = improvedTranslation; iterations++; } return { finalTranslation: currentTranslation, iterationsRequired: iterations, }; } ``` ## Multi-Step Tool Usage If your use case involves solving problems where the solution path is poorly defined or too complex to map out as a workflow in advance, you may want to provide the LLM with a set of lower-level tools and allow it to break down the task into small pieces that it can solve on its own iteratively, without discrete instructions. To implement this kind of agentic pattern, you need to call an LLM in a loop until a task is complete. The AI SDK makes this simple with the `stopWhen` parameter. The AI SDK gives you control over the stopping conditions, enabling you to keep the LLM running until one of the conditions are met. The SDK automatically triggers an additional request to the model after every tool result (each request is considered a "step"), continuing until the model does not generate a tool call or other stopping conditions (e.g. `stepCountIs`) you define are satisfied. <Note>`stopWhen` can be used with both `generateText` and `streamText`</Note> ### Using `stopWhen` This example demonstrates how to create an agent that solves math problems. It has a calculator tool (using [math.js](https://mathjs.org/)) that it can call to evaluate mathematical expressions. ```ts file='main.ts' import { openai } from '@ai-sdk/openai'; import { generateText, tool, stepCountIs } from 'ai'; import * as mathjs from 'mathjs'; import { z } from 'zod'; const { text: answer } = await generateText({ model: openai('gpt-4o-2024-08-06'), tools: { calculate: tool({ description: 'A tool for evaluating mathematical expressions. ' + 'Example expressions: ' + "'1.2 * (2 + 4.5)', '12.7 cm to inch', 'sin(45 deg) ^ 2'.", inputSchema: z.object({ expression: z.string() }), execute: async ({ expression }) => mathjs.evaluate(expression), }), }, stopWhen: stepCountIs(10), system: 'You are solving math problems. ' + 'Reason step by step. ' + 'Use the calculator when necessary. ' + 'When you give the final answer, ' + 'provide an explanation for how you arrived at it.', prompt: 'A taxi driver earns $9461 per 1-hour of work. ' + 'If he works 12 hours a day and in 1 hour ' + 'he uses 12 liters of petrol with a price of $134 for 1 liter. ' + 'How much money does he earn in one day?', }); console.log(`ANSWER: ${answer}`); ``` ### Structured Answers When building an agent for tasks like mathematical analysis or report generation, it's often useful to have the agent's final output structured in a consistent format that your application can process. You can use an **answer tool** and the `toolChoice: 'required'` setting to force the LLM to answer with a structured output that matches the schema of the answer tool. The answer tool has no `execute` function, so invoking it will terminate the agent. ```ts highlight="6,16-29,31,45" import { openai } from '@ai-sdk/openai'; import { generateText, tool, stepCountIs } from 'ai'; import 'dotenv/config'; import { z } from 'zod'; const { toolCalls } = await generateText({ model: openai('gpt-4o-2024-08-06'), tools: { calculate: tool({ description: 'A tool for evaluating mathematical expressions. Example expressions: ' + "'1.2 * (2 + 4.5)', '12.7 cm to inch', 'sin(45 deg) ^ 2'.", inputSchema: z.object({ expression: z.string() }), execute: async ({ expression }) => mathjs.evaluate(expression), }), // answer tool: the LLM will provide a structured answer answer: tool({ description: 'A tool for providing the final answer.', inputSchema: z.object({ steps: z.array( z.object({ calculation: z.string(), reasoning: z.string(), }), ), answer: z.string(), }), // no execute function - invoking it will terminate the agent }), }, toolChoice: 'required', stopWhen: stepCountIs(10), system: 'You are solving math problems. ' + 'Reason step by step. ' + 'Use the calculator when necessary. ' + 'The calculator can only do simple additions, subtractions, multiplications, and divisions. ' + 'When you give the final answer, provide an explanation for how you got it.', prompt: 'A taxi driver earns $9461 per 1-hour work. ' + 'If he works 12 hours a day and in 1 hour he uses 14-liters petrol with price $134 for 1-liter. ' + 'How much money does he earn in one day?', }); console.log(`FINAL TOOL CALLS: ${JSON.stringify(toolCalls, null, 2)}`); ``` <Note> You can also use the [`experimental_output`](/docs/ai-sdk-core/generating-structured-data#structured-output-with-generatetext) setting for `generateText` to generate structured outputs. </Note> ### Accessing all steps Calling `generateText` with `stopWhen` can result in several calls to the LLM (steps). You can access information from all steps by using the `steps` property of the response. ```ts highlight="3,9-10" import { generateText, stepCountIs } from 'ai'; const { steps } = await generateText({ model: openai('gpt-4o'), stopWhen: stepCountIs(10), // ... }); // extract all tool calls from the steps: const allToolCalls = steps.flatMap(step => step.toolCalls); ``` ### Getting notified on each completed step You can use the `onStepFinish` callback to get notified on each completed step. It is triggered when a step is finished, i.e. all text deltas, tool calls, and tool results for the step are available. ```tsx highlight="6-8" import { generateText, stepCountIs } from 'ai'; const result = await generateText({ model: 'openai/gpt-4.1', stopWhen: stepCountIs(10), onStepFinish({ text, toolCalls, toolResults, finishReason, usage }) { // your own logic, e.g. for saving the chat history or recording usage }, // ... }); ``` --- File: /ai/content/docs/02-foundations/index.mdx --- --- title: Foundations description: A section that covers foundational knowledge around LLMs and concepts crucial to the AI SDK --- # Foundations <IndexCards cards={[ { title: 'Overview', description: 'Learn about foundational concepts around AI and LLMs.', href: '/docs/foundations/overview', }, { title: 'Providers and Models', description: 'Learn about the providers and models that you can use with the AI SDK.', href: '/docs/foundations/providers-and-models', }, { title: 'Prompts', description: 'Learn about how Prompts are used and defined in the AI SDK.', href: '/docs/foundations/prompts', }, { title: 'Tools', description: 'Learn about tools in the AI SDK.', href: '/docs/foundations/tools', }, { title: 'Streaming', description: 'Learn why streaming is used for AI applications.', href: '/docs/foundations/streaming', }, { title: 'Agents', description: 'Learn how to build agents with the AI SDK.', href: '/docs/foundations/agents', }, ]} /> --- File: /ai/content/docs/02-getting-started/01-navigating-the-library.mdx --- --- title: Navigating the Library description: Learn how to navigate the AI SDK. --- # Navigating the Library The AI SDK is a powerful toolkit for building AI applications. This page will help you pick the right tools for your requirements. Let’s start with a quick overview of the AI SDK, which is comprised of three parts: - **[AI SDK Core](/docs/ai-sdk-core/overview):** A unified, provider agnostic API for generating text, structured objects, and tool calls with LLMs. - **[AI SDK UI](/docs/ai-sdk-ui/overview):** A set of framework-agnostic hooks for building chat and generative user interfaces. - [AI SDK RSC](/docs/ai-sdk-rsc/overview): Stream generative user interfaces with React Server Components (RSC). Development is currently experimental and we recommend using [AI SDK UI](/docs/ai-sdk-ui/overview). ## Choosing the Right Tool for Your Environment When deciding which part of the AI SDK to use, your first consideration should be the environment and existing stack you are working with. Different components of the SDK are tailored to specific frameworks and environments. | Library | Purpose | Environment Compatibility | | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | [AI SDK Core](/docs/ai-sdk-core/overview) | Call any LLM with unified API (e.g. [generateText](/docs/reference/ai-sdk-core/generate-text) and [generateObject](/docs/reference/ai-sdk-core/generate-object)) | Any JS environment (e.g. Node.js, Deno, Browser) | | [AI SDK UI](/docs/ai-sdk-ui/overview) | Build streaming chat and generative UIs (e.g. [useChat](/docs/reference/ai-sdk-ui/use-chat)) | React & Next.js, Vue & Nuxt, Svelte & SvelteKit | | [AI SDK RSC](/docs/ai-sdk-rsc/overview) | Stream generative UIs from Server to Client (e.g. [streamUI](/docs/reference/ai-sdk-rsc/stream-ui)). Development is currently experimental and we recommend using [AI SDK UI](/docs/ai-sdk-ui/overview). | Any framework that supports React Server Components (e.g. Next.js) | ## Environment Compatibility These tools have been designed to work seamlessly with each other and it's likely that you will be using them together. Let's look at how you could decide which libraries to use based on your application environment, existing stack, and requirements. The following table outlines AI SDK compatibility based on environment: | Environment | [AI SDK Core](/docs/ai-sdk-core/overview) | [AI SDK UI](/docs/ai-sdk-ui/overview) | [AI SDK RSC](/docs/ai-sdk-rsc/overview) | | --------------------- | ----------------------------------------- | ------------------------------------- | --------------------------------------- | | None / Node.js / Deno | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | Vue / Nuxt | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | Svelte / SvelteKit | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | Next.js Pages Router | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | Next.js App Router | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | ## When to use AI SDK UI AI SDK UI provides a set of framework-agnostic hooks for quickly building **production-ready AI-native applications**. It offers: - Full support for streaming chat and client-side generative UI - Utilities for handling common AI interaction patterns (i.e. chat, completion, assistant) - Production-tested reliability and performance - Compatibility across popular frameworks ## AI SDK UI Framework Compatibility AI SDK UI supports the following frameworks: [React](https://react.dev/), [Svelte](https://svelte.dev/), and [Vue.js](https://vuejs.org/). Here is a comparison of the supported functions across these frameworks: | Function | React | Svelte | Vue.js | | ---------------------------------------------------------- | ------------------- | ------------------- | ------------------- | | [useChat](/docs/reference/ai-sdk-ui/use-chat) | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [useChat](/docs/reference/ai-sdk-ui/use-chat) tool calling | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | [useCompletion](/docs/reference/ai-sdk-ui/use-completion) | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [useObject](/docs/reference/ai-sdk-ui/use-object) | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Note> [Contributions](https://github.com/vercel/ai/blob/main/CONTRIBUTING.md) are welcome to implement missing features for non-React frameworks. </Note> ## When to use AI SDK RSC <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> [React Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components) (RSCs) provide a new approach to building React applications that allow components to render on the server, fetch data directly, and stream the results to the client, reducing bundle size and improving performance. They also introduce a new way to call server-side functions from anywhere in your application called [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations). AI SDK RSC provides a number of utilities that allow you to stream values and UI directly from the server to the client. However, **it's important to be aware of current limitations**: - **Cancellation**: currently, it is not possible to abort a stream using Server Actions. This will be improved in future releases of React and Next.js. - **Increased Data Transfer**: using [`createStreamableUI`](/docs/reference/ai-sdk-rsc/create-streamable-ui) can lead to quadratic data transfer (quadratic to the length of generated text). You can avoid this using [ `createStreamableValue` ](/docs/reference/ai-sdk-rsc/create-streamable-value) instead, and rendering the component client-side. - **Re-mounting Issue During Streaming**: when using `createStreamableUI`, components re-mount on `.done()`, causing [flickering](https://github.com/vercel/ai/issues/2232). Given these limitations, **we recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production applications**. --- File: /ai/content/docs/02-getting-started/02-nextjs-app-router.mdx --- --- title: Next.js App Router description: Welcome to the AI SDK quickstart guide for Next.js App Router! --- # Next.js App Router Quickstart The AI SDK is a powerful Typescript library designed to help developers build AI-powered applications. In this quickstart tutorial, you'll build a simple AI-chatbot with a streaming user interface. Along the way, you'll learn key concepts and techniques that are fundamental to using the SDK in your own projects. If you are unfamiliar with the concepts of [Prompt Engineering](/docs/advanced/prompt-engineering) and [HTTP Streaming](/docs/advanced/why-streaming), you can optionally read these documents first. ## Prerequisites To follow this quickstart, you'll need: - Node.js 18+ and pnpm installed on your local development machine. - An OpenAI API key. If you haven't obtained your OpenAI API key, you can do so by [signing up](https://platform.openai.com/signup/) on the OpenAI website. ## Create Your Application Start by creating a new Next.js application. This command will create a new directory named `my-ai-app` and set up a basic Next.js application inside it. <div className="mb-4"> <Note> Be sure to select yes when prompted to use the App Router and Tailwind CSS. If you are looking for the Next.js Pages Router quickstart guide, you can find it [here](/docs/getting-started/nextjs-pages-router). </Note> </div> <Snippet text="pnpm create next-app@latest my-ai-app" /> Navigate to the newly created directory: <Snippet text="cd my-ai-app" /> ### Install dependencies Install `ai`, `@ai-sdk/react`, and `@ai-sdk/openai`, the AI package, AI SDK's React hooks, and AI SDK's [ OpenAI provider ](/providers/ai-sdk-providers/openai) respectively. <Note> The AI SDK is designed to be a unified interface to interact with any large language model. This means that you can change model and providers with just one line of code! Learn more about [available providers](/providers) and [building custom providers](/providers/community-providers/custom-providers) in the [providers](/providers) section. </Note> <div className="my-4"> <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add ai @ai-sdk/react @ai-sdk/openai zod" dark /> </Tab> <Tab> <Snippet text="npm install ai @ai-sdk/react @ai-sdk/openai zod" dark /> </Tab> <Tab> <Snippet text="yarn add ai @ai-sdk/react @ai-sdk/openai zod" dark /> </Tab> </Tabs> </div> ### Configure OpenAI API key Create a `.env.local` file in your project root and add your OpenAI API Key. This key is used to authenticate your application with the OpenAI service. <Snippet text="touch .env.local" /> Edit the `.env.local` file: ```env filename=".env.local" OPENAI_API_KEY=xxxxxxxxx ``` Replace `xxxxxxxxx` with your actual OpenAI API key. <Note className="mb-4"> The AI SDK's OpenAI Provider will default to using the `OPENAI_API_KEY` environment variable. </Note> ## Create a Route Handler Create a route handler, `app/api/chat/route.ts` and add the following code: ```tsx filename="app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { streamText, UIMessage, convertToModelMessages } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; 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(); } ``` Let's take a look at what is happening in this code: 1. Define an asynchronous `POST` request handler and extract `messages` from the body of the request. The `messages` variable contains a history of the conversation between you and the chatbot and provides the chatbot with the necessary context to make the next generation. The `messages` are of UIMessage type, which are designed for use in application UI - they contain the entire message history and associated metadata like timestamps. 2. Call [`streamText`](/docs/reference/ai-sdk-core/stream-text), which is imported from the `ai` package. This function accepts a configuration object that contains a `model` provider (imported from `@ai-sdk/openai`) and `messages` (defined in step 1). You can pass additional [settings](/docs/ai-sdk-core/settings) to further customise the model's behaviour. The `messages` key expects a `ModelMessage[]` array. This type is different from `UIMessage` in that it does not include metadata, such as timestamps or sender information. To convert between these types, we use the `convertToModelMessages` function, which strips the UI-specific metadata and transforms the `UIMessage[]` array into the `ModelMessage[]` format that the model expects. 3. The `streamText` function returns a [`StreamTextResult`](/docs/reference/ai-sdk-core/stream-text#result-object). This result object contains the [ `toUIMessageStreamResponse` ](/docs/reference/ai-sdk-core/stream-text#to-data-stream-response) function which converts the result to a streamed response object. 4. Finally, return the result to the client to stream the response. This Route Handler creates a POST request endpoint at `/api/chat`. ## Wire up the UI Now that you have a Route Handler that can query an LLM, it's time to setup your frontend. The AI SDK's [ UI ](/docs/ai-sdk-ui) package abstracts the complexity of a chat interface into one hook, [`useChat`](/docs/reference/ai-sdk-ui/use-chat). Update your root page (`app/page.tsx`) with the following code to show a list of chat messages and provide a user message input: ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat(); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, i) => { switch (part.type) { case 'text': return <div key={`${message.id}-${i}`}>{part.text}</div>; } })} </div> ))} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` <Note> Make sure you add the `"use client"` directive to the top of your file. This allows you to add interactivity with Javascript. </Note> This page utilizes the `useChat` hook, which will, by default, use the `POST` API route you created earlier (`/api/chat`). The hook provides functions and state for handling user input and form submission. The `useChat` hook provides multiple utility functions and state variables: - `messages` - the current chat messages (an array of objects with `id`, `role`, and `parts` properties). - `sendMessage` - a function to send a message to the chat API. The component uses local state (`useState`) to manage the input field value, and handles form submission by calling `sendMessage` with the input text and then clearing the input field. 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: <Snippet text="pnpm run dev" /> Head to your browser and open http://localhost:3000. You should see an input field. Test it out by entering a message and see the AI chatbot respond in real-time! The AI SDK makes it fast and easy to build AI chat interfaces with Next.js. ## Enhance Your Chatbot with Tools While large language models (LLMs) have incredible generation capabilities, they struggle with discrete tasks (e.g. mathematics) and interacting with the outside world (e.g. getting the weather). This is where [tools](/docs/ai-sdk-core/tools-and-tool-calling) come in. Tools are actions that an LLM can invoke. The results of these actions can be reported back to the LLM to be considered in the next response. For example, if a user asks about the current weather, without tools, the model would only be able to provide general information based on its training data. But with a weather tool, it can fetch and provide up-to-date, location-specific weather information. Let's enhance your chatbot by adding a simple weather tool. ### Update Your Route Handler Modify your `app/api/chat/route.ts` file to include the new weather tool: ```tsx filename="app/api/chat/route.ts" highlight="2,13-27" import { openai } from '@ai-sdk/openai'; import { streamText, UIMessage, convertToModelMessages, tool } from 'ai'; import { z } from 'zod'; export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), }, }); return result.toUIMessageStreamResponse(); } ``` In this updated code: 1. You import the `tool` function from the `ai` package and `z` from `zod` for schema validation. 2. You define a `tools` object with a `weather` tool. This tool: - Has a description that helps the model understand when to use it. - Defines `inputSchema` using a Zod schema, specifying that it requires a `location` string to execute this tool. The model will attempt to extract this input 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 input. The `execute` function will then be automatically run, and the tool output will be added to the `messages` as a `tool` message. 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 on the client via the `tool-weather` part of the `message.parts` array. <Note> Tool parts are always named `tool-{toolName}`, where `{toolName}` is the key you used when defining the tool. In this case, since we defined the tool as `weather`, the part type is `tool-weather`. </Note> ### Update the UI To display the tool invocation in your UI, update your `app/page.tsx` file: ```tsx filename="app/page.tsx" highlight="16-21" 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat(); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, i) => { switch (part.type) { case 'text': return <div key={`${message.id}-${i}`}>{part.text}</div>; case 'tool-weather': return ( <pre key={`${message.id}-${i}`}> {JSON.stringify(part, null, 2)} </pre> ); } })} </div> ))} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` With this change, you're updating the UI to handle different message parts. For text parts, you display the text content as before. For weather 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 call and its result displayed in your chat interface. ## Enabling Multi-Step Tool Calls You may have noticed that while the tool is now visible in the chat interface, the model isn't using this information to answer your original query. This is because once the model generates a tool call, it has technically completed its generation. To solve this, you can enable multi-step tool calls using `stopWhen`. By default, `stopWhen` is set to `stepCountIs(1)`, which means generation stops after the first step when there are tool results. By changing this condition, you can allow the model to automatically send tool results back to itself to trigger additional generations until your specified stopping condition is met. In this case, you want the model to continue generating so it can use the weather tool results to answer your original question. ### Update Your Route Handler Modify your `app/api/chat/route.ts` file to include the `stopWhen` condition: ```tsx filename="app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { streamText, UIMessage, convertToModelMessages, tool, stepCountIs, } from 'ai'; import { z } from 'zod'; export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), }, }); return result.toUIMessageStreamResponse(); } ``` In this updated code: 1. You set `stopWhen` to be when `stepCountIs` 5, allowing the model to use up to 5 "steps" for any given generation. 2. You add an `onStepFinish` callback to log any `toolResults` from each step of the interaction, helping you understand the model's tool usage. This means we can also delete the `toolCall` and `toolResult` `console.log` statements from the previous example. Head back to the browser and ask about the weather in a location. You should now see the model using the weather tool results to answer your question. By setting `stopWhen: stepCountIs(5)`, you're allowing the model to use up to 5 "steps" for any given generation. This enables more complex interactions and allows the model to gather and process information over several steps if needed. You can see this in action by adding another tool to convert the temperature from Celsius to Fahrenheit. ### Add another tool Update your `app/api/chat/route.ts` file to add a new tool to convert the temperature from Fahrenheit to Celsius: ```tsx filename="app/api/chat/route.ts" highlight="34-47" import { openai } from '@ai-sdk/openai'; import { streamText, UIMessage, convertToModelMessages, tool, stepCountIs, } from 'ai'; import { z } from 'zod'; export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), convertFahrenheitToCelsius: tool({ description: 'Convert a temperature in fahrenheit to celsius', inputSchema: z.object({ temperature: z .number() .describe('The temperature in fahrenheit to convert'), }), execute: async ({ temperature }) => { const celsius = Math.round((temperature - 32) * (5 / 9)); return { celsius, }; }, }), }, }); return result.toUIMessageStreamResponse(); } ``` ### Update Your Frontend update your `app/page.tsx` file to render the new temperature conversion tool: ```tsx filename="app/page.tsx" highlight="21" 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat(); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, i) => { switch (part.type) { case 'text': return <div key={`${message.id}-${i}`}>{part.text}</div>; case 'tool-weather': case 'tool-convertFahrenheitToCelsius': return ( <pre key={`${message.id}-${i}`}> {JSON.stringify(part, null, 2)} </pre> ); } })} </div> ))} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` This update handles the new `tool-convertFahrenheitToCelsius` part type, displaying the temperature conversion tool calls and results in the UI. Now, when you ask "What's the weather in New York in celsius?", you should see a more complete interaction: 1. The model will call the weather tool for New York. 2. You'll see the tool output displayed. 3. It will then call the temperature conversion tool to convert the temperature from Fahrenheit to Celsius. 4. The model will then use that information to provide a natural language response about the weather in New York. This multi-step approach allows the model to gather information and use it to provide more accurate and contextual responses, making your chatbot considerably more useful. This simple example demonstrates how tools can expand your model's capabilities. You can create more complex tools to integrate with real APIs, databases, or any other external systems, allowing the model to access and process real-world data in real-time. Tools bridge the gap between the model's knowledge cutoff and current information. ## Where to Next? You've built an AI chatbot using the AI SDK! From here, you have several paths to explore: - To learn more about the AI SDK, read through the [documentation](/docs). - If you're interested in diving deeper with guides, check out the [RAG (retrieval-augmented generation)](/docs/guides/rag-chatbot) and [multi-modal chatbot](/docs/guides/multi-modal-chatbot) guides. - To jumpstart your first AI project, explore available [templates](https://vercel.com/templates?type=ai). --- File: /ai/content/docs/02-getting-started/03-nextjs-pages-router.mdx --- --- title: Next.js Pages Router description: Welcome to the AI SDK quickstart guide for Next.js Pages Router! --- # Next.js Pages Router Quickstart The AI SDK is a powerful Typescript library designed to help developers build AI-powered applications. In this quickstart tutorial, you'll build a simple AI-chatbot with a streaming user interface. Along the way, you'll learn key concepts and techniques that are fundamental to using the SDK in your own projects. If you are unfamiliar with the concepts of [Prompt Engineering](/docs/advanced/prompt-engineering) and [HTTP Streaming](/docs/advanced/why-streaming), you can optionally read these documents first. ## Prerequisites To follow this quickstart, you'll need: - Node.js 18+ and pnpm installed on your local development machine. - An OpenAI API key. If you haven't obtained your OpenAI API key, you can do so by [signing up](https://platform.openai.com/signup/) on the OpenAI website. ## Setup Your Application Start by creating a new Next.js application. This command will create a new directory named `my-ai-app` and set up a basic Next.js application inside it. <Note> Be sure to select no when prompted to use the App Router. If you are looking for the Next.js App Router quickstart guide, you can find it [here](/docs/getting-started/nextjs-app-router). </Note> <Snippet text="pnpm create next-app@latest my-ai-app" /> Navigate to the newly created directory: <Snippet text="cd my-ai-app" /> ### Install dependencies Install `ai`, `@ai-sdk/react`, and `@ai-sdk/openai`, the AI package, AI SDK's React hooks, and AI SDK's [ OpenAI provider ](/providers/ai-sdk-providers/openai) respectively. <Note> The AI SDK is designed to be a unified interface to interact with any large language model. This means that you can change model and providers with just one line of code! Learn more about [available providers](/providers) and [building custom providers](/providers/community-providers/custom-providers) in the [providers](/providers) section. </Note> <div className="my-4"> <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add ai @ai-sdk/react @ai-sdk/openai zod" dark /> </Tab> <Tab> <Snippet text="npm install ai @ai-sdk/react @ai-sdk/openai zod" dark /> </Tab> <Tab> <Snippet text="yarn add ai @ai-sdk/react @ai-sdk/openai zod" dark /> </Tab> </Tabs> </div> ### Configure OpenAI API key Create a `.env.local` file in your project root and add your OpenAI API Key. This key is used to authenticate your application with the OpenAI service. <Snippet text="touch .env.local" /> Edit the `.env.local` file: ```env filename=".env.local" OPENAI_API_KEY=xxxxxxxxx ``` Replace `xxxxxxxxx` with your actual OpenAI API key. <Note className="mb-4"> The AI SDK's OpenAI Provider will default to using the `OPENAI_API_KEY` environment variable. </Note> ## Create a Route Handler <Note> As long as you are on Next.js 13+, you can use Route Handlers (using the App Router) alongside the Pages Router. This is recommended to enable you to use the Web APIs interface/signature and to better support streaming. </Note> Create a Route Handler (`app/api/chat/route.ts`) and add the following code: ```tsx filename="app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { streamText, UIMessage, convertToModelMessages } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; 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(); } ``` Let's take a look at what is happening in this code: 1. Define an asynchronous `POST` request handler and extract `messages` from the body of the request. The `messages` variable contains a history of the conversation between you and the chatbot and provides the chatbot with the necessary context to make the next generation. The `messages` are of UIMessage type, which are designed for use in application UI - they contain the entire message history and associated metadata like timestamps. 2. Call [`streamText`](/docs/reference/ai-sdk-core/stream-text), which is imported from the `ai` package. This function accepts a configuration object that contains a `model` provider (imported from `@ai-sdk/openai`) and `messages` (defined in step 1). You can pass additional [settings](/docs/ai-sdk-core/settings) to further customise the model's behaviour. The `messages` key expects a `ModelMessage[]` array. This type is different from `UIMessage` in that it does not include metadata, such as timestamps or sender information. To convert between these types, we use the `convertToModelMessages` function, which strips the UI-specific metadata and transforms the `UIMessage[]` array into the `ModelMessage[]` format that the model expects. 3. The `streamText` function returns a [`StreamTextResult`](/docs/reference/ai-sdk-core/stream-text#result-object). This result object contains the [ `toUIMessageStreamResponse` ](/docs/reference/ai-sdk-core/stream-text#to-data-stream-response) function which converts the result to a streamed response object. 4. Finally, return the result to the client to stream the response. This Route Handler creates a POST request endpoint at `/api/chat`. ## Wire up the UI Now that you have an API route that can query an LLM, it's time to setup your frontend. The AI SDK's [ UI ](/docs/ai-sdk-ui) package abstract the complexity of a chat interface into one hook, [`useChat`](/docs/reference/ai-sdk-ui/use-chat). Update your root page (`pages/index.tsx`) with the following code to show a list of chat messages and provide a user message input: ```tsx filename="pages/index.tsx" import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat(); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, i) => { switch (part.type) { case 'text': return <div key={`${message.id}-${i}`}>{part.text}</div>; } })} </div> ))} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` This page utilizes the `useChat` hook, which will, by default, use the `POST` API route you created earlier (`/api/chat`). The hook provides functions and state for handling user input and form submission. The `useChat` hook provides multiple utility functions and state variables: - `messages` - the current chat messages (an array of objects with `id`, `role`, and `parts` properties). - `sendMessage` - a function to send a message to the chat API. The component uses local state (`useState`) to manage the input field value, and handles form submission by calling `sendMessage` with the input text and then clearing the input field. 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: <Snippet text="pnpm run dev" /> Head to your browser and open http://localhost:3000. You should see an input field. Test it out by entering a message and see the AI chatbot respond in real-time! The AI SDK makes it fast and easy to build AI chat interfaces with Next.js. ## Enhance Your Chatbot with Tools While large language models (LLMs) have incredible generation capabilities, they struggle with discrete tasks (e.g. mathematics) and interacting with the outside world (e.g. getting the weather). This is where [tools](/docs/ai-sdk-core/tools-and-tool-calling) come in. Tools are actions that an LLM can invoke. The results of these actions can be reported back to the LLM to be considered in the next response. For example, if a user asks about the current weather, without tools, the model would only be able to provide general information based on its training data. But with a weather tool, it can fetch and provide up-to-date, location-specific weather information. ### Update Your Route Handler Let's start by giving your chatbot a weather tool. Update your Route Handler (`app/api/chat/route.ts`): ```tsx filename="app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { streamText, UIMessage, convertToModelMessages, tool } from 'ai'; import { z } from 'zod'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, }); return result.toUIMessageStreamResponse(); } ``` In this updated code: 1. You import the `tool` function from the `ai` package and `z` from `zod` for schema validation. 2. You define a `tools` object with a `weather` tool. This tool: - Has a description that helps the model understand when to use it. - Defines `inputSchema` using a Zod schema, specifying that it requires a `location` string to execute this tool. The model will attempt to extract this input 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 input. The `execute` function will then be automatically run, and the tool output will be added to the `messages` as a `tool` message. 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 on the client via the `tool-weather` part of the `message.parts` array. <Note> Tool parts are always named `tool-{toolName}`, where `{toolName}` is the key you used when defining the tool. In this case, since we defined the tool as `weather`, the part type is `tool-weather`. </Note> ### Update the UI To display the tool invocations in your UI, update your `pages/index.tsx` file: ```tsx filename="pages/index.tsx" highlight="16-21" import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat(); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, i) => { switch (part.type) { case 'text': return <div key={`${message.id}-${i}`}>{part.text}</div>; case 'tool-weather': return ( <pre key={`${message.id}-${i}`}> {JSON.stringify(part, null, 2)} </pre> ); } })} </div> ))} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` With this change, you're updating the UI to handle different message parts. For text parts, you display the text content as before. For weather 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 call and its result displayed in your chat interface. ## Enabling Multi-Step Tool Calls You may have noticed that while the tool is now visible in the chat interface, the model isn't using this information to answer your original query. This is because once the model generates a tool call, it has technically completed its generation. To solve this, you can enable multi-step tool calls using `stopWhen`. By default, `stopWhen` is set to `stepCountIs(1)`, which means generation stops after the first step when there are tool results. By changing this condition, you can allow the model to automatically send tool results back to itself to trigger additional generations until your specified stopping condition is met. In this case, you want the model to continue generating so it can use the weather tool results to answer your original question. ### Update Your Route Handler Modify your `app/api/chat/route.ts` file to include the `stopWhen` condition: ```tsx filename="app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { streamText, UIMessage, convertToModelMessages, tool, stepCountIs, } from 'ai'; import { z } from 'zod'; export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), }, }); return result.toUIMessageStreamResponse(); } ``` Head back to the browser and ask about the weather in a location. You should now see the model using the weather tool results to answer your question. By setting `stopWhen: stepCountIs(5)`, you're allowing the model to use up to 5 "steps" for any given generation. This enables more complex interactions and allows the model to gather and process information over several steps if needed. You can see this in action by adding another tool to convert the temperature from Celsius to Fahrenheit. ### Add another tool Update your `app/api/chat/route.ts` file to add a new tool to convert the temperature from Fahrenheit to Celsius: ```tsx filename="app/api/chat/route.ts" highlight="27-40" import { openai } from '@ai-sdk/openai'; import { streamText, UIMessage, convertToModelMessages, tool, stepCountIs, } from 'ai'; import { z } from 'zod'; export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), convertFahrenheitToCelsius: tool({ description: 'Convert a temperature in fahrenheit to celsius', inputSchema: z.object({ temperature: z .number() .describe('The temperature in fahrenheit to convert'), }), execute: async ({ temperature }) => { const celsius = Math.round((temperature - 32) * (5 / 9)); return { celsius, }; }, }), }, }); return result.toUIMessageStreamResponse(); } ``` ### Update Your Frontend Update your `pages/index.tsx` file to render the new temperature conversion tool: ```tsx filename="pages/index.tsx" highlight="21" import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat(); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, i) => { switch (part.type) { case 'text': return <div key={`${message.id}-${i}`}>{part.text}</div>; case 'tool-weather': case 'tool-convertFahrenheitToCelsius': return ( <pre key={`${message.id}-${i}`}> {JSON.stringify(part, null, 2)} </pre> ); } })} </div> ))} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` This update handles the new `tool-convertFahrenheitToCelsius` part type, displaying the temperature conversion tool calls and results in the UI. Now, when you ask "What's the weather in New York in celsius?", you should see a more complete interaction: 1. The model will call the weather tool for New York. 2. You'll see the tool output displayed. 3. It will then call the temperature conversion tool to convert the temperature from Fahrenheit to Celsius. 4. The model will then use that information to provide a natural language response about the weather in New York. This multi-step approach allows the model to gather information and use it to provide more accurate and contextual responses, making your chatbot considerably more useful. This simple example demonstrates how tools can expand your model's capabilities. You can create more complex tools to integrate with real APIs, databases, or any other external systems, allowing the model to access and process real-world data in real-time. Tools bridge the gap between the model's knowledge cutoff and current information. ## Where to Next? You've built an AI chatbot using the AI SDK! From here, you have several paths to explore: - To learn more about the AI SDK, read through the [documentation](/docs). - If you're interested in diving deeper with guides, check out the [RAG (retrieval-augmented generation)](/docs/guides/rag-chatbot) and [multi-modal chatbot](/docs/guides/multi-modal-chatbot) guides. - To jumpstart your first AI project, explore available [templates](https://vercel.com/templates?type=ai). --- File: /ai/content/docs/02-getting-started/04-svelte.mdx --- --- title: Svelte description: Welcome to the AI SDK quickstart guide for Svelte! --- # Svelte Quickstart The AI SDK is a powerful Typescript library designed to help developers build AI-powered applications. In this quickstart tutorial, you'll build a simple AI-chatbot with a streaming user interface. Along the way, you'll learn key concepts and techniques that are fundamental to using the SDK in your own projects. If you are unfamiliar with the concepts of [Prompt Engineering](/docs/advanced/prompt-engineering) and [HTTP Streaming](/docs/advanced/why-streaming), you can optionally read these documents first. ## Prerequisites To follow this quickstart, you'll need: - Node.js 18+ and pnpm installed on your local development machine. - An OpenAI API key. If you haven't obtained your OpenAI API key, you can do so by [signing up](https://platform.openai.com/signup/) on the OpenAI website. ## Set Up Your Application <Note>This guide applies to SvelteKit versions 4 and below.</Note> Start by creating a new SvelteKit application. This command will create a new directory named `my-ai-app` and set up a basic SvelteKit application inside it. <Snippet text="npx sv create my-ai-app" /> Navigate to the newly created directory: <Snippet text="cd my-ai-app" /> ### Install Dependencies Install `ai` and `@ai-sdk/openai`, the AI SDK's OpenAI provider. <Note> The AI SDK is designed to be a unified interface to interact with any large language model. This means that you can change model and providers with just one line of code! Learn more about [available providers](/providers) and [building custom providers](/providers/community-providers/custom-providers) in the [providers](/providers) section. </Note> <div className="my-4"> <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add -D ai @ai-sdk/openai @ai-sdk/svelte zod" dark /> </Tab> <Tab> <Snippet text="npm install -D ai @ai-sdk/openai @ai-sdk/svelte zod" dark /> </Tab> <Tab> <Snippet text="yarn add -D ai @ai-sdk/openai @ai-sdk/svelte zod" dark /> </Tab> </Tabs> </div> ### Configure OpenAI API Key Create a `.env.local` file in your project root and add your OpenAI API Key. This key is used to authenticate your application with the OpenAI service. <Snippet text="touch .env.local" /> Edit the `.env.local` file: ```env filename=".env.local" OPENAI_API_KEY=xxxxxxxxx ``` Replace `xxxxxxxxx` with your actual OpenAI API key. <Note className="mb-4"> Vite does not automatically load environment variables onto `process.env`, so you'll need to import `OPENAI_API_KEY` from `$env/static/private` in your code (see below). </Note> ## Create an API route Create a SvelteKit Endpoint, `src/routes/api/chat/+server.ts` and add the following code: ```tsx filename="src/routes/api/chat/+server.ts" import { createOpenAI } from '@ai-sdk/openai'; import { streamText, type UIMessage, convertToModelMessages } from 'ai'; import { OPENAI_API_KEY } from '$env/static/private'; const openai = createOpenAI({ apiKey: OPENAI_API_KEY, }); export async function POST({ request }) { const { messages }: { messages: UIMessage[] } = await request.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } ``` <Note> If you see type errors with `OPENAI_API_KEY` or your `POST` function, run the dev server. </Note> Let's take a look at what is happening in this code: 1. Create an OpenAI provider instance with the `createOpenAI` function from the `@ai-sdk/openai` package. 2. Define a `POST` request handler and extract `messages` from the body of the request. The `messages` variable contains a history of the conversation between you and the chatbot and provides the chatbot with the necessary context to make the next generation. The `messages` are of UIMessage type, which are designed for use in application UI - they contain the entire message history and associated metadata like timestamps. 3. Call [`streamText`](/docs/reference/ai-sdk-core/stream-text), which is imported from the `ai` package. This function accepts a configuration object that contains a `model` provider (defined in step 1) and `messages` (defined in step 2). You can pass additional [settings](/docs/ai-sdk-core/settings) to further customise the model's behaviour. The `messages` key expects a `ModelMessage[]` array. This type is different from `UIMessage` in that it does not include metadata, such as timestamps or sender information. To convert between these types, we use the `convertToModelMessages` function, which strips the UI-specific metadata and transforms the `UIMessage[]` array into the `ModelMessage[]` format that the model expects. 4. The `streamText` function returns a [`StreamTextResult`](/docs/reference/ai-sdk-core/stream-text#result-object). This result object contains the [ `toUIMessageStreamResponse` ](/docs/reference/ai-sdk-core/stream-text#to-data-stream-response) function which converts the result to a streamed response object. 5. Return the result to the client to stream the response. ## Wire up the UI Now that you have an API route that can query an LLM, it's time to set up your frontend. The AI SDK's [UI](/docs/ai-sdk-ui) package abstracts the complexity of a chat interface into one class, `Chat`. Its properties and API are largely the same as React's [`useChat`](/docs/reference/ai-sdk-ui/use-chat). Update your root page (`src/routes/+page.svelte`) with the following code to show a list of chat messages and provide a user message input: ```svelte filename="src/routes/+page.svelte" <script lang="ts"> import { Chat } from '@ai-sdk/svelte'; let input = ''; const chat = new Chat({}); function handleSubmit(event: SubmitEvent) { event.preventDefault(); chat.sendMessage({ text: input }); input = ''; } </script> <main> <ul> {#each chat.messages as message, messageIndex (messageIndex)} <li> <div>{message.role}</div> <div> {#each message.parts as part, partIndex (partIndex)} {#if part.type === 'text'} <div>{part.text}</div> {/if} {/each} </div> </li> {/each} </ul> <form onsubmit={handleSubmit}> <input bind:value={input} /> <button type="submit">Send</button> </form> </main> ``` This page utilizes the `Chat` class, which will, by default, use the `POST` route handler you created earlier. The class 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 `parts` properties). - `sendMessage` - a function to send a message to the chat API. The component uses local state to manage the input field value, and handles form submission by calling `sendMessage` with the input text and then clearing the input field. 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: <Snippet text="pnpm run dev" /> Head to your browser and open http://localhost:5173. You should see an input field. Test it out by entering a message and see the AI chatbot respond in real-time! The AI SDK makes it fast and easy to build AI chat interfaces with Svelte. ## Enhance Your Chatbot with Tools While large language models (LLMs) have incredible generation capabilities, they struggle with discrete tasks (e.g. mathematics) and interacting with the outside world (e.g. getting the weather). This is where [tools](/docs/ai-sdk-core/tools-and-tool-calling) come in. Tools are actions that an LLM can invoke. The results of these actions can be reported back to the LLM to be considered in the next response. For example, if a user asks about the current weather, without tools, the model would only be able to provide general information based on its training data. But with a weather tool, it can fetch and provide up-to-date, location-specific weather information. Let's enhance your chatbot by adding a simple weather tool. ### Update Your API Route Modify your `src/routes/api/chat/+server.ts` file to include the new weather tool: ```tsx filename="src/routes/api/chat/+server.ts" highlight="2,3,17-31" import { createOpenAI } from '@ai-sdk/openai'; import { streamText, type UIMessage, convertToModelMessages, tool, stepCountIs, } from 'ai'; import { z } from 'zod'; import { OPENAI_API_KEY } from '$env/static/private'; const openai = createOpenAI({ apiKey: OPENAI_API_KEY, }); export async function POST({ request }) { const { messages }: { messages: UIMessage[] } = await request.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), }, }); return result.toUIMessageStreamResponse(); } ``` In this updated code: 1. You import the `tool` function from the `ai` package and `z` from `zod` for schema validation. 2. You define a `tools` object with a `weather` tool. This tool: - Has a description that helps the model understand when to use it. - Defines `inputSchema` using a Zod schema, specifying that it requires a `location` string to execute this tool. The model will attempt to extract this input 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 input. The `execute` function will then be automatically run, and the tool output will be added to the `messages` as a `tool` message. 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 on the client via the `tool-weather` part of the `message.parts` array. <Note> Tool parts are always named `tool-{toolName}`, where `{toolName}` is the key you used when defining the tool. In this case, since we defined the tool as `weather`, the part type is `tool-weather`. </Note> ### Update the UI To display the tool invocation in your UI, update your `src/routes/+page.svelte` file: ```svelte filename="src/routes/+page.svelte" <script lang="ts"> import { Chat } from '@ai-sdk/svelte'; let input = ''; const chat = new Chat({}); function handleSubmit(event: SubmitEvent) { event.preventDefault(); chat.sendMessage({ text: input }); input = ''; } </script> <main> <ul> {#each chat.messages as message, messageIndex (messageIndex)} <li> <div>{message.role}</div> <div> {#each message.parts as part, partIndex (partIndex)} {#if part.type === 'text'} <div>{part.text}</div> {:else if part.type === 'tool-weather'} <pre>{JSON.stringify(part, null, 2)}</pre> {/if} {/each} </div> </li> {/each} </ul> <form onsubmit={handleSubmit}> <input bind:value={input} /> <button type="submit">Send</button> </form> </main> ``` With this change, you're updating the UI to handle different message parts. For text parts, you display the text content as before. For weather 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 call and its result displayed in your chat interface. ## Enabling Multi-Step Tool Calls You may have noticed that while the tool is now visible in the chat interface, the model isn't using this information to answer your original query. This is because once the model generates a tool call, it has technically completed its generation. To solve this, you can enable multi-step tool calls using `stopWhen`. By default, `stopWhen` is set to `stepCountIs(1)`, which means generation stops after the first step when there are tool results. By changing this condition, you can allow the model to automatically send tool results back to itself to trigger additional generations until your specified stopping condition is met. In this case, you want the model to continue generating so it can use the weather tool results to answer your original question. ### Update Your API Route Modify your `src/routes/api/chat/+server.ts` file to include the `stopWhen` condition: ```ts filename="src/routes/api/chat/+server.ts" highlight="15" import { createOpenAI } from '@ai-sdk/openai'; import { streamText, type UIMessage, convertToModelMessages, tool, stepCountIs, } from 'ai'; import { z } from 'zod'; import { OPENAI_API_KEY } from '$env/static/private'; const openai = createOpenAI({ apiKey: OPENAI_API_KEY, }); export async function POST({ request }) { const { messages }: { messages: UIMessage[] } = await request.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), }, }); return result.toUIMessageStreamResponse(); } ``` Head back to the browser and ask about the weather in a location. You should now see the model using the weather tool results to answer your question. By setting `stopWhen: stepCountIs(5)`, you're allowing the model to use up to 5 "steps" for any given generation. This enables more complex interactions and allows the model to gather and process information over several steps if needed. You can see this in action by adding another tool to convert the temperature from Fahrenheit to Celsius. ### Add another tool Update your `src/routes/api/chat/+server.ts` file to add a new tool to convert the temperature from Fahrenheit to Celsius: ```tsx filename="src/routes/api/chat/+server.ts" highlight="32-45" import { createOpenAI } from '@ai-sdk/openai'; import { streamText, type UIMessage, convertToModelMessages, tool, stepCountIs, } from 'ai'; import { z } from 'zod'; import { OPENAI_API_KEY } from '$env/static/private'; const openai = createOpenAI({ apiKey: OPENAI_API_KEY, }); export async function POST({ request }) { const { messages }: { messages: UIMessage[] } = await request.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), convertFahrenheitToCelsius: tool({ description: 'Convert a temperature in fahrenheit to celsius', inputSchema: z.object({ temperature: z .number() .describe('The temperature in fahrenheit to convert'), }), execute: async ({ temperature }) => { const celsius = Math.round((temperature - 32) * (5 / 9)); return { celsius, }; }, }), }, }); return result.toUIMessageStreamResponse(); } ``` ### Update Your Frontend Update your UI to handle the new temperature conversion tool by modifying the tool part handling: ```svelte filename="src/routes/+page.svelte" highlight="17" <script lang="ts"> import { Chat } from '@ai-sdk/svelte'; let input = ''; const chat = new Chat({}); function handleSubmit(event: SubmitEvent) { event.preventDefault(); chat.sendMessage({ text: input }); input = ''; } </script> <main> <ul> {#each chat.messages as message, messageIndex (messageIndex)} <li> <div>{message.role}</div> <div> {#each message.parts as part, partIndex (partIndex)} {#if part.type === 'text'} <div>{part.text}</div> {:else if part.type === 'tool-weather' || part.type === 'tool-convertFahrenheitToCelsius'} <pre>{JSON.stringify(part, null, 2)}</pre> {/if} {/each} </div> </li> {/each} </ul> <form onsubmit={handleSubmit}> <input bind:value={input} /> <button type="submit">Send</button> </form> </main> ``` This update handles the new `tool-convertFahrenheitToCelsius` part type, displaying the temperature conversion tool calls and results in the UI. Now, when you ask "What's the weather in New York in celsius?", you should see a more complete interaction: 1. The model will call the weather tool for New York. 2. You'll see the tool output displayed. 3. It will then call the temperature conversion tool to convert the temperature from Fahrenheit to Celsius. 4. The model will then use that information to provide a natural language response about the weather in New York. This multi-step approach allows the model to gather information and use it to provide more accurate and contextual responses, making your chatbot considerably more useful. This simple example demonstrates how tools can expand your model's capabilities. You can create more complex tools to integrate with real APIs, databases, or any other external systems, allowing the model to access and process real-world data in real-time. Tools bridge the gap between the model's knowledge cutoff and current information. ## How does `@ai-sdk/svelte` differ from `@ai-sdk/react`? The surface-level difference is that Svelte uses classes to manage state, whereas React uses hooks, so `useChat` in React is `Chat` in Svelte. Other than that, there are a few things to keep in mind: ### 1. Arguments to classes aren't reactive by default Unlike in React, where hooks are rerun any time their containing component is invalidated, code in the `script` block of a Svelte component is only run once when the component is created. This means that, if you want arguments to your class to be reactive, you need to make sure you pass a _reference_ into the class, rather than a value: ```svelte <script> import { Chat } from '@ai-sdk/svelte'; let { id } = $props(); // won't work; the class instance will be created once, `id` will be copied by value, and won't update when $props.id changes let chat = new Chat({ id }); // will work; passes `id` by reference, so `Chat` always has the latest value let chat = new Chat({ get id() { return id; }, }); </script> ``` Keep in mind that this normally doesn't matter; most parameters you'll pass into the Chat class are static (for example, you typically wouldn't expect your `onError` handler to change). ### 2. You can't destructure class properties In vanilla JavaScript, destructuring class properties copies them by value and "disconnects" them from their class instance: ```js const classInstance = new Whatever(); classInstance.foo = 'bar'; const { foo } = classInstance; classInstance.foo = 'baz'; console.log(foo); // 'bar' ``` The same is true of classes in Svelte: ```svelte <script> import { Chat } from '@ai-sdk/svelte'; const chat = new Chat({}); let { messages } = chat; chat.append({ content: 'Hello, world!', role: 'user' }).then(() => { console.log(messages); // [] console.log(chat.messages); // [{ content: 'Hello, world!', role: 'user' }] (plus some other stuff) }); </script> ``` ### 3. Instance synchronization requires context In React, hook instances with the same `id` are synchronized -- so two instances of `useChat` will have the same `messages`, `status`, etc. if they have the same `id`. For most use cases, you probably don't need this behavior -- but if you do, you can create a context in your root layout file using `createAIContext`: ```svelte <script> import { createAIContext } from '@ai-sdk/svelte'; let { children } = $props(); createAIContext(); // all hooks created after this or in components that are children of this component // will have synchronized state </script> {@render children()} ``` ## Where to Next? You've built an AI chatbot using the AI SDK! From here, you have several paths to explore: - To learn more about the AI SDK, read through the [documentation](/docs). - If you're interested in diving deeper with guides, check out the [RAG (retrieval-augmented generation)](/docs/guides/rag-chatbot) and [multi-modal chatbot](/docs/guides/multi-modal-chatbot) guides. - To jumpstart your first AI project, explore available [templates](https://vercel.com/templates?type=ai). - To learn more about Svelte, check out the [official documentation](https://svelte.dev/docs/svelte). --- File: /ai/content/docs/02-getting-started/05-nuxt.mdx --- --- title: Vue.js (Nuxt) description: Welcome to the AI SDK quickstart guide for Vue.js (Nuxt)! --- # Vue.js (Nuxt) Quickstart The AI SDK is a powerful Typescript library designed to help developers build AI-powered applications. In this quickstart tutorial, you'll build a simple AI-chatbot with a streaming user interface. Along the way, you'll learn key concepts and techniques that are fundamental to using the SDK in your own projects. If you are unfamiliar with the concepts of [Prompt Engineering](/docs/advanced/prompt-engineering) and [HTTP Streaming](/docs/advanced/why-streaming), you can optionally read these documents first. ## Prerequisites To follow this quickstart, you'll need: - Node.js 18+ and pnpm installed on your local development machine. - An OpenAI API key. If you haven't obtained your OpenAI API key, you can do so by [signing up](https://platform.openai.com/signup/) on the OpenAI website. ## Setup Your Application 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. <Snippet text="pnpm create nuxt my-ai-app" /> Navigate to the newly created directory: <Snippet text="cd my-ai-app" /> ### Install dependencies Install `ai` and `@ai-sdk/openai`, the AI SDK's OpenAI provider. <Note> The AI SDK is designed to be a unified interface to interact with any large language model. This means that you can change model and providers with just one line of code! Learn more about [available providers](/providers) and [building custom providers](/providers/community-providers/custom-providers) in the [providers](/providers) section. </Note> <div className="my-4"> <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add ai @ai-sdk/openai @ai-sdk/vue zod" dark /> </Tab> <Tab> <Snippet text="npm install ai @ai-sdk/openai @ai-sdk/vue zod" dark /> </Tab> <Tab> <Snippet text="yarn add ai @ai-sdk/openai @ai-sdk/vue zod" dark /> </Tab> </Tabs> </div> ### Configure OpenAI API key Create a `.env` file in your project root and add your OpenAI API Key. This key is used to authenticate your application with the OpenAI service. <Snippet text="touch .env" /> Edit the `.env` file: ```env filename=".env" NUXT_OPENAI_API_KEY=xxxxxxxxx ``` Replace `xxxxxxxxx` with your actual OpenAI API key and configure the environment variable in `nuxt.config.ts`: ```ts filename="nuxt.config.ts" export default defineNuxtConfig({ // rest of your nuxt config runtimeConfig: { openaiApiKey: '', }, }); ``` <Note className="mb-4"> The AI SDK's OpenAI Provider will default to using the `OPENAI_API_KEY` environment variable. </Note> ## Create an API route Create an API route, `server/api/chat.ts` and add the following code: ```typescript filename="server/api/chat.ts" import { streamText, UIMessage, convertToModelMessages } from 'ai'; import { createOpenAI } from '@ai-sdk/openai'; export default defineLazyEventHandler(async () => { const apiKey = useRuntimeConfig().openaiApiKey; if (!apiKey) throw new Error('Missing OpenAI API key'); const openai = createOpenAI({ apiKey: apiKey, }); return defineEventHandler(async (event: any) => { const { messages }: { messages: UIMessage[] } = await readBody(event); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); }); }); ``` Let's take a look at what is happening in this code: 1. Create an OpenAI provider instance with the `createOpenAI` function from the `@ai-sdk/openai` package. 2. Define an Event Handler and extract `messages` from the body of the request. The `messages` variable contains a history of the conversation between you and the chatbot and provides the chatbot with the necessary context to make the next generation. The `messages` are of UIMessage type, which are designed for use in application UI - they contain the entire message history and associated metadata like timestamps. 3. Call [`streamText`](/docs/reference/ai-sdk-core/stream-text), which is imported from the `ai` package. This function accepts a configuration object that contains a `model` provider (defined in step 1) and `messages` (defined in step 2). You can pass additional [settings](/docs/ai-sdk-core/settings) to further customise the model's behaviour. The `messages` key expects a `ModelMessage[]` array. This type is different from `UIMessage` in that it does not include metadata, such as timestamps or sender information. To convert between these types, we use the `convertToModelMessages` function, which strips the UI-specific metadata and transforms the `UIMessage[]` array into the `ModelMessage[]` format that the model expects. 4. The `streamText` function returns a [`StreamTextResult`](/docs/reference/ai-sdk-core/stream-text#result). This result object contains the [ `toDataStreamResponse` ](/docs/reference/ai-sdk-core/stream-text#to-data-stream-response) function which converts the result to a streamed response object. 5. Return the result to the client to stream the response. ## Wire up the UI Now that you have an API route that can query an LLM, it's time to setup your frontend. The AI SDK's [ UI ](/docs/ai-sdk-ui/overview) package abstract the complexity of a chat interface into one hook, [`useChat`](/docs/reference/ai-sdk-ui/use-chat). Update your root page (`pages/index.vue`) with the following code to show a list of chat messages and provide a user message input: ```typescript filename="pages/index.vue" <script setup lang="ts"> import { Chat } from "@ai-sdk/vue"; import { ref } from "vue"; const input = ref(""); const chat = new Chat({}); const handleSubmit = (e: Event) => { e.preventDefault(); chat.sendMessage({ text: input.value }); input.value = ""; }; </script> <template> <div> <div v-for="(m, index) in chat.messages" :key="m.id ? m.id : index"> {{ m.role === "user" ? "User: " : "AI: " }} <div v-for="(part, index) in m.parts" :key="`${m.id}-${part.type}-${index}`" > <div v-if="part.type === 'text'">{{ part.text }}</div> </div> </div> <form @submit="handleSubmit"> <input v-model="input" placeholder="Say something..." /> </form> </div> </template> ``` <Note> If your project has `app.vue` instead of `pages/index.vue`, delete the `app.vue` file and create a new `pages/index.vue` file with the code above. </Note> This page utilizes the `useChat` hook, which will, by default, use the API route you created earlier (`/api/chat`). The hook provides functions and state for handling user input and form submission. The `useChat` hook provides multiple utility functions and state variables: - `messages` - the current chat messages (an array of objects with `id`, `role`, and `parts` properties). - `sendMessage` - a function to send a message to the chat API. The component uses local state (`ref`) to manage the input field value, and handles form submission by calling `sendMessage` with the input text and then clearing the input field. 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: <Snippet text="pnpm run dev" /> Head to your browser and open http://localhost:3000. You should see an input field. Test it out by entering a message and see the AI chatbot respond in real-time! The AI SDK makes it fast and easy to build AI chat interfaces with Nuxt. ## Enhance Your Chatbot with Tools While large language models (LLMs) have incredible generation capabilities, they struggle with discrete tasks (e.g. mathematics) and interacting with the outside world (e.g. getting the weather). This is where [tools](/docs/ai-sdk-core/tools-and-tool-calling) come in. Tools are actions that an LLM can invoke. The results of these actions can be reported back to the LLM to be considered in the next response. For example, if a user asks about the current weather, without tools, the model would only be able to provide general information based on its training data. But with a weather tool, it can fetch and provide up-to-date, location-specific weather information. Let's enhance your chatbot by adding a simple weather tool. ### Update Your API Route Modify your `server/api/chat.ts` file to include the new weather tool: ```typescript filename="server/api/chat.ts" highlight="1,18-34" import { streamText, UIMessage, convertToModelMessages, tool } from 'ai'; import { createOpenAI } from '@ai-sdk/openai'; import { z } from 'zod'; export default defineLazyEventHandler(async () => { const apiKey = useRuntimeConfig().openaiApiKey; if (!apiKey) throw new Error('Missing OpenAI API key'); const openai = createOpenAI({ apiKey: apiKey, }); return defineEventHandler(async (event: any) => { const { messages }: { messages: UIMessage[] } = await readBody(event); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), }, }); return result.toUIMessageStreamResponse(); }); }); ``` In this updated code: 1. You import the `tool` function from the `ai` package and `z` from `zod` for schema validation. 2. You define a `tools` object with a `weather` tool. This tool: - Has a description that helps the model understand when to use it. - Defines `inputSchema` using a Zod schema, specifying that it requires a `location` string to execute this tool. The model will attempt to extract this input 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 input. The `execute` function will then be automatically run, and the tool output will be added to the `messages` as a `tool` message. 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 on the client via the `tool-weather` part of the `message.parts` array. <Note> Tool parts are always named `tool-{toolName}`, where `{toolName}` is the key you used when defining the tool. In this case, since we defined the tool as `weather`, the part type is `tool-weather`. </Note> ### Update the UI To display the tool invocation in your UI, update your `pages/index.vue` file: ```typescript filename="pages/index.vue" highlight="16-18" <script setup lang="ts"> import { Chat } from "@ai-sdk/vue"; import { ref } from "vue"; const input = ref(""); const chat = new Chat({}); const handleSubmit = (e: Event) => { e.preventDefault(); chat.sendMessage({ text: input.value }); input.value = ""; }; </script> <template> <div> <div v-for="(m, index) in chat.messages" :key="m.id ? m.id : index"> {{ m.role === "user" ? "User: " : "AI: " }} <div v-for="(part, index) in m.parts" :key="`${m.id}-${part.type}-${index}`" > <div v-if="part.type === 'text'">{{ part.text }}</div> <pre v-if="part.type === 'tool-weather'">{{ JSON.stringify(part, null, 2) }}</pre> </div> </div> <form @submit="handleSubmit"> <input v-model="input" placeholder="Say something..." /> </form> </div> </template> ``` With this change, you're updating the UI to handle different message parts. For text parts, you display the text content as before. For weather 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 call and its result displayed in your chat interface. ## Enabling Multi-Step Tool Calls You may have noticed that while the tool is now visible in the chat interface, the model isn't using this information to answer your original query. This is because once the model generates a tool call, it has technically completed its generation. To solve this, you can enable multi-step tool calls using `stopWhen`. By default, `stopWhen` is set to `stepCountIs(1)`, which means generation stops after the first step when there are tool results. By changing this condition, you can allow the model to automatically send tool results back to itself to trigger additional generations until your specified stopping condition is met. In this case, you want the model to continue generating so it can use the weather tool results to answer your original question. ### Update Your API Route Modify your `server/api/chat.ts` file to include the `stopWhen` condition: ```typescript filename="server/api/chat.ts" highlight="24" import { streamText, UIMessage, convertToModelMessages, tool, stepCountIs, } from 'ai'; import { createOpenAI } from '@ai-sdk/openai'; import { z } from 'zod'; export default defineLazyEventHandler(async () => { const apiKey = useRuntimeConfig().openaiApiKey; if (!apiKey) throw new Error('Missing OpenAI API key'); const openai = createOpenAI({ apiKey: apiKey, }); return defineEventHandler(async (event: any) => { const { messages }: { messages: UIMessage[] } = await readBody(event); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), }, }); return result.toUIMessageStreamResponse(); }); }); ``` Head back to the browser and ask about the weather in a location. You should now see the model using the weather tool results to answer your question. By setting `stopWhen: stepCountIs(5)`, you're allowing the model to use up to 5 "steps" for any given generation. This enables more complex interactions and allows the model to gather and process information over several steps if needed. You can see this in action by adding another tool to convert the temperature from Fahrenheit to Celsius. ### Add another tool Update your `server/api/chat.ts` file to add a new tool to convert the temperature from Fahrenheit to Celsius: ```typescript filename="server/api/chat.ts" highlight="34-47" import { streamText, UIMessage, convertToModelMessages, tool, stepCountIs, } from 'ai'; import { createOpenAI } from '@ai-sdk/openai'; import { z } from 'zod'; export default defineLazyEventHandler(async () => { const apiKey = useRuntimeConfig().openaiApiKey; if (!apiKey) throw new Error('Missing OpenAI API key'); const openai = createOpenAI({ apiKey: apiKey, }); return defineEventHandler(async (event: any) => { const { messages }: { messages: UIMessage[] } = await readBody(event); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), convertFahrenheitToCelsius: tool({ description: 'Convert a temperature in fahrenheit to celsius', inputSchema: z.object({ temperature: z .number() .describe('The temperature in fahrenheit to convert'), }), execute: async ({ temperature }) => { const celsius = Math.round((temperature - 32) * (5 / 9)); return { celsius, }; }, }), }, }); return result.toUIMessageStreamResponse(); }); }); ``` ### Update Your Frontend Update your UI to handle the new temperature conversion tool by modifying the tool part handling: ```typescript filename="pages/index.vue" highlight="24" <script setup lang="ts"> import { Chat } from "@ai-sdk/vue"; import { ref } from "vue"; const input = ref(""); const chat = new Chat({}); const handleSubmit = (e: Event) => { e.preventDefault(); chat.sendMessage({ text: input.value }); input.value = ""; }; </script> <template> <div> <div v-for="(m, index) in chat.messages" :key="m.id ? m.id : index"> {{ m.role === "user" ? "User: " : "AI: " }} <div v-for="(part, index) in m.parts" :key="`${m.id}-${part.type}-${index}`" > <div v-if="part.type === 'text'">{{ part.text }}</div> <pre v-if=" part.type === 'tool-weather' || part.type === 'tool-convertFahrenheitToCelsius' " >{{ JSON.stringify(part, null, 2) }}</pre > </div> </div> <form @submit="handleSubmit"> <input v-model="input" placeholder="Say something..." /> </form> </div> </template> ``` This update handles the new `tool-convertFahrenheitToCelsius` part type, displaying the temperature conversion tool calls and results in the UI. Now, when you ask "What's the weather in New York in celsius?", you should see a more complete interaction: 1. The model will call the weather tool for New York. 2. You'll see the tool output displayed. 3. It will then call the temperature conversion tool to convert the temperature from Fahrenheit to Celsius. 4. The model will then use that information to provide a natural language response about the weather in New York. This multi-step approach allows the model to gather information and use it to provide more accurate and contextual responses, making your chatbot considerably more useful. This simple example demonstrates how tools can expand your model's capabilities. You can create more complex tools to integrate with real APIs, databases, or any other external systems, allowing the model to access and process real-world data in real-time. Tools bridge the gap between the model's knowledge cutoff and current information. ## Where to Next? You've built an AI chatbot using the AI SDK! From here, you have several paths to explore: - To learn more about the AI SDK, read through the [documentation](/docs). - If you're interested in diving deeper with guides, check out the [RAG (retrieval-augmented generation)](/docs/guides/rag-chatbot) and [multi-modal chatbot](/docs/guides/multi-modal-chatbot) guides. - To jumpstart your first AI project, explore available [templates](https://vercel.com/templates?type=ai). --- File: /ai/content/docs/02-getting-started/06-nodejs.mdx --- --- title: Node.js description: Welcome to the AI SDK quickstart guide for Node.js! --- # Node.js Quickstart The AI SDK is a powerful Typescript library designed to help developers build AI-powered applications. In this quickstart tutorial, you'll build a simple AI-chatbot with a streaming user interface. Along the way, you'll learn key concepts and techniques that are fundamental to using the SDK in your own projects. If you are unfamiliar with the concepts of [Prompt Engineering](/docs/advanced/prompt-engineering) and [HTTP Streaming](/docs/advanced/why-streaming), you can optionally read these documents first. ## Prerequisites To follow this quickstart, you'll need: - Node.js 18+ and pnpm installed on your local development machine. - An OpenAI API key. If you haven't obtained your OpenAI API key, you can do so by [signing up](https://platform.openai.com/signup/) on the OpenAI website. ## Setup Your Application Start by creating a new directory using the `mkdir` command. Change into your new directory and then run the `pnpm init` command. This will create a `package.json` in your new directory. ```bash mkdir my-ai-app cd my-ai-app pnpm init ``` ### Install Dependencies Install `ai` and `@ai-sdk/openai`, the AI SDK's OpenAI provider, along with other necessary dependencies. <Note> The AI SDK is designed to be a unified interface to interact with any large language model. This means that you can change model and providers with just one line of code! Learn more about [available providers](/providers) and [building custom providers](/providers/community-providers/custom-providers) in the [providers](/providers) section. </Note> ```bash pnpm add ai@beta @ai-sdk/openai@beta zod dotenv pnpm add -D @types/node tsx typescript ``` The `ai` and `@ai-sdk/openai` packages contain the AI SDK and the [ AI SDK OpenAI provider](/providers/ai-sdk-providers/openai), respectively. You will use `zod` to define type-safe schemas that you will pass to the large language model (LLM). You will use `dotenv` to access environment variables (your OpenAI key) within your application. There are also three development dependencies, installed with the `-D` flag, that are necessary to run your Typescript code. ### Configure OpenAI API key Create a `.env` file in your project's root directory and add your OpenAI API Key. This key is used to authenticate your application with the OpenAI service. <Snippet text="touch .env" /> Edit the `.env` file: ```env filename=".env" OPENAI_API_KEY=xxxxxxxxx ``` Replace `xxxxxxxxx` with your actual OpenAI API key. <Note className="mb-4"> The AI SDK's OpenAI Provider will default to using the `OPENAI_API_KEY` environment variable. </Note> ## Create Your Application Create an `index.ts` file in the root of your project and add the following code: ```ts filename="index.ts" import { openai } from '@ai-sdk/openai'; import { ModelMessage, streamText } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const result = streamText({ model: openai('gpt-4o'), messages, }); let fullResponse = ''; process.stdout.write('\nAssistant: '); for await (const delta of result.textStream) { fullResponse += delta; process.stdout.write(delta); } process.stdout.write('\n\n'); messages.push({ role: 'assistant', content: fullResponse }); } } main().catch(console.error); ``` Let's take a look at what is happening in this code: 1. Set up a readline interface to take input from the terminal, enabling interactive sessions directly from the command line. 2. Initialize an array called `messages` to store the history of your conversation. This history allows the model to maintain context in ongoing dialogues. 3. In the `main` function: - Prompt for and capture user input, storing it in `userInput`. - Add user input to the `messages` array as a user message. - Call [`streamText`](/docs/reference/ai-sdk-core/stream-text), which is imported from the `ai` package. This function accepts a configuration object that contains a `model` provider and `messages`. - Iterate over the text stream returned by the `streamText` function (`result.textStream`) and print the contents of the stream to the terminal. - Add the assistant's response to the `messages` array. ## Running Your Application With that, you have built everything you need for your chatbot! To start your application, use the command: <Snippet text="pnpm tsx index.ts" /> You should see a prompt in your terminal. Test it out by entering a message and see the AI chatbot respond in real-time! The AI SDK makes it fast and easy to build AI chat interfaces with Node.js. ## Enhance Your Chatbot with Tools While large language models (LLMs) have incredible generation capabilities, they struggle with discrete tasks (e.g. mathematics) and interacting with the outside world (e.g. getting the weather). This is where [tools](/docs/ai-sdk-core/tools-and-tool-calling) come in. Tools are actions that an LLM can invoke. The results of these actions can be reported back to the LLM to be considered in the next response. For example, if a user asks about the current weather, without tools, the model would only be able to provide general information based on its training data. But with a weather tool, it can fetch and provide up-to-date, location-specific weather information. Let's enhance your chatbot by adding a simple weather tool. ### Update Your Application Modify your `index.ts` file to include the new weather tool: ```ts filename="index.ts" highlight="2,4,25-38" import { openai } from '@ai-sdk/openai'; import { ModelMessage, streamText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod'; import * as readline from 'node:readline/promises'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const result = streamText({ model: openai('gpt-4o'), messages, tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), }, }); let fullResponse = ''; process.stdout.write('\nAssistant: '); for await (const delta of result.textStream) { fullResponse += delta; process.stdout.write(delta); } process.stdout.write('\n\n'); messages.push({ role: 'assistant', content: fullResponse }); } } main().catch(console.error); ``` In this updated code: 1. You import the `tool` function from the `ai` package. 2. You define a `tools` object with a `weather` tool. This tool: - Has a description that helps the model understand when to use it. - Defines `inputSchema` using a Zod schema, specifying that it requires a `location` string to execute this tool. The model will attempt to extract this input 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 the results will be used by the model to generate its response. Try asking something like "What's the weather in New York?" and see how the model uses the new tool. Notice the blank "assistant" response? 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 `toolCall` and `toolResult` keys of the result object. ```typescript highlight="47-48" import { openai } from '@ai-sdk/openai'; import { ModelMessage, streamText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod'; import * as readline from 'node:readline/promises'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const result = streamText({ model: openai('gpt-4o'), messages, tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), }, }); let fullResponse = ''; process.stdout.write('\nAssistant: '); for await (const delta of result.textStream) { fullResponse += delta; process.stdout.write(delta); } process.stdout.write('\n\n'); console.log(await result.toolCalls); console.log(await result.toolResults); messages.push({ role: 'assistant', content: fullResponse }); } } main().catch(console.error); ``` Now, when you ask about the weather, you'll see the tool call and its result displayed in your chat interface. ## Enabling Multi-Step Tool Calls You may have noticed that while the tool results are visible in the chat interface, the model isn't using this information to answer your original query. This is because once the model generates a tool call, it has technically completed its generation. To solve this, you can enable multi-step tool calls using `stopWhen`. This feature will automatically send tool results back to the model to trigger an additional generation until the stopping condition you define is met. In this case, you want the model to answer your question using the results from the weather tool. ### Update Your Application Modify your `index.ts` file to configure stopping conditions with `stopWhen`: ```ts filename="index.ts" highlight="39-42" import { openai } from '@ai-sdk/openai'; import { ModelMessage, streamText, tool, stepCountIs } from 'ai'; import 'dotenv/config'; import { z } from 'zod'; import * as readline from 'node:readline/promises'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const result = streamText({ model: openai('gpt-4o'), messages, tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), }, stopWhen: stepCountIs(5), onStepFinish: async ({ toolResults }) => { if (toolResults.length) { console.log(JSON.stringify(toolResults, null, 2)); } }, }); let fullResponse = ''; process.stdout.write('\nAssistant: '); for await (const delta of result.textStream) { fullResponse += delta; process.stdout.write(delta); } process.stdout.write('\n\n'); messages.push({ role: 'assistant', content: fullResponse }); } } main().catch(console.error); ``` In this updated code: 1. You set `stopWhen` to be when `stepCountIs` 5, allowing the model to use up to 5 "steps" for any given generation. 2. You add an `onStepFinish` callback to log any `toolResults` from each step of the interaction, helping you understand the model's tool usage. This means we can also delete the `toolCall` and `toolResult` `console.log` statements from the previous example. Now, when you ask about the weather in a location, you should see the model using the weather tool results to answer your question. By setting `stopWhen: stepCountIs(5)`, you're allowing the model to use up to 5 "steps" for any given generation. This enables more complex interactions and allows the model to gather and process information over several steps if needed. You can see this in action by adding another tool to convert the temperature from Celsius to Fahrenheit. ### Adding a second tool Update your `index.ts` file to add a new tool to convert the temperature from Celsius to Fahrenheit: ```ts filename="index.ts" highlight="38-49" import { openai } from '@ai-sdk/openai'; import { ModelMessage, streamText, tool, stepCountIs } from 'ai'; import 'dotenv/config'; import { z } from 'zod'; import * as readline from 'node:readline/promises'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const result = streamText({ model: openai('gpt-4o'), messages, tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), convertFahrenheitToCelsius: tool({ description: 'Convert a temperature in fahrenheit to celsius', inputSchema: z.object({ temperature: z .number() .describe('The temperature in fahrenheit to convert'), }), execute: async ({ temperature }) => { const celsius = Math.round((temperature - 32) * (5 / 9)); return { celsius, }; }, }), }, stopWhen: stepCountIs(5), onStepFinish: async ({ toolResults }) => { if (toolResults.length) { console.log(JSON.stringify(toolResults, null, 2)); } }, }); let fullResponse = ''; process.stdout.write('\nAssistant: '); for await (const delta of result.textStream) { fullResponse += delta; process.stdout.write(delta); } process.stdout.write('\n\n'); messages.push({ role: 'assistant', content: fullResponse }); } } main().catch(console.error); ``` Now, when you ask "What's the weather in New York in celsius?", you should see a more complete interaction: 1. The model will call the weather tool for New York. 2. You'll see the tool result logged. 3. It will then call the temperature conversion tool to convert the temperature from Fahrenheit to Celsius. 4. The model will then use that information to provide a natural language response about the weather in New York. This multi-step approach allows the model to gather information and use it to provide more accurate and contextual responses, making your chatbot considerably more useful. This example demonstrates how tools can expand your model's capabilities. You can create more complex tools to integrate with real APIs, databases, or any other external systems, allowing the model to access and process real-world data in real-time and perform actions that interact with the outside world. Tools bridge the gap between the model's knowledge cutoff and current information, while also enabling it to take meaningful actions beyond just generating text responses. ## Where to Next? You've built an AI chatbot using the AI SDK! From here, you have several paths to explore: - To learn more about the AI SDK, read through the [documentation](/docs). - If you're interested in diving deeper with guides, check out the [RAG (retrieval-augmented generation)](/docs/guides/rag-chatbot) and [multi-modal chatbot](/docs/guides/multi-modal-chatbot) guides. - To jumpstart your first AI project, explore available [templates](https://vercel.com/templates?type=ai). --- File: /ai/content/docs/02-getting-started/07-expo.mdx --- --- title: Expo description: Welcome to the AI SDK quickstart guide for Expo! --- # Expo Quickstart In this quickstart tutorial, you'll build a simple AI-chatbot with a streaming user interface with [Expo](https://expo.dev/). Along the way, you'll learn key concepts and techniques that are fundamental to using the SDK in your own projects. If you are unfamiliar with the concepts of [Prompt Engineering](/docs/advanced/prompt-engineering) and [HTTP Streaming](/docs/advanced/why-streaming), you can optionally read these documents first. ## Prerequisites To follow this quickstart, you'll need: - Node.js 18+ and pnpm installed on your local development machine. - An OpenAI API key. If you haven't obtained your OpenAI API key, you can do so by [signing up](https://platform.openai.com/signup/) on the OpenAI website. ## Create Your Application Start by creating a new Expo application. This command will create a new directory named `my-ai-app` and set up a basic Expo application inside it. <Snippet text="pnpm create expo-app@latest my-ai-app" /> Navigate to the newly created directory: <Snippet text="cd my-ai-app" /> <Note>This guide requires Expo 52 or higher.</Note> ### Install dependencies Install `ai`, `@ai-sdk/react` and `@ai-sdk/openai`, the AI package, the AI React package and AI SDK's [ OpenAI provider ](/providers/ai-sdk-providers/openai) respectively. <Note> The AI SDK is designed to be a unified interface to interact with any large language model. This means that you can change model and providers with just one line of code! Learn more about [available providers](/providers) and [building custom providers](/providers/community-providers/custom-providers) in the [providers](/providers) section. </Note> <div className="my-4"> <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add ai @ai-sdk/openai @ai-sdk/react zod" dark /> </Tab> <Tab> <Snippet text="npm install ai @ai-sdk/openai @ai-sdk/react zod" dark /> </Tab> <Tab> <Snippet text="yarn add ai @ai-sdk/openai @ai-sdk/react zod" dark /> </Tab> </Tabs> </div> ### Configure OpenAI API key Create a `.env.local` file in your project root and add your OpenAI API Key. This key is used to authenticate your application with the OpenAI service. <Snippet text="touch .env.local" /> Edit the `.env.local` file: ```env filename=".env.local" OPENAI_API_KEY=xxxxxxxxx ``` Replace `xxxxxxxxx` with your actual OpenAI API key. <Note className="mb-4"> The AI SDK's OpenAI Provider will default to using the `OPENAI_API_KEY` environment variable. </Note> ## Create an API Route Create a route handler, `app/api/chat+api.ts` and add the following code: ```tsx filename="app/api/chat+api.ts" import { openai } from '@ai-sdk/openai'; import { streamText, UIMessage, convertToModelMessages } 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({ headers: { 'Content-Type': 'application/octet-stream', 'Content-Encoding': 'none', }, }); } ``` Let's take a look at what is happening in this code: 1. Define an asynchronous `POST` request handler and extract `messages` from the body of the request. The `messages` variable contains a history of the conversation between you and the chatbot and provides the chatbot with the necessary context to make the next generation. 2. Call [`streamText`](/docs/reference/ai-sdk-core/stream-text), which is imported from the `ai` package. This function accepts a configuration object that contains a `model` provider (imported from `@ai-sdk/openai`) and `messages` (defined in step 1). You can pass additional [settings](/docs/ai-sdk-core/settings) to further customise the model's behaviour. 3. The `streamText` function returns a [`StreamTextResult`](/docs/reference/ai-sdk-core/stream-text#result-object). This result object contains the [ `toDataStreamResponse` ](/docs/reference/ai-sdk-core/stream-text#to-data-stream-response) function which converts the result to a streamed response object. 4. Finally, return the result to the client to stream the response. This API route creates a POST request endpoint at `/api/chat`. ## Wire up the UI Now that you have an API route that can query an LLM, it's time to setup your frontend. The AI SDK's [ UI ](/docs/ai-sdk-ui) package abstracts the complexity of a chat interface into one hook, [`useChat`](/docs/reference/ai-sdk-ui/use-chat). Update your root page (`app/(tabs)/index.tsx`) with the following code to show a list of chat messages and provide a user message input: ```tsx filename="app/(tabs)/index.tsx" import { generateAPIUrl } from '@/utils'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { fetch as expoFetch } from 'expo/fetch'; import { useState } from 'react'; import { View, TextInput, ScrollView, Text, SafeAreaView } from 'react-native'; export default function App() { const [input, setInput] = useState(''); const { messages, error, sendMessage } = useChat({ transport: new DefaultChatTransport({ fetch: expoFetch as unknown as typeof globalThis.fetch, api: generateAPIUrl('/api/chat'), }), onError: error => console.error(error, 'ERROR'), }); if (error) return <Text>{error.message}</Text>; return ( <SafeAreaView style={{ height: '100%' }}> <View style={{ height: '95%', display: 'flex', flexDirection: 'column', paddingHorizontal: 8, }} > <ScrollView style={{ flex: 1 }}> {messages.map(m => ( <View key={m.id} style={{ marginVertical: 8 }}> <View> <Text style={{ fontWeight: 700 }}>{m.role}</Text> {m.parts.map((part, i) => { switch (part.type) { case 'text': return <Text key={`${m.id}-${i}`}>{part.text}</Text>; } })} </View> </View> ))} </ScrollView> <View style={{ marginTop: 8 }}> <TextInput style={{ backgroundColor: 'white', padding: 8 }} placeholder="Say something..." value={input} onChange={e => setInput(e.nativeEvent.text)} onSubmitEditing={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} autoFocus={true} /> </View> </View> </SafeAreaView> ); } ``` This page utilizes the `useChat` hook, which will, by default, use the `POST` API route you created earlier (`/api/chat`). The hook provides functions and state for handling user input and form submission. The `useChat` hook provides multiple utility functions and state variables: - `messages` - the current chat messages (an array of objects with `id`, `role`, and `parts` properties). - `sendMessage` - a function to send a message to the chat API. The component uses local state (`useState`) to manage the input field value, and handles form submission by calling `sendMessage` with the input text and then clearing the input field. 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. <Note> You use the expo/fetch function instead of the native node fetch to enable streaming of chat responses. This requires Expo 52 or higher. </Note> ### Create the API URL Generator Because you're using expo/fetch for streaming responses instead of the native fetch function, you'll need an API URL generator to ensure you are using the correct base url and format depending on the client environment (e.g. web or mobile). Create a new file called `utils.ts` in the root of your project and add the following code: ```ts filename="utils.ts" import Constants from 'expo-constants'; export const generateAPIUrl = (relativePath: string) => { const origin = Constants.experienceUrl.replace('exp://', 'http://'); const path = relativePath.startsWith('/') ? relativePath : `/${relativePath}`; if (process.env.NODE_ENV === 'development') { return origin.concat(path); } if (!process.env.EXPO_PUBLIC_API_BASE_URL) { throw new Error( 'EXPO_PUBLIC_API_BASE_URL environment variable is not defined', ); } return process.env.EXPO_PUBLIC_API_BASE_URL.concat(path); }; ``` This utility function handles URL generation for both development and production environments, ensuring your API calls work correctly across different devices and configurations. <Note> Before deploying to production, you must set the `EXPO_PUBLIC_API_BASE_URL` environment variable in your production environment. This variable should point to the base URL of your API server. </Note> ## Running Your Application With that, you have built everything you need for your chatbot! To start your application, use the command: <Snippet text="pnpm expo" /> Head to your browser and open http://localhost:8081. You should see an input field. Test it out by entering a message and see the AI chatbot respond in real-time! The AI SDK makes it fast and easy to build AI chat interfaces with Expo. <Note> If you experience "Property `structuredClone` doesn't exist" errors on mobile, add the [polyfills described below](#polyfills). </Note> ## Enhance Your Chatbot with Tools While large language models (LLMs) have incredible generation capabilities, they struggle with discrete tasks (e.g. mathematics) and interacting with the outside world (e.g. getting the weather). This is where [tools](/docs/ai-sdk-core/tools-and-tool-calling) come in. Tools are actions that an LLM can invoke. The results of these actions can be reported back to the LLM to be considered in the next response. For example, if a user asks about the current weather, without tools, the model would only be able to provide general information based on its training data. But with a weather tool, it can fetch and provide up-to-date, location-specific weather information. Let's enhance your chatbot by adding a simple weather tool. ### Update Your API route Modify your `app/api/chat+api.ts` file to include the new weather tool: ```tsx filename="app/api/chat+api.ts" highlight="2,8,11,13-27" import { openai } from '@ai-sdk/openai'; import { streamText, UIMessage, convertToModelMessages, tool } from 'ai'; import { z } from 'zod'; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), }, }); return result.toUIMessageStreamResponse({ headers: { 'Content-Type': 'application/octet-stream', 'Content-Encoding': 'none', }, }); } ``` In this updated code: 1. You import the `tool` function from the `ai` package and `z` from `zod` for schema validation. 2. You define a `tools` object with a `weather` tool. This tool: - Has a description that helps the model understand when to use it. - Defines `inputSchema` using a Zod schema, specifying that it requires a `location` string to execute this tool. The model will attempt to extract this input 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 input. The `execute` function will then be automatically run, and the tool output will be added to the `messages` as a `tool` message. <Note> You may need to restart your development server for the changes to take effect. </Note> 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 on the client via the `tool-weather` part of the `message.parts` array. <Note> Tool parts are always named `tool-{toolName}`, where `{toolName}` is the key you used when defining the tool. In this case, since we defined the tool as `weather`, the part type is `tool-weather`. </Note> ### Update the UI To display the weather tool invocation in your UI, update your `app/(tabs)/index.tsx` file: ```tsx filename="app/(tabs)/index.tsx" highlight="31-35" import { generateAPIUrl } from '@/utils'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { fetch as expoFetch } from 'expo/fetch'; import { useState } from 'react'; import { View, TextInput, ScrollView, Text, SafeAreaView } from 'react-native'; export default function App() { const [input, setInput] = useState(''); const { messages, error, sendMessage } = useChat({ transport: new DefaultChatTransport({ fetch: expoFetch as unknown as typeof globalThis.fetch, api: generateAPIUrl('/api/chat'), }), onError: error => console.error(error, 'ERROR'), }); if (error) return <Text>{error.message}</Text>; return ( <SafeAreaView style={{ height: '100%' }}> <View style={{ height: '95%', display: 'flex', flexDirection: 'column', paddingHorizontal: 8, }} > <ScrollView style={{ flex: 1 }}> {messages.map(m => ( <View key={m.id} style={{ marginVertical: 8 }}> <View> <Text style={{ fontWeight: 700 }}>{m.role}</Text> {m.parts.map((part, i) => { switch (part.type) { case 'text': return <Text key={`${m.id}-${i}`}>{part.text}</Text>; case 'tool-weather': return ( <Text key={`${m.id}-${i}`}> {JSON.stringify(part, null, 2)} </Text> ); } })} </View> </View> ))} </ScrollView> <View style={{ marginTop: 8 }}> <TextInput style={{ backgroundColor: 'white', padding: 8 }} placeholder="Say something..." value={input} onChange={e => setInput(e.nativeEvent.text)} onSubmitEditing={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} autoFocus={true} /> </View> </View> </SafeAreaView> ); } ``` <Note> You may need to restart your development server for the changes to take effect. </Note> With this change, you're updating the UI to handle different message parts. For text parts, you display the text content as before. For weather 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 call and its result displayed in your chat interface. ## Enabling Multi-Step Tool Calls You may have noticed that while the tool results are visible in the chat interface, the model isn't using this information to answer your original query. This is because once the model generates a tool call, it has technically completed its generation. To solve this, you can enable multi-step tool calls using `stopWhen`. By default, `stopWhen` is set to `stepCountIs(1)`, which means generation stops after the first step when there are tool results. By changing this condition, you can allow the model to automatically send tool results back to itself to trigger additional generations until your specified stopping condition is met. In this case, you want the model to continue generating so it can use the weather tool results to answer your original question. ### Update Your API Route Modify your `app/api/chat+api.ts` file to include the `stopWhen` condition: ```tsx filename="app/api/chat+api.ts" highlight="11" import { openai } from '@ai-sdk/openai'; import { streamText, UIMessage, convertToModelMessages, tool, stepCountIs, } from 'ai'; import { z } from 'zod'; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), }, }); return result.toUIMessageStreamResponse({ headers: { 'Content-Type': 'application/octet-stream', 'Content-Encoding': 'none', }, }); } ``` <Note> You may need to restart your development server for the changes to take effect. </Note> Head back to the Expo app and ask about the weather in a location. You should now see the model using the weather tool results to answer your question. By setting `stopWhen: stepCountIs(5)`, you're allowing the model to use up to 5 "steps" for any given generation. This enables more complex interactions and allows the model to gather and process information over several steps if needed. You can see this in action by adding another tool to convert the temperature from Fahrenheit to Celsius. ### Add More Tools Update your `app/api/chat+api.ts` file to add a new tool to convert the temperature from Fahrenheit to Celsius: ```tsx filename="app/api/chat+api.ts" highlight="29-42" import { openai } from '@ai-sdk/openai'; import { streamText, UIMessage, convertToModelMessages, tool, stepCountIs, } from 'ai'; import { z } from 'zod'; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), tools: { weather: tool({ description: 'Get the weather in a location (fahrenheit)', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => { const temperature = Math.round(Math.random() * (90 - 32) + 32); return { location, temperature, }; }, }), convertFahrenheitToCelsius: tool({ description: 'Convert a temperature in fahrenheit to celsius', inputSchema: z.object({ temperature: z .number() .describe('The temperature in fahrenheit to convert'), }), execute: async ({ temperature }) => { const celsius = Math.round((temperature - 32) * (5 / 9)); return { celsius, }; }, }), }, }); return result.toUIMessageStreamResponse({ headers: { 'Content-Type': 'application/octet-stream', 'Content-Encoding': 'none', }, }); } ``` <Note> You may need to restart your development server for the changes to take effect. </Note> ### Update the UI for the new tool To display the temperature conversion tool invocation in your UI, update your `app/(tabs)/index.tsx` file to handle the new tool part: ```tsx filename="app/(tabs)/index.tsx" highlight="37-42" import { generateAPIUrl } from '@/utils'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { fetch as expoFetch } from 'expo/fetch'; import { useState } from 'react'; import { View, TextInput, ScrollView, Text, SafeAreaView } from 'react-native'; export default function App() { const [input, setInput] = useState(''); const { messages, error, sendMessage } = useChat({ transport: new DefaultChatTransport({ fetch: expoFetch as unknown as typeof globalThis.fetch, api: generateAPIUrl('/api/chat'), }), onError: error => console.error(error, 'ERROR'), }); if (error) return <Text>{error.message}</Text>; return ( <SafeAreaView style={{ height: '100%' }}> <View style={{ height: '95%', display: 'flex', flexDirection: 'column', paddingHorizontal: 8, }} > <ScrollView style={{ flex: 1 }}> {messages.map(m => ( <View key={m.id} style={{ marginVertical: 8 }}> <View> <Text style={{ fontWeight: 700 }}>{m.role}</Text> {m.parts.map((part, i) => { switch (part.type) { case 'text': return <Text key={`${m.id}-${i}`}>{part.text}</Text>; case 'tool-weather': case 'tool-convertFahrenheitToCelsius': return ( <Text key={`${m.id}-${i}`}> {JSON.stringify(part, null, 2)} </Text> ); } })} </View> </View> ))} </ScrollView> <View style={{ marginTop: 8 }}> <TextInput style={{ backgroundColor: 'white', padding: 8 }} placeholder="Say something..." value={input} onChange={e => setInput(e.nativeEvent.text)} onSubmitEditing={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} autoFocus={true} /> </View> </View> </SafeAreaView> ); } ``` <Note> You may need to restart your development server for the changes to take effect. </Note> Now, when you ask "What's the weather in New York in celsius?", you should see a more complete interaction: 1. The model will call the weather tool for New York. 2. You'll see the tool result displayed. 3. It will then call the temperature conversion tool to convert the temperature from Fahrenheit to Celsius. 4. The model will then use that information to provide a natural language response about the weather in New York. This multi-step approach allows the model to gather information and use it to provide more accurate and contextual responses, making your chatbot considerably more useful. This simple example demonstrates how tools can expand your model's capabilities. You can create more complex tools to integrate with real APIs, databases, or any other external systems, allowing the model to access and process real-world data in real-time. Tools bridge the gap between the model's knowledge cutoff and current information. ## Polyfills Several functions that are internally used by the AI SDK might not available in the Expo runtime depending on your configuration and the target platform. First, install the following packages: <div className="my-4"> <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ungap/structured-clone @stardazed/streams-text-encoding" dark /> </Tab> <Tab> <Snippet text="npm install @ungap/structured-clone @stardazed/streams-text-encoding" dark /> </Tab> <Tab> <Snippet text="yarn add @ungap/structured-clone @stardazed/streams-text-encoding" dark /> </Tab> </Tabs> </div> Then create a new file in the root of your project with the following polyfills: ```ts filename="polyfills.js" import { Platform } from 'react-native'; import structuredClone from '@ungap/structured-clone'; if (Platform.OS !== 'web') { const setupPolyfills = async () => { const { polyfillGlobal } = await import( 'react-native/Libraries/Utilities/PolyfillFunctions' ); const { TextEncoderStream, TextDecoderStream } = await import( '@stardazed/streams-text-encoding' ); if (!('structuredClone' in global)) { polyfillGlobal('structuredClone', () => structuredClone); } polyfillGlobal('TextEncoderStream', () => TextEncoderStream); polyfillGlobal('TextDecoderStream', () => TextDecoderStream); }; setupPolyfills(); } export {}; ``` Finally, import the polyfills in your root `_layout.tsx`: ```ts filename="_layout.tsx" import '@/polyfills'; ``` ## Where to Next? You've built an AI chatbot using the AI SDK! From here, you have several paths to explore: - To learn more about the AI SDK, read through the [documentation](/docs). - If you're interested in diving deeper with guides, check out the [RAG (retrieval-augmented generation)](/docs/guides/rag-chatbot) and [multi-modal chatbot](/docs/guides/multi-modal-chatbot) guides. - To jumpstart your first AI project, explore available [templates](https://vercel.com/templates?type=ai). --- File: /ai/content/docs/02-getting-started/index.mdx --- --- title: Getting Started description: Welcome to the AI SDK documentation! --- # Getting Started The following guides are intended to provide you with an introduction to some of the core features provided by the AI SDK. <QuickstartFrameworkCards /> ## Backend Framework Examples You can also use [AI SDK Core](/docs/ai-sdk-core/overview) and [AI SDK UI](/docs/ai-sdk-ui/overview) with the following backend frameworks: <IndexCards cards={[ { title: 'Node.js HTTP Server', description: 'Send AI responses from a Node.js HTTP server.', href: '/examples/api-servers/node-js-http-server', }, { title: 'Express', description: 'Send AI responses from an Express server.', href: '/examples/api-servers/express', }, { title: 'Hono', description: 'Send AI responses from a Hono server.', href: '/examples/api-servers/hono', }, { title: 'Fastify', description: 'Send AI responses from a Fastify server.', href: '/examples/api-servers/fastify', }, { title: 'Nest.js', description: 'Send AI responses from a Nest.js server.', href: '/examples/api-servers/nest', }, ]} /> --- File: /ai/content/docs/03-ai-sdk-core/01-overview.mdx --- --- title: Overview description: An overview of AI SDK Core. --- # AI SDK Core Large Language Models (LLMs) are advanced programs that can understand, create, and engage with human language on a large scale. They are trained on vast amounts of written material to recognize patterns in language and predict what might come next in a given piece of text. AI SDK Core **simplifies working with LLMs by offering a standardized way of integrating them into your app** - so you can focus on building great AI applications for your users, not waste time on technical details. For example, here’s how you can generate text with various models using the AI SDK: <PreviewSwitchProviders /> ## AI SDK Core Functions AI SDK Core has various functions designed for [text generation](./generating-text), [structured data generation](./generating-structured-data), and [tool usage](./tools-and-tool-calling). These functions take a standardized approach to setting up [prompts](./prompts) and [settings](./settings), making it easier to work with different models. - [`generateText`](/docs/ai-sdk-core/generating-text): Generates text and [tool calls](./tools-and-tool-calling). This function is ideal for non-interactive use cases such as automation tasks where you need to write text (e.g. drafting email or summarizing web pages) and for agents that use tools. - [`streamText`](/docs/ai-sdk-core/generating-text): Stream text and tool calls. You can use the `streamText` function for interactive use cases such as [chat bots](/docs/ai-sdk-ui/chatbot) and [content streaming](/docs/ai-sdk-ui/completion). - [`generateObject`](/docs/ai-sdk-core/generating-structured-data): Generates a typed, structured object that matches a [Zod](https://zod.dev/) schema. You can use this function to force the language model to return structured data, e.g. for information extraction, synthetic data generation, or classification tasks. - [`streamObject`](/docs/ai-sdk-core/generating-structured-data): Stream a structured object that matches a Zod schema. You can use this function to [stream generated UIs](/docs/ai-sdk-ui/object-generation). ## API Reference Please check out the [AI SDK Core API Reference](/docs/reference/ai-sdk-core) for more details on each function. --- File: /ai/content/docs/03-ai-sdk-core/05-generating-text.mdx --- --- title: Generating Text description: Learn how to generate text with the AI SDK. --- # Generating and Streaming Text Large language models (LLMs) can generate text in response to a prompt, which can contain instructions and information to process. For example, you can ask a model to come up with a recipe, draft an email, or summarize a document. The AI SDK Core provides two functions to generate text and stream it from LLMs: - [`generateText`](#generatetext): Generates text for a given prompt and model. - [`streamText`](#streamtext): Streams text from a given prompt and model. Advanced LLM features such as [tool calling](./tools-and-tool-calling) and [structured data generation](./generating-structured-data) are built on top of text generation. ## `generateText` You can generate text using the [`generateText`](/docs/reference/ai-sdk-core/generate-text) function. This function is ideal for non-interactive use cases where you need to write text (e.g. drafting email or summarizing web pages) and for agents that use tools. ```tsx import { generateText } from 'ai'; const { text } = await generateText({ model: 'openai/gpt-4.1', prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` You can use more [advanced prompts](./prompts) to generate text with more complex instructions and content: ```tsx import { generateText } from 'ai'; const { text } = await generateText({ model: 'openai/gpt-4.1', system: 'You are a professional writer. ' + 'You write simple, clear, and concise content.', prompt: `Summarize the following article in 3-5 sentences: ${article}`, }); ``` The result object of `generateText` contains several promises that resolve when all required data is available: - `result.content`: The content that was generated in the last step. - `result.text`: The generated text. - `result.reasoning`: The full reasoning that the model has generated in the last step. - `result.reasoningText`: The reasoning text of the model (only available for some models). - `result.files`: The files that were generated in the last step. - `result.sources`: Sources that have been used as references in the last step (only available for some models). - `result.toolCalls`: The tool calls that were made in the last step. - `result.toolResults`: The results of the tool calls from the last step. - `result.finishReason`: The reason the model finished generating text. - `result.usage`: The usage of the model during the final step of text generation. - `result.totalUsage`: The total usage across all steps (for multi-step generations). - `result.warnings`: Warnings from the model provider (e.g. unsupported settings). - `result.request`: Additional request information. - `result.response`: Additional response information, including response messages and body. - `result.providerMetadata`: Additional provider-specific metadata. - `result.steps`: Details for all steps, useful for getting information about intermediate steps. - `result.experimental_output`: The generated structured output using the `experimental_output` specification. ### Accessing response headers & body Sometimes you need access to the full response from the model provider, e.g. to access some provider-specific headers or body content. You can access the raw response headers and body using the `response` property: ```ts import { generateText } from 'ai'; const result = await generateText({ // ... }); console.log(JSON.stringify(result.response.headers, null, 2)); console.log(JSON.stringify(result.response.body, null, 2)); ``` ## `streamText` Depending on your model and prompt, it can take a large language model (LLM) up to a minute to finish generating its response. This delay can be unacceptable for interactive use cases such as chatbots or real-time applications, where users expect immediate responses. AI SDK Core provides the [`streamText`](/docs/reference/ai-sdk-core/stream-text) function which simplifies streaming text from LLMs: ```ts import { streamText } from 'ai'; const result = streamText({ model: 'openai/gpt-4.1', prompt: 'Invent a new holiday and describe its traditions.', }); // example: use textStream as an async iterable for await (const textPart of result.textStream) { console.log(textPart); } ``` <Note> `result.textStream` is both a `ReadableStream` and an `AsyncIterable`. </Note> <Note type="warning"> `streamText` immediately starts streaming and suppresses errors to prevent server crashes. Use the `onError` callback to log errors. </Note> You can use `streamText` on its own or in combination with [AI SDK UI](/examples/next-pages/basics/streaming-text-generation) and [AI SDK RSC](/examples/next-app/basics/streaming-text-generation). The result object contains several helper functions to make the integration into [AI SDK UI](/docs/ai-sdk-ui) easier: - `result.toUIMessageStreamResponse()`: Creates a UI Message stream HTTP response (with tool calls etc.) that can be used in a Next.js App Router API route. - `result.pipeUIMessageStreamToResponse()`: Writes UI Message stream delta output to a Node.js response-like object. - `result.toTextStreamResponse()`: Creates a simple text stream HTTP response. - `result.pipeTextStreamToResponse()`: Writes text delta output to a Node.js response-like object. <Note> `streamText` is using backpressure and only generates tokens as they are requested. You need to consume the stream in order for it to finish. </Note> It also provides several promises that resolve when the stream is finished: - `result.content`: The content that was generated in the last step. - `result.text`: The generated text. - `result.reasoning`: The full reasoning that the model has generated. - `result.reasoningText`: The reasoning text of the model (only available for some models). - `result.files`: Files that have been generated by the model in the last step. - `result.sources`: Sources that have been used as references in the last step (only available for some models). - `result.toolCalls`: The tool calls that have been executed in the last step. - `result.toolResults`: The tool results that have been generated in the last step. - `result.finishReason`: The reason the model finished generating text. - `result.usage`: The usage of the model during the final step of text generation. - `result.totalUsage`: The total usage across all steps (for multi-step generations). - `result.warnings`: Warnings from the model provider (e.g. unsupported settings). - `result.steps`: Details for all steps, useful for getting information about intermediate steps. - `result.request`: Additional request information from the last step. - `result.response`: Additional response information from the last step. - `result.providerMetadata`: Additional provider-specific metadata from the last step. ### `onError` callback `streamText` immediately starts streaming to enable sending data without waiting for the model. Errors become part of the stream and are not thrown to prevent e.g. servers from crashing. To log errors, you can provide an `onError` callback that is triggered when an error occurs. ```tsx highlight="6-8" import { streamText } from 'ai'; const result = streamText({ model: 'openai/gpt-4.1', prompt: 'Invent a new holiday and describe its traditions.', onError({ error }) { console.error(error); // your error logging logic here }, }); ``` ### `onChunk` callback When using `streamText`, you can provide an `onChunk` callback that is triggered for each chunk of the stream. It receives the following chunk types: - `text` - `reasoning` - `source` - `tool-call` - `tool-input-start` - `tool-input-delta` - `tool-result` - `raw` ```tsx highlight="6-11" import { streamText } from 'ai'; const result = streamText({ model: 'openai/gpt-4.1', prompt: 'Invent a new holiday and describe its traditions.', onChunk({ chunk }) { // implement your own logic here, e.g.: if (chunk.type === 'text') { console.log(chunk.text); } }, }); ``` ### `onFinish` callback When using `streamText`, you can provide an `onFinish` callback that is triggered when the stream is finished ( [API Reference](/docs/reference/ai-sdk-core/stream-text#on-finish) ). It contains the text, usage information, finish reason, messages, steps, total usage, and more: ```tsx highlight="6-8" import { streamText } from 'ai'; const result = streamText({ model: 'openai/gpt-4.1', prompt: 'Invent a new holiday and describe its traditions.', onFinish({ text, finishReason, usage, response, steps, totalUsage }) { // your own logic, e.g. for saving the chat history or recording usage const messages = response.messages; // messages that were generated }, }); ``` ### `fullStream` property You can read a stream with all events using the `fullStream` property. This can be useful if you want to implement your own UI or handle the stream in a different way. Here is an example of how to use the `fullStream` property: ```tsx import { streamText } from 'ai'; import { z } from 'zod'; const result = streamText({ model: 'openai/gpt-4.1', tools: { cityAttractions: { inputSchema: z.object({ city: z.string() }), execute: async ({ city }) => ({ attractions: ['attraction1', 'attraction2', 'attraction3'], }), }, }, prompt: 'What are some San Francisco tourist attractions?', }); for await (const part of result.fullStream) { switch (part.type) { case 'start': { // handle start of stream break; } case 'start-step': { // handle start of step break; } case 'text-start': { // handle text start break; } case 'text-delta': { // handle text delta here break; } case 'text-end': { // handle text end break; } case 'reasoning-start': { // handle reasoning start break; } case 'reasoning-delta': { // handle reasoning delta here break; } case 'reasoning-end': { // handle reasoning end break; } case 'source': { // handle source here break; } case 'file': { // handle file here break; } case 'tool-call': { switch (part.toolName) { case 'cityAttractions': { // handle tool call here break; } } break; } case 'tool-input-start': { // handle tool input start break; } case 'tool-input-delta': { // handle tool input delta break; } case 'tool-input-end': { // handle tool input end break; } case 'tool-result': { switch (part.toolName) { case 'cityAttractions': { // handle tool result here break; } } break; } case 'tool-error': { // handle tool error break; } case 'finish-step': { // handle finish step break; } case 'finish': { // handle finish here break; } case 'error': { // handle error here break; } case 'raw': { // handle raw value break; } } } ``` ### Stream transformation You can use the `experimental_transform` option to transform the stream. This is useful for e.g. filtering, changing, or smoothing the text stream. The transformations are applied before the callbacks are invoked and the promises are resolved. If you e.g. have a transformation that changes all text to uppercase, the `onFinish` callback will receive the transformed text. #### Smoothing streams The AI SDK Core provides a [`smoothStream` function](/docs/reference/ai-sdk-core/smooth-stream) that can be used to smooth out text streaming. ```tsx highlight="6" import { smoothStream, streamText } from 'ai'; const result = streamText({ model, prompt, experimental_transform: smoothStream(), }); ``` #### Custom transformations You can also implement your own custom transformations. The transformation function receives the tools that are available to the model, and returns a function that is used to transform the stream. Tools can either be generic or limited to the tools that you are using. Here is an example of how to implement a custom transformation that converts all text to uppercase: ```ts const upperCaseTransform = <TOOLS extends ToolSet>() => (options: { tools: TOOLS; stopStream: () => void }) => new TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>({ transform(chunk, controller) { controller.enqueue( // for text chunks, convert the text to uppercase: chunk.type === 'text' ? { ...chunk, text: chunk.text.toUpperCase() } : chunk, ); }, }); ``` You can also stop the stream using the `stopStream` function. This is e.g. useful if you want to stop the stream when model guardrails are violated, e.g. by generating inappropriate content. When you invoke `stopStream`, it is important to simulate the `step-finish` and `finish` events to guarantee that a well-formed stream is returned and all callbacks are invoked. ```ts const stopWordTransform = <TOOLS extends ToolSet>() => ({ stopStream }: { stopStream: () => void }) => new TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>({ // note: this is a simplified transformation for testing; // in a real-world version more there would need to be // stream buffering and scanning to correctly emit prior text // and to detect all STOP occurrences. transform(chunk, controller) { if (chunk.type !== 'text') { controller.enqueue(chunk); return; } if (chunk.text.includes('STOP')) { // stop the stream stopStream(); // simulate the finish-step event controller.enqueue({ type: 'finish-step', finishReason: 'stop', logprobs: undefined, usage: { completionTokens: NaN, promptTokens: NaN, totalTokens: NaN, }, request: {}, response: { id: 'response-id', modelId: 'mock-model-id', timestamp: new Date(0), }, warnings: [], isContinued: false, }); // simulate the finish event controller.enqueue({ type: 'finish', finishReason: 'stop', logprobs: undefined, usage: { completionTokens: NaN, promptTokens: NaN, totalTokens: NaN, }, response: { id: 'response-id', modelId: 'mock-model-id', timestamp: new Date(0), }, }); return; } controller.enqueue(chunk); }, }); ``` #### Multiple transformations You can also provide multiple transformations. They are applied in the order they are provided. ```tsx highlight="4" const result = streamText({ model, prompt, experimental_transform: [firstTransform, secondTransform], }); ``` ## Sources Some providers such as [Perplexity](/providers/ai-sdk-providers/perplexity#sources) and [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai#sources) include sources in the response. Currently sources are limited to web pages that ground the response. You can access them using the `sources` property of the result. Each `url` source contains the following properties: - `id`: The ID of the source. - `url`: The URL of the source. - `title`: The optional title of the source. - `providerMetadata`: Provider metadata for the source. When you use `generateText`, you can access the sources using the `sources` property: ```ts const result = await generateText({ model: google('gemini-2.5-flash'), tools: { google_search: google.tools.googleSearch({}), }, prompt: 'List the top 5 San Francisco news from the past week.', }); for (const source of result.sources) { if (source.sourceType === 'url') { console.log('ID:', source.id); console.log('Title:', source.title); console.log('URL:', source.url); console.log('Provider metadata:', source.providerMetadata); console.log(); } } ``` When you use `streamText`, you can access the sources using the `fullStream` property: ```tsx const result = streamText({ model: google('gemini-2.5-flash'), tools: { google_search: google.tools.googleSearch({}), }, prompt: 'List the top 5 San Francisco news from the past week.', }); for await (const part of result.fullStream) { if (part.type === 'source' && part.sourceType === 'url') { console.log('ID:', part.id); console.log('Title:', part.title); console.log('URL:', part.url); console.log('Provider metadata:', part.providerMetadata); console.log(); } } ``` The sources are also available in the `result.sources` promise. ## Examples You can see `generateText` and `streamText` in action using various frameworks in the following examples: ### `generateText` <ExampleLinks examples={[ { title: 'Learn to generate text in Node.js', link: '/examples/node/generating-text/generate-text', }, { title: 'Learn to generate text in Next.js with Route Handlers (AI SDK UI)', link: '/examples/next-pages/basics/generating-text', }, { title: 'Learn to generate text in Next.js with Server Actions (AI SDK RSC)', link: '/examples/next-app/basics/generating-text', }, ]} /> ### `streamText` <ExampleLinks examples={[ { title: 'Learn to stream text in Node.js', link: '/examples/node/generating-text/stream-text', }, { title: 'Learn to stream text in Next.js with Route Handlers (AI SDK UI)', link: '/examples/next-pages/basics/streaming-text-generation', }, { title: 'Learn to stream text in Next.js with Server Actions (AI SDK RSC)', link: '/examples/next-app/basics/streaming-text-generation', }, ]} /> --- File: /ai/content/docs/03-ai-sdk-core/10-generating-structured-data.mdx --- --- title: Generating Structured Data description: Learn how to generate structured data with the AI SDK. --- # Generating Structured Data While text generation can be useful, your use case will likely call for generating structured data. For example, you might want to extract information from text, classify data, or generate synthetic data. Many language models are capable of generating structured data, often defined as using "JSON modes" or "tools". However, you need to manually provide schemas and then validate the generated data as LLMs can produce incorrect or incomplete structured data. The AI SDK standardises structured object generation across model providers with the [`generateObject`](/docs/reference/ai-sdk-core/generate-object) and [`streamObject`](/docs/reference/ai-sdk-core/stream-object) functions. You can use both functions with different output strategies, e.g. `array`, `object`, `enum`, or `no-schema`, and with different generation modes, e.g. `auto`, `tool`, or `json`. You can use [Zod schemas](/docs/reference/ai-sdk-core/zod-schema), [Valibot](/docs/reference/ai-sdk-core/valibot-schema), or [JSON schemas](/docs/reference/ai-sdk-core/json-schema) to specify the shape of the data that you want, and the AI model will generate data that conforms to that structure. <Note> You can pass Zod objects directly to the AI SDK functions or use the `zodSchema` helper function. </Note> ## Generate Object The `generateObject` generates structured data from a prompt. The schema is also used to validate the generated data, ensuring type safety and correctness. ```ts import { generateObject } from 'ai'; import { z } from 'zod'; const { object } = await generateObject({ model: 'openai/gpt-4.1', schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.object({ name: z.string(), amount: z.string() })), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); ``` <Note> See `generateObject` in action with [these examples](#more-examples) </Note> ### Accessing response headers & body Sometimes you need access to the full response from the model provider, e.g. to access some provider-specific headers or body content. You can access the raw response headers and body using the `response` property: ```ts import { generateObject } from 'ai'; const result = await generateObject({ // ... }); console.log(JSON.stringify(result.response.headers, null, 2)); console.log(JSON.stringify(result.response.body, null, 2)); ``` ## Stream Object Given the added complexity of returning structured data, model response time can be unacceptable for your interactive use case. With the [`streamObject`](/docs/reference/ai-sdk-core/stream-object) function, you can stream the model's response as it is generated. ```ts import { streamObject } from 'ai'; const { partialObjectStream } = streamObject({ // ... }); // use partialObjectStream as an async iterable for await (const partialObject of partialObjectStream) { console.log(partialObject); } ``` You can use `streamObject` to stream generated UIs in combination with React Server Components (see [Generative UI](../ai-sdk-rsc))) or the [`useObject`](/docs/reference/ai-sdk-ui/use-object) hook. <Note>See `streamObject` in action with [these examples](#more-examples)</Note> ### `onError` callback `streamObject` immediately starts streaming. Errors become part of the stream and are not thrown to prevent e.g. servers from crashing. To log errors, you can provide an `onError` callback that is triggered when an error occurs. ```tsx highlight="5-7" import { streamObject } from 'ai'; const result = streamObject({ // ... onError({ error }) { console.error(error); // your error logging logic here }, }); ``` ## Output Strategy You can use both functions with different output strategies, e.g. `array`, `object`, `enum`, or `no-schema`. ### Object The default output strategy is `object`, which returns the generated data as an object. You don't need to specify the output strategy if you want to use the default. ### Array If you want to generate an array of objects, you can set the output strategy to `array`. When you use the `array` output strategy, the schema specifies the shape of an array element. With `streamObject`, you can also stream the generated array elements using `elementStream`. ```ts highlight="7,18" import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import { z } from 'zod'; const { elementStream } = streamObject({ model: openai('gpt-4.1'), output: 'array', schema: z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), prompt: 'Generate 3 hero descriptions for a fantasy role playing game.', }); for await (const hero of elementStream) { console.log(hero); } ``` ### Enum If you want to generate a specific enum value, e.g. for classification tasks, you can set the output strategy to `enum` and provide a list of possible values in the `enum` parameter. <Note>Enum output is only available with `generateObject`.</Note> ```ts highlight="5-6" import { generateObject } from 'ai'; const { object } = await generateObject({ model: 'openai/gpt-4.1', output: 'enum', enum: ['action', 'comedy', 'drama', 'horror', 'sci-fi'], prompt: 'Classify the genre of this movie plot: ' + '"A group of astronauts travel through a wormhole in search of a ' + 'new habitable planet for humanity."', }); ``` ### No Schema In some cases, you might not want to use a schema, for example when the data is a dynamic user request. You can use the `output` setting to set the output format to `no-schema` in those cases and omit the schema parameter. ```ts highlight="6" import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; const { object } = await generateObject({ model: openai('gpt-4.1'), output: 'no-schema', prompt: 'Generate a lasagna recipe.', }); ``` ## Schema Name and Description You can optionally specify a name and description for the schema. These are used by some providers for additional LLM guidance, e.g. via tool or schema name. ```ts highlight="6-7" import { generateObject } from 'ai'; import { z } from 'zod'; const { object } = await generateObject({ model: 'openai/gpt-4.1', schemaName: 'Recipe', schemaDescription: 'A recipe for a dish.', schema: z.object({ name: z.string(), ingredients: z.array(z.object({ name: z.string(), amount: z.string() })), steps: z.array(z.string()), }), prompt: 'Generate a lasagna recipe.', }); ``` ## Error Handling When `generateObject` cannot generate a valid object, it throws a [`AI_NoObjectGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-object-generated-error). This error occurs when the AI provider fails to generate a parsable object that conforms to the schema. It can arise due to the following reasons: - The model failed to generate a response. - The model generated a response that could not be parsed. - The model generated a response that could not be validated against the schema. The error preserves the following information to help you log the issue: - `text`: The text that was generated by the model. This can be the raw text or the tool call text, depending on the object generation mode. - `response`: Metadata about the language model response, including response id, timestamp, and model. - `usage`: Request token usage. - `cause`: The cause of the error (e.g. a JSON parsing error). You can use this for more detailed error handling. ```ts import { generateObject, NoObjectGeneratedError } from 'ai'; try { await generateObject({ model, schema, prompt }); } catch (error) { if (NoObjectGeneratedError.isInstance(error)) { console.log('NoObjectGeneratedError'); console.log('Cause:', error.cause); console.log('Text:', error.text); console.log('Response:', error.response); console.log('Usage:', error.usage); } } ``` ## Repairing Invalid or Malformed JSON <Note type="warning"> The `repairText` function is experimental and may change in the future. </Note> Sometimes the model will generate invalid or malformed JSON. You can use the `repairText` function to attempt to repair the JSON. It receives the error, either a `JSONParseError` or a `TypeValidationError`, and the text that was generated by the model. You can then attempt to repair the text and return the repaired text. ```ts highlight="7-10" import { generateObject } from 'ai'; const { object } = await generateObject({ model, schema, prompt, experimental_repairText: async ({ text, error }) => { // example: add a closing brace to the text return text + '}'; }, }); ``` ## Structured outputs with `generateText` and `streamText` You can generate structured data with `generateText` and `streamText` by using the `experimental_output` setting. <Note> Some models, e.g. those by OpenAI, support structured outputs and tool calling at the same time. This is only possible with `generateText` and `streamText`. </Note> <Note type="warning"> Structured output generation with `generateText` and `streamText` is experimental and may change in the future. </Note> ### `generateText` ```ts highlight="2,4-18" // experimental_output is a structured object that matches the schema: const { experimental_output } = await generateText({ // ... experimental_output: Output.object({ schema: z.object({ name: z.string(), age: z.number().nullable().describe('Age of the person.'), contact: z.object({ type: z.literal('email'), value: z.string(), }), occupation: z.object({ type: z.literal('employed'), company: z.string(), position: z.string(), }), }), }), prompt: 'Generate an example person for testing.', }); ``` ### `streamText` ```ts highlight="2,4-18" // experimental_partialOutputStream contains generated partial objects: const { experimental_partialOutputStream } = await streamText({ // ... experimental_output: Output.object({ schema: z.object({ name: z.string(), age: z.number().nullable().describe('Age of the person.'), contact: z.object({ type: z.literal('email'), value: z.string(), }), occupation: z.object({ type: z.literal('employed'), company: z.string(), position: z.string(), }), }), }), prompt: 'Generate an example person for testing.', }); ``` ## More Examples You can see `generateObject` and `streamObject` in action using various frameworks in the following examples: ### `generateObject` <ExampleLinks examples={[ { title: 'Learn to generate objects in Node.js', link: '/examples/node/generating-structured-data/generate-object', }, { title: 'Learn to generate objects in Next.js with Route Handlers (AI SDK UI)', link: '/examples/next-pages/basics/generating-object', }, { title: 'Learn to generate objects in Next.js with Server Actions (AI SDK RSC)', link: '/examples/next-app/basics/generating-object', }, ]} /> ### `streamObject` <ExampleLinks examples={[ { title: 'Learn to stream objects in Node.js', link: '/examples/node/streaming-structured-data/stream-object', }, { title: 'Learn to stream objects in Next.js with Route Handlers (AI SDK UI)', link: '/examples/next-pages/basics/streaming-object-generation', }, { title: 'Learn to stream objects in Next.js with Server Actions (AI SDK RSC)', link: '/examples/next-app/basics/streaming-object-generation', }, ]} /> --- File: /ai/content/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx --- --- title: Tool Calling description: Learn about tool calling and multi-step calls (using stopWhen) with AI SDK Core. --- # Tool Calling As covered under Foundations, [tools](/docs/foundations/tools) are objects that can be called by the model to perform a specific task. AI SDK Core tools contain three elements: - **`description`**: An optional description of the tool that can influence when the tool is picked. - **`inputSchema`**: A [Zod schema](/docs/foundations/tools#schemas) or a [JSON schema](/docs/reference/ai-sdk-core/json-schema) that defines the input parameters. The schema is consumed by the LLM, and also used to validate the LLM tool calls. - **`execute`**: An optional async function that is called with the arguments from the tool call. It produces a value of type `RESULT` (generic type). It is optional because you might want to forward tool calls to the client or to a queue instead of executing them in the same process. <Note className="mb-2"> You can use the [`tool`](/docs/reference/ai-sdk-core/tool) helper function to infer the types of the `execute` parameters. </Note> The `tools` parameter of `generateText` and `streamText` is an object that has the tool names as keys and the tools as values: ```ts highlight="6-17" import { z } from 'zod'; import { generateText, tool } from 'ai'; const result = await generateText({ model: 'openai/gpt-4o', tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, prompt: 'What is the weather in San Francisco?', }); ``` <Note> When a model uses a tool, it is called a "tool call" and the output of the tool is called a "tool result". </Note> Tool calling is not restricted to only text generation. You can also use it to render user interfaces (Generative UI). ## Multi-Step Calls (using stopWhen) With the `stopWhen` setting, you can enable multi-step calls in `generateText` and `streamText`. When `stopWhen` is set and the model generates a tool call, the AI SDK will trigger a new generation passing in the tool result until there are no further tool calls or the stopping condition is met. <Note> The `stopWhen` conditions are only evaluated when the last step contains tool results. </Note> By default, when you use `generateText` or `streamText`, it triggers a single generation. This works well for many use cases where you can rely on the model's training data to generate a response. However, when you provide tools, the model now has the choice to either generate a normal text response, or generate a tool call. If the model generates a tool call, it's generation is complete and that step is finished. You may want the model to generate text after the tool has been executed, either to summarize the tool results in the context of the users query. In many cases, you may also want the model to use multiple tools in a single response. This is where multi-step calls come in. You can think of multi-step calls in a similar way to a conversation with a human. When you ask a question, if the person does not have the requisite knowledge in their common knowledge (a model's training data), the person may need to look up information (use a tool) before they can provide you with an answer. In the same way, the model may need to call a tool to get the information it needs to answer your question where each generation (tool call or text generation) is a step. ### Example In the following example, there are two steps: 1. **Step 1** 1. The prompt `'What is the weather in San Francisco?'` is sent to the model. 1. The model generates a tool call. 1. The tool call is executed. 1. **Step 2** 1. The tool result is sent to the model. 1. The model generates a response considering the tool result. ```ts highlight="18-19" import { z } from 'zod'; import { generateText, tool, stepCountIs } from 'ai'; const { text, steps } = await generateText({ model: 'openai/gpt-4o', tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), // stop after 5 steps if tools were called prompt: 'What is the weather in San Francisco?', }); ``` <Note>You can use `streamText` in a similar way.</Note> ### Steps To access intermediate tool calls and results, you can use the `steps` property in the result object or the `streamText` `onFinish` callback. It contains all the text, tool calls, tool results, and more from each step. #### Example: Extract tool results from all steps ```ts highlight="3,9-10" import { generateText } from 'ai'; const { steps } = await generateText({ model: openai('gpt-4o'), stopWhen: stepCountIs(10), // ... }); // extract all tool calls from the steps: const allToolCalls = steps.flatMap(step => step.toolCalls); ``` ### `onStepFinish` callback When using `generateText` or `streamText`, you can provide an `onStepFinish` callback that is triggered when a step is finished, i.e. all text deltas, tool calls, and tool results for the step are available. When you have multiple steps, the callback is triggered for each step. ```tsx highlight="5-7" import { generateText } from 'ai'; const result = await generateText({ // ... onStepFinish({ text, toolCalls, toolResults, finishReason, usage }) { // your own logic, e.g. for saving the chat history or recording usage }, }); ``` ### `prepareStep` callback The `prepareStep` callback is called before a step is started. It is called with the following parameters: - `model`: The model that was passed into `generateText`. - `stopWhen`: The stopping condition that was passed into `generateText`. - `stepNumber`: The number of the step that is being executed. - `steps`: The steps that have been executed so far. - `messages`: The messages that will be sent to the model for the current step. You can use it to provide different settings for a step, including modifying the input messages. ```tsx highlight="5-7" import { generateText } from 'ai'; const result = await generateText({ // ... prepareStep: async ({ model, stepNumber, steps, messages }) => { 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: activeTools: ['tool1'], }; } // when nothing is returned, the default settings are used }, }); ``` #### Message Modification for Longer Agentic Loops In longer agentic loops, you can use the `messages` parameter to modify the input messages for each step. This is particularly useful for prompt compression: ```tsx prepareStep: async ({ stepNumber, steps, messages }) => { // Compress conversation history for longer loops if (messages.length > 20) { return { messages: messages.slice(-10), }; } return {}; }, ``` ## Response Messages Adding the generated assistant and tool messages to your conversation history is a common task, especially if you are using multi-step tool calls. Both `generateText` and `streamText` have a `response.messages` property that you can use to add the assistant and tool messages to your conversation history. It is also available in the `onFinish` callback of `streamText`. The `response.messages` property contains an array of `ModelMessage` objects that you can add to your conversation history: ```ts import { generateText, ModelMessage } from 'ai'; const messages: ModelMessage[] = [ // ... ]; const { response } = await generateText({ // ... messages, }); // add the response messages to your conversation history: messages.push(...response.messages); // streamText: ...((await response).messages) ``` ## Dynamic Tools AI SDK Core supports dynamic tools for scenarios where tool schemas are not known at compile time. This is useful for: - MCP (Model Context Protocol) tools without schemas - User-defined functions at runtime - Tools loaded from external sources ### Using dynamicTool The `dynamicTool` helper creates tools with unknown input/output types: ```ts import { dynamicTool } from 'ai'; import { z } from 'zod'; const customTool = dynamicTool({ description: 'Execute a custom function', inputSchema: z.object({}), execute: async input => { // input is typed as 'unknown' // You need to validate/cast it at runtime const { action, parameters } = input as any; // Execute your dynamic logic return { result: `Executed ${action}` }; }, }); ``` ### Type-Safe Handling When using both static and dynamic tools, use the `dynamic` flag for type narrowing: ```ts const result = await generateText({ model: 'openai/gpt-4o', tools: { // Static tool with known types weather: weatherTool, // Dynamic tool custom: dynamicTool({ /* ... */ }), }, onStepFinish: ({ toolCalls, toolResults }) => { // Type-safe iteration for (const toolCall of toolCalls) { if (toolCall.dynamic) { // Dynamic tool: input is 'unknown' console.log('Dynamic:', toolCall.toolName, toolCall.input); continue; } // Static tool: full type inference switch (toolCall.toolName) { case 'weather': console.log(toolCall.input.location); // typed as string break; } } }, }); ``` ## Tool Choice You can use the `toolChoice` setting to influence when a tool is selected. It supports the following settings: - `auto` (default): the model can choose whether and which tools to call. - `required`: the model must call a tool. It can choose which tool to call. - `none`: the model must not call tools - `{ type: 'tool', toolName: string (typed) }`: the model must call the specified tool ```ts highlight="18" import { z } from 'zod'; import { generateText, tool } from 'ai'; const result = await generateText({ model: 'openai/gpt-4o', tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, toolChoice: 'required', // force the model to call a tool prompt: 'What is the weather in San Francisco?', }); ``` ## Tool Execution Options When tools are called, they receive additional options as a second parameter. ### Tool Call ID The ID of the tool call is forwarded to the tool execution. You can use it e.g. when sending tool-call related information with stream data. ```ts highlight="14-20" import { streamText, tool, createUIMessageStream, createUIMessageStreamResponse, } from 'ai'; export async function POST(req: Request) { const { messages } = await req.json(); const stream = createUIMessageStream({ execute: ({ writer }) => { const result = streamText({ // ... messages, tools: { myTool: tool({ // ... execute: async (args, { toolCallId }) => { // return e.g. custom status for tool call writer.write({ type: 'data-tool-status', id: toolCallId, data: { name: 'myTool', status: 'in-progress', }, }); // ... }, }), }, }); writer.merge(result.toUIMessageStream()); }, }); return createUIMessageStreamResponse({ stream }); } ``` ### Messages The messages that were sent to the language model to initiate the response that contained the tool call are forwarded to the tool execution. You can access them in the second parameter of the `execute` function. In multi-step calls, the messages contain the text, tool calls, and tool results from all previous steps. ```ts highlight="8-9" import { generateText, tool } from 'ai'; const result = await generateText({ // ... tools: { myTool: tool({ // ... execute: async (args, { messages }) => { // use the message history in e.g. calls to other language models return { ... }; }, }), }, }); ``` ### Abort Signals The abort signals from `generateText` and `streamText` are forwarded to the tool execution. You can access them in the second parameter of the `execute` function and e.g. abort long-running computations or forward them to fetch calls inside tools. ```ts highlight="6,11,14" import { z } from 'zod'; import { generateText, tool } from 'ai'; const result = await generateText({ model: 'openai/gpt-4.1', abortSignal: myAbortSignal, // signal that will be forwarded to tools tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string() }), execute: async ({ location }, { abortSignal }) => { return fetch( `https://api.weatherapi.com/v1/current.json?q=${location}`, { signal: abortSignal }, // forward the abort signal to fetch ); }, }), }, prompt: 'What is the weather in San Francisco?', }); ``` ### Context (experimental) You can pass in arbitrary context from `generateText` or `streamText` via the `experimental_context` setting. This context is available in the `experimental_context` tool execution option. ```ts const result = await generateText({ // ... tools: { someTool: tool({ // ... execute: async (input, { experimental_context: context }) => { const typedContext = context as { example: string }; // or use type validation library // ... }, }), }, experimental_context: { example: '123' }, }); ``` ## Types Modularizing your code often requires defining types to ensure type safety and reusability. To enable this, the AI SDK provides several helper types for tools, tool calls, and tool results. You can use them to strongly type your variables, function parameters, and return types in parts of the code that are not directly related to `streamText` or `generateText`. Each tool call is typed with `ToolCall<NAME extends string, ARGS>`, depending on the tool that has been invoked. Similarly, the tool results are typed with `ToolResult<NAME extends string, ARGS, RESULT>`. The tools in `streamText` and `generateText` are defined as a `ToolSet`. The type inference helpers `TypedToolCall<TOOLS extends ToolSet>` and `TypedToolResult<TOOLS extends ToolSet>` can be used to extract the tool call and tool result types from the tools. ```ts highlight="18-19,23-24" import { openai } from '@ai-sdk/openai'; import { TypedToolCall, TypedToolResult, generateText, tool } from 'ai'; import { z } from 'zod'; const myToolSet = { firstTool: tool({ description: 'Greets the user', inputSchema: z.object({ name: z.string() }), execute: async ({ name }) => `Hello, ${name}!`, }), secondTool: tool({ description: 'Tells the user their age', inputSchema: z.object({ age: z.number() }), execute: async ({ age }) => `You are ${age} years old!`, }), }; type MyToolCall = TypedToolCall<typeof myToolSet>; type MyToolResult = TypedToolResult<typeof myToolSet>; async function generateSomething(prompt: string): Promise<{ text: string; toolCalls: Array<MyToolCall>; // typed tool calls toolResults: Array<MyToolResult>; // typed tool results }> { return generateText({ model: openai('gpt-4.1'), tools: myToolSet, prompt, }); } ``` ## Handling Errors The AI SDK has three tool-call related errors: - [`NoSuchToolError`](/docs/reference/ai-sdk-errors/ai-no-such-tool-error): the model tries to call a tool that is not defined in the tools object - [`InvalidToolArgumentsError`](/docs/reference/ai-sdk-errors/ai-invalid-tool-arguments-error): the model calls a tool with arguments that do not match the tool's input schema - [`ToolExecutionError`](/docs/reference/ai-sdk-errors/ai-tool-execution-error): an error that occurred during tool execution - [`ToolCallRepairError`](/docs/reference/ai-sdk-errors/ai-tool-call-repair-error): an error that occurred during tool call repair ### `generateText` `generateText` throws errors and can be handled using a `try`/`catch` block: ```ts try { const result = await generateText({ //... }); } catch (error) { if (NoSuchToolError.isInstance(error)) { // handle the no such tool error } else if (InvalidToolArgumentsError.isInstance(error)) { // handle the invalid tool arguments error } else if (ToolExecutionError.isInstance(error)) { // handle the tool execution error } else { // handle other errors } } ``` ### `streamText` `streamText` sends the errors as part of the full stream. The error parts contain the error object. When using `toUIMessageStreamResponse`, you can pass an `onError` function to extract the error message from the error part and forward it as part of the stream response: ```ts const result = streamText({ // ... }); return result.toUIMessageStreamResponse({ onError: error => { if (NoSuchToolError.isInstance(error)) { return 'The model tried to call a unknown tool.'; } else if (InvalidToolArgumentsError.isInstance(error)) { return 'The model called a tool with invalid arguments.'; } else if (ToolExecutionError.isInstance(error)) { return 'An error occurred during tool execution.'; } else { return 'An unknown error occurred.'; } }, }); ``` ## Tool Call Repair <Note type="warning"> The tool call repair feature is experimental and may change in the future. </Note> Language models sometimes fail to generate valid tool calls, especially when the input schema is complex or the model is smaller. You can use the `experimental_repairToolCall` function to attempt to repair the tool call with a custom function. You can use different strategies to repair the tool call: - Use a model with structured outputs to generate the arguments. - Send the messages, system prompt, and tool schema to a stronger model to generate the arguments. - Provide more specific repair instructions based on which tool was called. ### Example: Use a model with structured outputs for repair ```ts import { openai } from '@ai-sdk/openai'; import { generateObject, generateText, NoSuchToolError, tool } from 'ai'; const result = await generateText({ model, tools, prompt, experimental_repairToolCall: async ({ toolCall, tools, inputSchema, error, }) => { if (NoSuchToolError.isInstance(error)) { return null; // do not attempt to fix invalid tool names } const tool = tools[toolCall.toolName as keyof typeof tools]; const { object: repairedArgs } = await generateObject({ model: openai('gpt-4.1'), schema: tool.inputSchema, prompt: [ `The model tried to call the tool "${toolCall.toolName}"` + ` with the following arguments:`, JSON.stringify(toolCall.input), `The tool accepts the following schema:`, JSON.stringify(inputSchema(toolCall)), 'Please fix the arguments.', ].join('\n'), }); return { ...toolCall, input: JSON.stringify(repairedArgs) }; }, }); ``` ### Example: Use the re-ask strategy for repair ```ts import { openai } from '@ai-sdk/openai'; import { generateObject, generateText, NoSuchToolError, tool } from 'ai'; const result = await generateText({ model, tools, prompt, experimental_repairToolCall: async ({ toolCall, tools, error, messages, system, }) => { const result = await generateText({ model, system, messages: [ ...messages, { role: 'assistant', content: [ { type: 'tool-call', toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, input: toolCall.input, }, ], }, { role: 'tool' as const, content: [ { type: 'tool-result', toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, output: error.message, }, ], }, ], tools, }); const newToolCall = result.toolCalls.find( newToolCall => newToolCall.toolName === toolCall.toolName, ); return newToolCall != null ? { toolCallType: 'function' as const, toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, input: JSON.stringify(newToolCall.input), } : null; }, }); ``` ## Active Tools Language models can only handle a limited number of tools at a time, depending on the model. To allow for static typing using a large number of tools and limiting the available tools to the model at the same time, the AI SDK provides the `activeTools` property. It is an array of tool names that are currently active. By default, the value is `undefined` and all tools are active. ```ts highlight="7" import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; const { text } = await generateText({ model: openai('gpt-4.1'), tools: myToolSet, activeTools: ['firstTool'], }); ``` ## Multi-modal Tool Results <Note type="warning"> Multi-modal tool results are experimental and only supported by Anthropic. </Note> In order to send multi-modal tool results, e.g. screenshots, back to the model, they need to be converted into a specific format. AI SDK Core tools have an optional `toModelOutput` function that converts the tool result into a content part. Here is an example for converting a screenshot into a content part: ```ts highlight="22-27" const result = await generateText({ model: anthropic('claude-3-5-sonnet-20241022'), tools: { computer: anthropic.tools.computer_20241022({ // ... async execute({ action, coordinate, text }) { switch (action) { case 'screenshot': { return { type: 'image', data: fs .readFileSync('./data/screenshot-editor.png') .toString('base64'), }; } default: { return `executed ${action}`; } } }, // map to tool result content for LLM consumption: toModelOutput(result) { return { type: 'content', value: typeof result === 'string' ? [{ type: 'text', text: result }] : [{ type: 'image', data: result.data, mediaType: 'image/png' }], }; }, }), }, // ... }); ``` ## Extracting Tools Once you start having many tools, you might want to extract them into separate files. The `tool` helper function is crucial for this, because it ensures correct type inference. Here is an example of an extracted tool: ```ts filename="tools/weather-tool.ts" highlight="1,4-5" import { tool } from 'ai'; import { z } from 'zod'; // the `tool` helper function ensures correct type inference: export const weatherTool = tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }); ``` ## MCP Tools <Note type="warning"> The MCP tools feature is experimental and may change in the future. </Note> The AI SDK supports connecting to [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers to access their tools. This enables your AI applications to discover and use tools across various services through a standardized interface. ### Initializing an MCP Client Create an MCP client using either: - `SSE` (Server-Sent Events): Uses HTTP-based real-time communication, better suited for remote servers that need to send data over the network - `stdio`: Uses standard input and output streams for communication, ideal for local tool servers running on the same machine (like CLI tools or local services) - Custom transport: Bring your own transport by implementing the `MCPTransport` interface, ideal when implementing transports from MCP's official Typescript SDK (e.g. `StreamableHTTPClientTransport`) #### SSE Transport The SSE can be configured using a simple object with a `type` and `url` property: ```typescript import { experimental_createMCPClient as createMCPClient } from 'ai'; const mcpClient = await createMCPClient({ transport: { type: 'sse', url: 'https://my-server.com/sse', // optional: configure HTTP headers, e.g. for authentication headers: { Authorization: 'Bearer my-api-key', }, }, }); ``` #### Stdio Transport The Stdio transport requires importing the `StdioMCPTransport` class from the `ai/mcp-stdio` package: ```typescript import { experimental_createMCPClient as createMCPClient } from 'ai'; import { Experimental_StdioMCPTransport as StdioMCPTransport } from 'ai/mcp-stdio'; const mcpClient = await createMCPClient({ transport: new StdioMCPTransport({ command: 'node', args: ['src/stdio/dist/server.js'], }), }); ``` #### Custom Transport You can also bring your own transport, as long as it implements the `MCPTransport` interface. Below is an example of using the new `StreamableHTTPClientTransport` from MCP's official Typescript SDK: ```typescript import { MCPTransport, experimental_createMCPClient as createMCPClient, } from 'ai'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp'; const url = new URL('http://localhost:3000/mcp'); const mcpClient = await createMCPClient({ transport: new StreamableHTTPClientTransport(url, { sessionId: 'session_123', }), }); ``` <Note> The client returned by the `experimental_createMCPClient` function is a lightweight client intended for use in tool conversion. It currently does not support all features of the full MCP client, such as: authorization, session management, resumable streams, and receiving notifications. </Note> #### Closing the MCP Client After initialization, you should close the MCP client based on your usage pattern: - For short-lived usage (e.g., single requests), close the client when the response is finished - For long-running clients (e.g., command line apps), keep the client open but ensure it's closed when the application terminates When streaming responses, you can close the client when the LLM response has finished. For example, when using `streamText`, you should use the `onFinish` callback: ```typescript const mcpClient = await experimental_createMCPClient({ // ... }); const tools = await mcpClient.tools(); const result = await streamText({ model: openai('gpt-4.1'), tools, prompt: 'What is the weather in Brooklyn, New York?', onFinish: async () => { await mcpClient.close(); }, }); ``` When generating responses without streaming, you can use try/finally or cleanup functions in your framework: ```typescript let mcpClient: MCPClient | undefined; try { mcpClient = await experimental_createMCPClient({ // ... }); } finally { await mcpClient?.close(); } ``` ### Using MCP Tools The client's `tools` method acts as an adapter between MCP tools and AI SDK tools. It supports two approaches for working with tool schemas: #### Schema Discovery The simplest approach where all tools offered by the server are listed, and input parameter types are inferred based the schemas provided by the server: ```typescript const tools = await mcpClient.tools(); ``` **Pros:** - Simpler to implement - Automatically stays in sync with server changes **Cons:** - No TypeScript type safety during development - No IDE autocompletion for tool parameters - Errors only surface at runtime - Loads all tools from the server #### Schema Definition You can also define the tools and their input schemas explicitly in your client code: ```typescript import { z } from 'zod'; const tools = await mcpClient.tools({ schemas: { 'get-data': { inputSchema: z.object({ query: z.string().describe('The data query'), format: z.enum(['json', 'text']).optional(), }), }, // For tools with zero arguments, you should use an empty object: 'tool-with-no-args': { inputSchema: z.object({}), }, }, }); ``` **Pros:** - Control over which tools are loaded - Full TypeScript type safety - Better IDE support with autocompletion - Catch parameter mismatches during development **Cons:** - Need to manually keep schemas in sync with server - More code to maintain When you define `schemas`, the client will only pull the explicitly defined tools, even if the server offers additional tools. This can be beneficial for: - Keeping your application focused on the tools it needs - Reducing unnecessary tool loading - Making your tool dependencies explicit ## Examples You can see tools in action using various frameworks in the following examples: <ExampleLinks examples={[ { title: 'Learn to use tools in Node.js', link: '/cookbook/node/call-tools', }, { title: 'Learn to use tools in Next.js with Route Handlers', link: '/cookbook/next/call-tools', }, { title: 'Learn to use MCP tools in Node.js', link: '/cookbook/node/mcp-tools', }, ]} /> --- File: /ai/content/docs/03-ai-sdk-core/20-prompt-engineering.mdx --- --- title: Prompt Engineering description: Learn how to develop prompts with AI SDK Core. --- # Prompt Engineering ## Tips ### Prompts for Tools When you create prompts that include tools, getting good results can be tricky as the number and complexity of your tools increases. Here are a few tips to help you get the best results: 1. Use a model that is strong at tool calling, such as `gpt-4` or `gpt-4.1`. Weaker models will often struggle to call tools effectively and flawlessly. 1. Keep the number of tools low, e.g. to 5 or less. 1. Keep the complexity of the tool parameters low. Complex Zod schemas with many nested and optional elements, unions, etc. can be challenging for the model to work with. 1. Use semantically meaningful names for your tools, parameters, parameter properties, etc. The more information you pass to the model, the better it can understand what you want. 1. Add `.describe("...")` to your Zod schema properties to give the model hints about what a particular property is for. 1. When the output of a tool might be unclear to the model and there are dependencies between tools, use the `description` field of a tool to provide information about the output of the tool execution. 1. You can include example input/outputs of tool calls in your prompt to help the model understand how to use the tools. Keep in mind that the tools work with JSON objects, so the examples should use JSON. In general, the goal should be to give the model all information it needs in a clear way. ### Tool & Structured Data Schemas The mapping from Zod schemas to LLM inputs (typically JSON schema) is not always straightforward, since the mapping is not one-to-one. #### Zod Dates Zod expects JavaScript Date objects, but models return dates as strings. You can specify and validate the date format using `z.string().datetime()` or `z.string().date()`, and then use a Zod transformer to convert the string to a Date object. ```ts highlight="7-10" const result = await generateObject({ model: openai('gpt-4.1'), schema: z.object({ events: z.array( z.object({ event: z.string(), date: z .string() .date() .transform(value => new Date(value)), }), ), }), prompt: 'List 5 important events from the year 2000.', }); ``` #### Optional Parameters When working with tools that have optional parameters, you may encounter compatibility issues with certain providers that use strict schema validation. <Note> This is particularly relevant for OpenAI models with structured outputs (strict mode). </Note> For maximum compatibility, optional parameters should use `.nullable()` instead of `.optional()`: ```ts highlight="6,7,16,17" // This may fail with strict schema validation const failingTool = tool({ description: 'Execute a command', inputSchema: z.object({ command: z.string(), workdir: z.string().optional(), // This can cause errors timeout: z.string().optional(), }), }); // This works with strict schema validation const workingTool = tool({ description: 'Execute a command', inputSchema: z.object({ command: z.string(), workdir: z.string().nullable(), // Use nullable instead timeout: z.string().nullable(), }), }); ``` #### Temperature Settings For tool calls and object generation, it's recommended to use `temperature: 0` to ensure deterministic and consistent results: ```ts highlight="3" const result = await generateText({ model: openai('gpt-4o'), temperature: 0, // Recommended for tool calls tools: { myTool: tool({ description: 'Execute a command', inputSchema: z.object({ command: z.string(), }), }), }, prompt: 'Execute the ls command', }); ``` Lower temperature values reduce randomness in model outputs, which is particularly important when the model needs to: - Generate structured data with specific formats - Make precise tool calls with correct parameters - Follow strict schemas consistently ## Debugging ### Inspecting Warnings Not all providers support all AI SDK features. Providers either throw exceptions or return warnings when they do not support a feature. To check if your prompt, tools, and settings are handled correctly by the provider, you can check the call warnings: ```ts const result = await generateText({ model: openai('gpt-4o'), prompt: 'Hello, world!', }); console.log(result.warnings); ``` ### HTTP Request Bodies You can inspect the raw HTTP request bodies for models that expose them, e.g. [OpenAI](/providers/ai-sdk-providers/openai). This allows you to inspect the exact payload that is sent to the model provider in the provider-specific way. Request bodies are available via the `request.body` property of the response: ```ts highlight="6" const result = await generateText({ model: openai('gpt-4o'), prompt: 'Hello, world!', }); console.log(result.request.body); ``` --- File: /ai/content/docs/03-ai-sdk-core/25-settings.mdx --- --- title: Settings description: Learn how to configure the AI SDK. --- # Settings Large language models (LLMs) typically provide settings to augment their output. All AI SDK functions support the following common settings in addition to the model, the [prompt](./prompts), and additional provider-specific settings: ```ts highlight="3-5" const result = await generateText({ model: 'openai/gpt-4.1', maxOutputTokens: 512, temperature: 0.3, maxRetries: 5, prompt: 'Invent a new holiday and describe its traditions.', }); ``` <Note> Some providers do not support all common settings. If you use a setting with a provider that does not support it, a warning will be generated. You can check the `warnings` property in the result object to see if any warnings were generated. </Note> ### `maxOutputTokens` Maximum number of tokens to generate. ### `temperature` Temperature setting. The value is passed through to the provider. The range depends on the provider and model. For most providers, `0` means almost deterministic results, and higher values mean more randomness. It is recommended to set either `temperature` or `topP`, but not both. <Note>In AI SDK 5.0, temperature is no longer set to `0` by default.</Note> ### `topP` Nucleus sampling. The value is passed through to the provider. The range depends on the provider and model. For most providers, nucleus sampling is a number between 0 and 1. E.g. 0.1 would mean that only tokens with the top 10% probability mass are considered. It is recommended to set either `temperature` or `topP`, but not both. ### `topK` Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use `temperature`. ### `presencePenalty` The presence penalty affects the likelihood of the model to repeat information that is already in the prompt. The value is passed through to the provider. The range depends on the provider and model. For most providers, `0` means no penalty. ### `frequencyPenalty` The frequency penalty affects the likelihood of the model to repeatedly use the same words or phrases. The value is passed through to the provider. The range depends on the provider and model. For most providers, `0` means no penalty. ### `stopSequences` The stop sequences to use for stopping the text generation. If set, the model will stop generating text when one of the stop sequences is generated. Providers may have limits on the number of stop sequences. ### `seed` It is the seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results. ### `maxRetries` Maximum number of retries. Set to 0 to disable retries. Default: `2`. ### `abortSignal` An optional abort signal that can be used to cancel the call. The abort signal can e.g. be forwarded from a user interface to cancel the call, or to define a timeout. #### Example: Timeout ```ts const result = await generateText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', abortSignal: AbortSignal.timeout(5000), // 5 seconds }); ``` ### `headers` Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. You can use the request headers to provide additional information to the provider, depending on what the provider supports. For example, some observability providers support headers such as `Prompt-Id`. ```ts import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = await generateText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', headers: { 'Prompt-Id': 'my-prompt-id', }, }); ``` <Note> The `headers` setting is for request-specific headers. You can also set `headers` in the provider configuration. These headers will be sent with every request made by the provider. </Note> --- File: /ai/content/docs/03-ai-sdk-core/30-embeddings.mdx --- --- title: Embeddings description: Learn how to embed values with the AI SDK. --- # Embeddings Embeddings are a way to represent words, phrases, or images as vectors in a high-dimensional space. In this space, similar words are close to each other, and the distance between words can be used to measure their similarity. ## Embedding a Single Value The AI SDK provides the [`embed`](/docs/reference/ai-sdk-core/embed) function to embed single values, which is useful for tasks such as finding similar words or phrases or clustering text. You can use it with embeddings models, e.g. `openai.textEmbeddingModel('text-embedding-3-large')` or `mistral.textEmbeddingModel('mistral-embed')`. ```tsx import { embed } from 'ai'; import { openai } from '@ai-sdk/openai'; // 'embedding' is a single embedding object (number[]) const { embedding } = await embed({ model: openai.textEmbeddingModel('text-embedding-3-small'), value: 'sunny day at the beach', }); ``` ## Embedding Many Values When loading data, e.g. when preparing a data store for retrieval-augmented generation (RAG), it is often useful to embed many values at once (batch embedding). The AI SDK provides the [`embedMany`](/docs/reference/ai-sdk-core/embed-many) function for this purpose. Similar to `embed`, you can use it with embeddings models, e.g. `openai.textEmbeddingModel('text-embedding-3-large')` or `mistral.textEmbeddingModel('mistral-embed')`. ```tsx import { openai } from '@ai-sdk/openai'; import { embedMany } from 'ai'; // 'embeddings' is an array of embedding objects (number[][]). // It is sorted in the same order as the input values. const { embeddings } = await embedMany({ model: openai.textEmbeddingModel('text-embedding-3-small'), values: [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ], }); ``` ## Embedding Similarity After embedding values, you can calculate the similarity between them using the [`cosineSimilarity`](/docs/reference/ai-sdk-core/cosine-similarity) function. This is useful to e.g. find similar words or phrases in a dataset. You can also rank and filter related items based on their similarity. ```ts highlight={"2,10"} import { openai } from '@ai-sdk/openai'; import { cosineSimilarity, embedMany } from 'ai'; const { embeddings } = await embedMany({ model: openai.textEmbeddingModel('text-embedding-3-small'), values: ['sunny day at the beach', 'rainy afternoon in the city'], }); console.log( `cosine similarity: ${cosineSimilarity(embeddings[0], embeddings[1])}`, ); ``` ## Token Usage Many providers charge based on the number of tokens used to generate embeddings. Both `embed` and `embedMany` provide token usage information in the `usage` property of the result object: ```ts highlight={"4,9"} import { openai } from '@ai-sdk/openai'; import { embed } from 'ai'; const { embedding, usage } = await embed({ model: openai.textEmbeddingModel('text-embedding-3-small'), value: 'sunny day at the beach', }); console.log(usage); // { tokens: 10 } ``` ## Settings ### Provider Options Embedding model settings can be configured using `providerOptions` for provider-specific parameters: ```ts highlight={"5-9"} import { openai } from '@ai-sdk/openai'; import { embed } from 'ai'; const { embedding } = await embed({ model: openai.textEmbeddingModel('text-embedding-3-small'), value: 'sunny day at the beach', providerOptions: { openai: { dimensions: 512, // Reduce embedding dimensions }, }, }); ``` ### Parallel Requests The `embedMany` function now supports parallel processing with configurable `maxParallelCalls` to optimize performance: ```ts highlight={"4"} import { openai } from '@ai-sdk/openai'; import { embedMany } from 'ai'; const { embeddings, usage } = await embedMany({ maxParallelCalls: 2, // Limit parallel requests model: openai.textEmbeddingModel('text-embedding-3-small'), values: [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ], }); ``` ### Retries Both `embed` and `embedMany` accept an optional `maxRetries` parameter of type `number` that you can use to set the maximum number of retries for the embedding process. It defaults to `2` retries (3 attempts in total). You can set it to `0` to disable retries. ```ts highlight={"7"} import { openai } from '@ai-sdk/openai'; import { embed } from 'ai'; const { embedding } = await embed({ model: openai.textEmbeddingModel('text-embedding-3-small'), value: 'sunny day at the beach', maxRetries: 0, // Disable retries }); ``` ### Abort Signals and Timeouts Both `embed` and `embedMany` accept an optional `abortSignal` parameter of type [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that you can use to abort the embedding process or set a timeout. ```ts highlight={"7"} import { openai } from '@ai-sdk/openai'; import { embed } from 'ai'; const { embedding } = await embed({ model: openai.textEmbeddingModel('text-embedding-3-small'), value: 'sunny day at the beach', abortSignal: AbortSignal.timeout(1000), // Abort after 1 second }); ``` ### Custom Headers Both `embed` and `embedMany` accept an optional `headers` parameter of type `Record<string, string>` that you can use to add custom headers to the embedding request. ```ts highlight={"7"} import { openai } from '@ai-sdk/openai'; import { embed } from 'ai'; const { embedding } = await embed({ model: openai.textEmbeddingModel('text-embedding-3-small'), value: 'sunny day at the beach', headers: { 'X-Custom-Header': 'custom-value' }, }); ``` ## Response Information Both `embed` and `embedMany` return response information that includes the raw provider response: ```ts highlight={"4,9"} import { openai } from '@ai-sdk/openai'; import { embed } from 'ai'; const { embedding, response } = await embed({ model: openai.textEmbeddingModel('text-embedding-3-small'), value: 'sunny day at the beach', }); console.log(response); // Raw provider response ``` ## Embedding Providers & Models Several providers offer embedding models: | Provider | Model | Embedding Dimensions | | ----------------------------------------------------------------------------------------- | ------------------------------- | -------------------- | | [OpenAI](/providers/ai-sdk-providers/openai#embedding-models) | `text-embedding-3-large` | 3072 | | [OpenAI](/providers/ai-sdk-providers/openai#embedding-models) | `text-embedding-3-small` | 1536 | | [OpenAI](/providers/ai-sdk-providers/openai#embedding-models) | `text-embedding-ada-002` | 1536 | | [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai#embedding-models) | `gemini-embedding-001` | 3072 | | [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai#embedding-models) | `text-embedding-004` | 768 | | [Mistral](/providers/ai-sdk-providers/mistral#embedding-models) | `mistral-embed` | 1024 | | [Cohere](/providers/ai-sdk-providers/cohere#embedding-models) | `embed-english-v3.0` | 1024 | | [Cohere](/providers/ai-sdk-providers/cohere#embedding-models) | `embed-multilingual-v3.0` | 1024 | | [Cohere](/providers/ai-sdk-providers/cohere#embedding-models) | `embed-english-light-v3.0` | 384 | | [Cohere](/providers/ai-sdk-providers/cohere#embedding-models) | `embed-multilingual-light-v3.0` | 384 | | [Cohere](/providers/ai-sdk-providers/cohere#embedding-models) | `embed-english-v2.0` | 4096 | | [Cohere](/providers/ai-sdk-providers/cohere#embedding-models) | `embed-english-light-v2.0` | 1024 | | [Cohere](/providers/ai-sdk-providers/cohere#embedding-models) | `embed-multilingual-v2.0` | 768 | | [Amazon Bedrock](/providers/ai-sdk-providers/amazon-bedrock#embedding-models) | `amazon.titan-embed-text-v1` | 1536 | | [Amazon Bedrock](/providers/ai-sdk-providers/amazon-bedrock#embedding-models) | `amazon.titan-embed-text-v2:0` | 1024 | --- File: /ai/content/docs/03-ai-sdk-core/35-image-generation.mdx --- --- title: Image Generation description: Learn how to generate images with the AI SDK. --- # Image Generation <Note type="warning">Image generation is an experimental feature.</Note> The AI SDK provides the [`generateImage`](/docs/reference/ai-sdk-core/generate-image) function to generate images based on a given prompt using an image model. ```tsx import { experimental_generateImage as generateImage } from 'ai'; import { openai } from '@ai-sdk/openai'; const { image } = await generateImage({ model: openai.image('dall-e-3'), prompt: 'Santa Claus driving a Cadillac', }); ``` You can access the image data using the `base64` or `uint8Array` properties: ```tsx const base64 = image.base64; // base64 image data const uint8Array = image.uint8Array; // Uint8Array image data ``` ## Settings ### Size and Aspect Ratio Depending on the model, you can either specify the size or the aspect ratio. ##### Size The size is specified as a string in the format `{width}x{height}`. Models only support a few sizes, and the supported sizes are different for each model and provider. ```tsx highlight={"7"} import { experimental_generateImage as generateImage } from 'ai'; import { openai } from '@ai-sdk/openai'; const { image } = await generateImage({ model: openai.image('dall-e-3'), prompt: 'Santa Claus driving a Cadillac', size: '1024x1024', }); ``` ##### Aspect Ratio The aspect ratio is specified as a string in the format `{width}:{height}`. Models only support a few aspect ratios, and the supported aspect ratios are different for each model and provider. ```tsx highlight={"7"} import { experimental_generateImage as generateImage } from 'ai'; import { vertex } from '@ai-sdk/google-vertex'; const { image } = await generateImage({ model: vertex.image('imagen-3.0-generate-002'), prompt: 'Santa Claus driving a Cadillac', aspectRatio: '16:9', }); ``` ### Generating Multiple Images `generateImage` also supports generating multiple images at once: ```tsx highlight={"7"} import { experimental_generateImage as generateImage } from 'ai'; import { openai } from '@ai-sdk/openai'; const { images } = await generateImage({ model: openai.image('dall-e-2'), prompt: 'Santa Claus driving a Cadillac', n: 4, // number of images to generate }); ``` <Note> `generateImage` will automatically call the model as often as needed (in parallel) to generate the requested number of images. </Note> Each image model has an internal limit on how many images it can generate in a single API call. The AI SDK manages this automatically by batching requests appropriately when you request multiple images using the `n` parameter. By default, the SDK uses provider-documented limits (for example, DALL-E 3 can only generate 1 image per call, while DALL-E 2 supports up to 10). If needed, you can override this behavior using the `maxImagesPerCall` setting when generating your image. This is particularly useful when working with new or custom models where the default batch size might not be optimal: ```tsx const { images } = await generateImage({ model: openai.image('dall-e-2'), prompt: 'Santa Claus driving a Cadillac', maxImagesPerCall: 5, // Override the default batch size n: 10, // Will make 2 calls of 5 images each }); ``` ### Providing a Seed You can provide a seed to the `generateImage` function to control the output of the image generation process. If supported by the model, the same seed will always produce the same image. ```tsx highlight={"7"} import { experimental_generateImage as generateImage } from 'ai'; import { openai } from '@ai-sdk/openai'; const { image } = await generateImage({ model: openai.image('dall-e-3'), prompt: 'Santa Claus driving a Cadillac', seed: 1234567890, }); ``` ### Provider-specific Settings Image models often have provider- or even model-specific settings. You can pass such settings to the `generateImage` function using the `providerOptions` parameter. The options for the provider (`openai` in the example below) become request body properties. ```tsx highlight={"9"} import { experimental_generateImage as generateImage } from 'ai'; import { openai } from '@ai-sdk/openai'; const { image } = await generateImage({ model: openai.image('dall-e-3'), prompt: 'Santa Claus driving a Cadillac', size: '1024x1024', providerOptions: { openai: { style: 'vivid', quality: 'hd' }, }, }); ``` ### Abort Signals and Timeouts `generateImage` accepts an optional `abortSignal` parameter of type [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that you can use to abort the image generation process or set a timeout. ```ts highlight={"7"} import { openai } from '@ai-sdk/openai'; import { experimental_generateImage as generateImage } from 'ai'; const { image } = await generateImage({ model: openai.image('dall-e-3'), prompt: 'Santa Claus driving a Cadillac', abortSignal: AbortSignal.timeout(1000), // Abort after 1 second }); ``` ### Custom Headers `generateImage` accepts an optional `headers` parameter of type `Record<string, string>` that you can use to add custom headers to the image generation request. ```ts highlight={"7"} import { openai } from '@ai-sdk/openai'; import { experimental_generateImage as generateImage } from 'ai'; const { image } = await generateImage({ model: openai.image('dall-e-3'), prompt: 'Santa Claus driving a Cadillac', headers: { 'X-Custom-Header': 'custom-value' }, }); ``` ### Warnings If the model returns warnings, e.g. for unsupported parameters, they will be available in the `warnings` property of the response. ```tsx const { image, warnings } = await generateImage({ model: openai.image('dall-e-3'), prompt: 'Santa Claus driving a Cadillac', }); ``` ### Additional provider-specific meta data Some providers expose additional meta data for the result overall or per image. ```tsx const prompt = 'Santa Claus driving a Cadillac'; const { image, providerMetadata } = await generateImage({ model: openai.image('dall-e-3'), prompt, }); const revisedPrompt = providerMetadata.openai.images[0]?.revisedPrompt; console.log({ prompt, revisedPrompt, }); ``` The outer key of the returned `providerMetadata` is the provider name. The inner values are the metadata. An `images` key is always present in the metadata and is an array with the same length as the top level `images` key. ### Error Handling When `generateImage` cannot generate a valid image, it throws a [`AI_NoImageGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-image-generated-error). This error occurs when the AI provider fails to generate an image. It can arise due to the following reasons: - The model failed to generate a response - The model generated a response that could not be parsed The error preserves the following information to help you log the issue: - `responses`: Metadata about the image model responses, including timestamp, model, and headers. - `cause`: The cause of the error. You can use this for more detailed error handling ```ts import { generateImage, NoImageGeneratedError } from 'ai'; try { await generateImage({ model, prompt }); } catch (error) { if (NoImageGeneratedError.isInstance(error)) { console.log('NoImageGeneratedError'); console.log('Cause:', error.cause); console.log('Responses:', error.responses); } } ``` ## Generating Images with Language Models Some language models such as Google `gemini-2.0-flash-exp` support multi-modal outputs including images. With such models, you can access the generated images using the `files` property of the response. ```ts import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; const result = await generateText({ model: google('gemini-2.0-flash-exp'), providerOptions: { google: { responseModalities: ['TEXT', 'IMAGE'] }, }, prompt: 'Generate an image of a comic cat', }); for (const file of result.files) { if (file.mediaType.startsWith('image/')) { // The file object provides multiple data formats: // Access images as base64 string, Uint8Array binary data, or check type // - file.base64: string (data URL format) // - file.uint8Array: Uint8Array (binary data) // - file.mediaType: string (e.g. "image/png") } } ``` ## Image Models | Provider | Model | Support sizes (`width x height`) or aspect ratios (`width : height`) | | ------------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [xAI Grok](/providers/ai-sdk-providers/xai#image-models) | `grok-2-image` | 1024x768 (default) | | [OpenAI](/providers/ai-sdk-providers/openai#image-models) | `gpt-image-1` | 1024x1024, 1536x1024, 1024x1536 | | [OpenAI](/providers/ai-sdk-providers/openai#image-models) | `dall-e-3` | 1024x1024, 1792x1024, 1024x1792 | | [OpenAI](/providers/ai-sdk-providers/openai#image-models) | `dall-e-2` | 256x256, 512x512, 1024x1024 | | [Amazon Bedrock](/providers/ai-sdk-providers/amazon-bedrock#image-models) | `amazon.nova-canvas-v1:0` | 320-4096 (multiples of 16), 1:4 to 4:1, max 4.2M pixels | | [Fal](/providers/ai-sdk-providers/fal#image-models) | `fal-ai/flux/dev` | 1:1, 3:4, 4:3, 9:16, 16:9, 9:21, 21:9 | | [Fal](/providers/ai-sdk-providers/fal#image-models) | `fal-ai/flux-lora` | 1:1, 3:4, 4:3, 9:16, 16:9, 9:21, 21:9 | | [Fal](/providers/ai-sdk-providers/fal#image-models) | `fal-ai/fast-sdxl` | 1:1, 3:4, 4:3, 9:16, 16:9, 9:21, 21:9 | | [Fal](/providers/ai-sdk-providers/fal#image-models) | `fal-ai/flux-pro/v1.1-ultra` | 1:1, 3:4, 4:3, 9:16, 16:9, 9:21, 21:9 | | [Fal](/providers/ai-sdk-providers/fal#image-models) | `fal-ai/ideogram/v2` | 1:1, 3:4, 4:3, 9:16, 16:9, 9:21, 21:9 | | [Fal](/providers/ai-sdk-providers/fal#image-models) | `fal-ai/recraft-v3` | 1:1, 3:4, 4:3, 9:16, 16:9, 9:21, 21:9 | | [Fal](/providers/ai-sdk-providers/fal#image-models) | `fal-ai/stable-diffusion-3.5-large` | 1:1, 3:4, 4:3, 9:16, 16:9, 9:21, 21:9 | | [Fal](/providers/ai-sdk-providers/fal#image-models) | `fal-ai/hyper-sdxl` | 1:1, 3:4, 4:3, 9:16, 16:9, 9:21, 21:9 | | [DeepInfra](/providers/ai-sdk-providers/deepinfra#image-models) | `stabilityai/sd3.5` | 1:1, 16:9, 1:9, 3:2, 2:3, 4:5, 5:4, 9:16, 9:21 | | [DeepInfra](/providers/ai-sdk-providers/deepinfra#image-models) | `black-forest-labs/FLUX-1.1-pro` | 256-1440 (multiples of 32) | | [DeepInfra](/providers/ai-sdk-providers/deepinfra#image-models) | `black-forest-labs/FLUX-1-schnell` | 256-1440 (multiples of 32) | | [DeepInfra](/providers/ai-sdk-providers/deepinfra#image-models) | `black-forest-labs/FLUX-1-dev` | 256-1440 (multiples of 32) | | [DeepInfra](/providers/ai-sdk-providers/deepinfra#image-models) | `black-forest-labs/FLUX-pro` | 256-1440 (multiples of 32) | | [DeepInfra](/providers/ai-sdk-providers/deepinfra#image-models) | `stabilityai/sd3.5-medium` | 1:1, 16:9, 1:9, 3:2, 2:3, 4:5, 5:4, 9:16, 9:21 | | [DeepInfra](/providers/ai-sdk-providers/deepinfra#image-models) | `stabilityai/sdxl-turbo` | 1:1, 16:9, 1:9, 3:2, 2:3, 4:5, 5:4, 9:16, 9:21 | | [Replicate](/providers/ai-sdk-providers/replicate) | `black-forest-labs/flux-schnell` | 1:1, 2:3, 3:2, 4:5, 5:4, 16:9, 9:16, 9:21, 21:9 | | [Replicate](/providers/ai-sdk-providers/replicate) | `recraft-ai/recraft-v3` | 1024x1024, 1365x1024, 1024x1365, 1536x1024, 1024x1536, 1820x1024, 1024x1820, 1024x2048, 2048x1024, 1434x1024, 1024x1434, 1024x1280, 1280x1024, 1024x1707, 1707x1024 | | [Google](/providers/ai-sdk-providers/google#image-models) | `imagen-3.0-generate-002` | 1:1, 3:4, 4:3, 9:16, 16:9 | | [Google Vertex](/providers/ai-sdk-providers/google-vertex#image-models) | `imagen-3.0-generate-002` | 1:1, 3:4, 4:3, 9:16, 16:9 | | [Google Vertex](/providers/ai-sdk-providers/google-vertex#image-models) | `imagen-3.0-fast-generate-001` | 1:1, 3:4, 4:3, 9:16, 16:9 | | [Fireworks](/providers/ai-sdk-providers/fireworks#image-models) | `accounts/fireworks/models/flux-1-dev-fp8` | 1:1, 2:3, 3:2, 4:5, 5:4, 16:9, 9:16, 9:21, 21:9 | | [Fireworks](/providers/ai-sdk-providers/fireworks#image-models) | `accounts/fireworks/models/flux-1-schnell-fp8` | 1:1, 2:3, 3:2, 4:5, 5:4, 16:9, 9:16, 9:21, 21:9 | | [Fireworks](/providers/ai-sdk-providers/fireworks#image-models) | `accounts/fireworks/models/playground-v2-5-1024px-aesthetic` | 640x1536, 768x1344, 832x1216, 896x1152, 1024x1024, 1152x896, 1216x832, 1344x768, 1536x640 | | [Fireworks](/providers/ai-sdk-providers/fireworks#image-models) | `accounts/fireworks/models/japanese-stable-diffusion-xl` | 640x1536, 768x1344, 832x1216, 896x1152, 1024x1024, 1152x896, 1216x832, 1344x768, 1536x640 | | [Fireworks](/providers/ai-sdk-providers/fireworks#image-models) | `accounts/fireworks/models/playground-v2-1024px-aesthetic` | 640x1536, 768x1344, 832x1216, 896x1152, 1024x1024, 1152x896, 1216x832, 1344x768, 1536x640 | | [Fireworks](/providers/ai-sdk-providers/fireworks#image-models) | `accounts/fireworks/models/SSD-1B` | 640x1536, 768x1344, 832x1216, 896x1152, 1024x1024, 1152x896, 1216x832, 1344x768, 1536x640 | | [Fireworks](/providers/ai-sdk-providers/fireworks#image-models) | `accounts/fireworks/models/stable-diffusion-xl-1024-v1-0` | 640x1536, 768x1344, 832x1216, 896x1152, 1024x1024, 1152x896, 1216x832, 1344x768, 1536x640 | | [Luma](/providers/ai-sdk-providers/luma#image-models) | `photon-1` | 1:1, 3:4, 4:3, 9:16, 16:9, 9:21, 21:9 | | [Luma](/providers/ai-sdk-providers/luma#image-models) | `photon-flash-1` | 1:1, 3:4, 4:3, 9:16, 16:9, 9:21, 21:9 | | [Together.ai](/providers/ai-sdk-providers/togetherai#image-models) | `stabilityai/stable-diffusion-xl-base-1.0` | 512x512, 768x768, 1024x1024 | | [Together.ai](/providers/ai-sdk-providers/togetherai#image-models) | `black-forest-labs/FLUX.1-dev` | 512x512, 768x768, 1024x1024 | | [Together.ai](/providers/ai-sdk-providers/togetherai#image-models) | `black-forest-labs/FLUX.1-dev-lora` | 512x512, 768x768, 1024x1024 | | [Together.ai](/providers/ai-sdk-providers/togetherai#image-models) | `black-forest-labs/FLUX.1-schnell` | 512x512, 768x768, 1024x1024 | | [Together.ai](/providers/ai-sdk-providers/togetherai#image-models) | `black-forest-labs/FLUX.1-canny` | 512x512, 768x768, 1024x1024 | | [Together.ai](/providers/ai-sdk-providers/togetherai#image-models) | `black-forest-labs/FLUX.1-depth` | 512x512, 768x768, 1024x1024 | | [Together.ai](/providers/ai-sdk-providers/togetherai#image-models) | `black-forest-labs/FLUX.1-redux` | 512x512, 768x768, 1024x1024 | | [Together.ai](/providers/ai-sdk-providers/togetherai#image-models) | `black-forest-labs/FLUX.1.1-pro` | 512x512, 768x768, 1024x1024 | | [Together.ai](/providers/ai-sdk-providers/togetherai#image-models) | `black-forest-labs/FLUX.1-pro` | 512x512, 768x768, 1024x1024 | | [Together.ai](/providers/ai-sdk-providers/togetherai#image-models) | `black-forest-labs/FLUX.1-schnell-Free` | 512x512, 768x768, 1024x1024 | Above are a small subset of the image models supported by the AI SDK providers. For more, see the respective provider documentation. --- File: /ai/content/docs/03-ai-sdk-core/36-transcription.mdx --- --- title: Transcription description: Learn how to transcribe audio with the AI SDK. --- # Transcription <Note type="warning">Transcription is an experimental feature.</Note> The AI SDK provides the [`transcribe`](/docs/reference/ai-sdk-core/transcribe) function to transcribe audio using a transcription model. ```ts import { experimental_transcribe as transcribe } from 'ai'; import { openai } from '@ai-sdk/openai'; import { readFile } from 'fs/promises'; const transcript = await transcribe({ model: openai.transcription('whisper-1'), audio: await readFile('audio.mp3'), }); ``` The `audio` property can be a `Uint8Array`, `ArrayBuffer`, `Buffer`, `string` (base64 encoded audio data), or a `URL`. To access the generated transcript: ```ts const text = transcript.text; // transcript text e.g. "Hello, world!" const segments = transcript.segments; // array of segments with start and end times, if available const language = transcript.language; // language of the transcript e.g. "en", if available const durationInSeconds = transcript.durationInSeconds; // duration of the transcript in seconds, if available ``` ## Settings ### Provider-Specific settings Transcription models often have provider or model-specific settings which you can set using the `providerOptions` parameter. ```ts highlight="8-12" import { experimental_transcribe as transcribe } from 'ai'; import { openai } from '@ai-sdk/openai'; import { readFile } from 'fs/promises'; const transcript = await transcribe({ model: openai.transcription('whisper-1'), audio: await readFile('audio.mp3'), providerOptions: { openai: { timestampGranularities: ['word'], }, }, }); ``` ### Abort Signals and Timeouts `transcribe` accepts an optional `abortSignal` parameter of type [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that you can use to abort the transcription process or set a timeout. ```ts highlight="8" import { openai } from '@ai-sdk/openai'; import { experimental_transcribe as transcribe } from 'ai'; import { readFile } from 'fs/promises'; const transcript = await transcribe({ model: openai.transcription('whisper-1'), audio: await readFile('audio.mp3'), abortSignal: AbortSignal.timeout(1000), // Abort after 1 second }); ``` ### Custom Headers `transcribe` accepts an optional `headers` parameter of type `Record<string, string>` that you can use to add custom headers to the transcription request. ```ts highlight="8" import { openai } from '@ai-sdk/openai'; import { experimental_transcribe as transcribe } from 'ai'; import { readFile } from 'fs/promises'; const transcript = await transcribe({ model: openai.transcription('whisper-1'), audio: await readFile('audio.mp3'), headers: { 'X-Custom-Header': 'custom-value' }, }); ``` ### Warnings Warnings (e.g. unsupported parameters) are available on the `warnings` property. ```ts import { openai } from '@ai-sdk/openai'; import { experimental_transcribe as transcribe } from 'ai'; import { readFile } from 'fs/promises'; const transcript = await transcribe({ model: openai.transcription('whisper-1'), audio: await readFile('audio.mp3'), }); const warnings = transcript.warnings; ``` ### Error Handling When `transcribe` cannot generate a valid transcript, it throws a [`AI_NoTranscriptGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-transcript-generated-error). This error can arise for any the following reasons: - The model failed to generate a response - The model generated a response that could not be parsed The error preserves the following information to help you log the issue: - `responses`: Metadata about the transcription model responses, including timestamp, model, and headers. - `cause`: The cause of the error. You can use this for more detailed error handling. ```ts import { experimental_transcribe as transcribe, NoTranscriptGeneratedError, } from 'ai'; import { openai } from '@ai-sdk/openai'; import { readFile } from 'fs/promises'; try { await transcribe({ model: openai.transcription('whisper-1'), audio: await readFile('audio.mp3'), }); } catch (error) { if (NoTranscriptGeneratedError.isInstance(error)) { console.log('NoTranscriptGeneratedError'); console.log('Cause:', error.cause); console.log('Responses:', error.responses); } } ``` ## Transcription Models | Provider | Model | | ------------------------------------------------------------------------- | ---------------------------- | | [OpenAI](/providers/ai-sdk-providers/openai#transcription-models) | `whisper-1` | | [OpenAI](/providers/ai-sdk-providers/openai#transcription-models) | `gpt-4o-transcribe` | | [OpenAI](/providers/ai-sdk-providers/openai#transcription-models) | `gpt-4o-mini-transcribe` | | [ElevenLabs](/providers/ai-sdk-providers/elevenlabs#transcription-models) | `scribe_v1` | | [ElevenLabs](/providers/ai-sdk-providers/elevenlabs#transcription-models) | `scribe_v1_experimental` | | [Groq](/providers/ai-sdk-providers/groq#transcription-models) | `whisper-large-v3-turbo` | | [Groq](/providers/ai-sdk-providers/groq#transcription-models) | `distil-whisper-large-v3-en` | | [Groq](/providers/ai-sdk-providers/groq#transcription-models) | `whisper-large-v3` | | [Azure OpenAI](/providers/ai-sdk-providers/azure#transcription-models) | `whisper-1` | | [Azure OpenAI](/providers/ai-sdk-providers/azure#transcription-models) | `gpt-4o-transcribe` | | [Azure OpenAI](/providers/ai-sdk-providers/azure#transcription-models) | `gpt-4o-mini-transcribe` | | [Rev.ai](/providers/ai-sdk-providers/revai#transcription-models) | `machine` | | [Rev.ai](/providers/ai-sdk-providers/revai#transcription-models) | `low_cost` | | [Rev.ai](/providers/ai-sdk-providers/revai#transcription-models) | `fusion` | | [Deepgram](/providers/ai-sdk-providers/deepgram#transcription-models) | `base` (+ variants) | | [Deepgram](/providers/ai-sdk-providers/deepgram#transcription-models) | `enhanced` (+ variants) | | [Deepgram](/providers/ai-sdk-providers/deepgram#transcription-models) | `nova` (+ variants) | | [Deepgram](/providers/ai-sdk-providers/deepgram#transcription-models) | `nova-2` (+ variants) | | [Deepgram](/providers/ai-sdk-providers/deepgram#transcription-models) | `nova-3` (+ variants) | | [Gladia](/providers/ai-sdk-providers/gladia#transcription-models) | `default` | | [AssemblyAI](/providers/ai-sdk-providers/assemblyai#transcription-models) | `best` | | [AssemblyAI](/providers/ai-sdk-providers/assemblyai#transcription-models) | `nano` | | [Fal](/providers/ai-sdk-providers/fal#transcription-models) | `whisper` | | [Fal](/providers/ai-sdk-providers/fal#transcription-models) | `wizper` | Above are a small subset of the transcription models supported by the AI SDK providers. For more, see the respective provider documentation. --- File: /ai/content/docs/03-ai-sdk-core/37-speech.mdx --- --- title: Speech description: Learn how to generate speech from text with the AI SDK. --- # Speech <Note type="warning">Speech is an experimental feature.</Note> The AI SDK provides the [`generateSpeech`](/docs/reference/ai-sdk-core/generate-speech) function to generate speech from text using a speech model. ```ts import { experimental_generateSpeech as generateSpeech } from 'ai'; import { openai } from '@ai-sdk/openai'; import { readFile } from 'fs/promises'; const audio = await generateSpeech({ model: openai.speech('tts-1'), text: 'Hello, world!', voice: 'alloy', }); ``` ### Language Setting You can specify the language for speech generation (provider support varies): ```ts import { experimental_generateSpeech as generateSpeech } from 'ai'; import { lmnt } from '@ai-sdk/lmnt'; const audio = await generateSpeech({ model: lmnt.speech('aurora'), text: 'Hola, mundo!', language: 'es', // Spanish }); ``` To access the generated audio: ```ts const audio = audio.audioData; // audio data e.g. Uint8Array ``` ## Settings ### Provider-Specific settings You can set model-specific settings with the `providerOptions` parameter. ```ts highlight="8-12" import { experimental_generateSpeech as generateSpeech } from 'ai'; import { openai } from '@ai-sdk/openai'; import { readFile } from 'fs/promises'; const audio = await generateSpeech({ model: openai.speech('tts-1'), text: 'Hello, world!', providerOptions: { openai: { // ... }, }, }); ``` ### Abort Signals and Timeouts `generateSpeech` accepts an optional `abortSignal` parameter of type [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that you can use to abort the speech generation process or set a timeout. ```ts highlight="8" import { openai } from '@ai-sdk/openai'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import { readFile } from 'fs/promises'; const audio = await generateSpeech({ model: openai.speech('tts-1'), text: 'Hello, world!', abortSignal: AbortSignal.timeout(1000), // Abort after 1 second }); ``` ### Custom Headers `generateSpeech` accepts an optional `headers` parameter of type `Record<string, string>` that you can use to add custom headers to the speech generation request. ```ts highlight="8" import { openai } from '@ai-sdk/openai'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import { readFile } from 'fs/promises'; const audio = await generateSpeech({ model: openai.speech('tts-1'), text: 'Hello, world!', headers: { 'X-Custom-Header': 'custom-value' }, }); ``` ### Warnings Warnings (e.g. unsupported parameters) are available on the `warnings` property. ```ts import { openai } from '@ai-sdk/openai'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import { readFile } from 'fs/promises'; const audio = await generateSpeech({ model: openai.speech('tts-1'), text: 'Hello, world!', }); const warnings = audio.warnings; ``` ### Error Handling When `generateSpeech` cannot generate a valid audio, it throws a [`AI_NoAudioGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-audio-generated-error). This error can arise for any the following reasons: - The model failed to generate a response - The model generated a response that could not be parsed The error preserves the following information to help you log the issue: - `responses`: Metadata about the speech model responses, including timestamp, model, and headers. - `cause`: The cause of the error. You can use this for more detailed error handling. ```ts import { experimental_generateSpeech as generateSpeech, AI_NoAudioGeneratedError, } from 'ai'; import { openai } from '@ai-sdk/openai'; import { readFile } from 'fs/promises'; try { await generateSpeech({ model: openai.speech('tts-1'), text: 'Hello, world!', }); } catch (error) { if (AI_NoAudioGeneratedError.isInstance(error)) { console.log('AI_NoAudioGeneratedError'); console.log('Cause:', error.cause); console.log('Responses:', error.responses); } } ``` ## Speech Models | Provider | Model | | ---------------------------------------------------------- | ----------------- | | [OpenAI](/providers/ai-sdk-providers/openai#speech-models) | `tts-1` | | [OpenAI](/providers/ai-sdk-providers/openai#speech-models) | `tts-1-hd` | | [OpenAI](/providers/ai-sdk-providers/openai#speech-models) | `gpt-4o-mini-tts` | | [LMNT](/providers/ai-sdk-providers/lmnt#speech-models) | `aurora` | | [LMNT](/providers/ai-sdk-providers/lmnt#speech-models) | `blizzard` | | [Hume](/providers/ai-sdk-providers/hume#speech-models) | `default` | Above are a small subset of the speech models supported by the AI SDK providers. For more, see the respective provider documentation. --- File: /ai/content/docs/03-ai-sdk-core/40-middleware.mdx --- --- title: Language Model Middleware description: Learn how to use middleware to enhance the behavior of language models --- # Language Model Middleware Language model middleware is a way to enhance the behavior of language models by intercepting and modifying the calls to the language model. It can be used to add features like guardrails, RAG, caching, and logging in a language model agnostic way. Such middleware can be developed and distributed independently from the language models that they are applied to. ## Using Language Model Middleware You can use language model middleware with the `wrapLanguageModel` function. It takes a language model and a language model middleware and returns a new language model that incorporates the middleware. ```ts import { wrapLanguageModel } from 'ai'; const wrappedLanguageModel = wrapLanguageModel({ model: yourModel, middleware: yourLanguageModelMiddleware, }); ``` The wrapped language model can be used just like any other language model, e.g. in `streamText`: ```ts highlight="2" const result = streamText({ model: wrappedLanguageModel, prompt: 'What cities are in the United States?', }); ``` ## Multiple middlewares You can provide multiple middlewares to the `wrapLanguageModel` function. The middlewares will be applied in the order they are provided. ```ts const wrappedLanguageModel = wrapLanguageModel({ model: yourModel, middleware: [firstMiddleware, secondMiddleware], }); // applied as: firstMiddleware(secondMiddleware(yourModel)) ``` ## Built-in Middleware The AI SDK comes with several built-in middlewares that you can use to configure language models: - `extractReasoningMiddleware`: Extracts reasoning information from the generated text and exposes it as a `reasoning` property on the result. - `simulateStreamingMiddleware`: Simulates streaming behavior with responses from non-streaming language models. - `defaultSettingsMiddleware`: Applies default settings to a language model. ### Extract Reasoning Some providers and models expose reasoning information in the generated text using special tags, e.g. &lt;think&gt; and &lt;/think&gt;. The `extractReasoningMiddleware` function can be used to extract this reasoning information and expose it as a `reasoning` property on the result. ```ts import { wrapLanguageModel, extractReasoningMiddleware } from 'ai'; const model = wrapLanguageModel({ model: yourModel, middleware: extractReasoningMiddleware({ tagName: 'think' }), }); ``` You can then use that enhanced model in functions like `generateText` and `streamText`. The `extractReasoningMiddleware` function also includes a `startWithReasoning` option. When set to `true`, the reasoning tag will be prepended to the generated text. This is useful for models that do not include the reasoning tag at the beginning of the response. For more details, see the [DeepSeek R1 guide](/docs/guides/r1#deepseek-r1-middleware). ### Simulate Streaming The `simulateStreamingMiddleware` function can be used to simulate streaming behavior with responses from non-streaming language models. This is useful when you want to maintain a consistent streaming interface even when using models that only provide complete responses. ```ts import { wrapLanguageModel, simulateStreamingMiddleware } from 'ai'; const model = wrapLanguageModel({ model: yourModel, middleware: simulateStreamingMiddleware(), }); ``` ### Default Settings The `defaultSettingsMiddleware` function can be used to apply default settings to a language model. ```ts import { wrapLanguageModel, defaultSettingsMiddleware } from 'ai'; const model = wrapLanguageModel({ model: yourModel, middleware: defaultSettingsMiddleware({ settings: { temperature: 0.5, maxOutputTokens: 800, providerOptions: { openai: { store: false } }, }, }), }); ``` ## Community Middleware The AI SDK provides a Language Model Middleware specification. Community members can develop middleware that adheres to this specification, making it compatible with the AI SDK ecosystem. Here are some community middlewares that you can explore: ### Custom tool call parser The [Custom tool call parser](https://github.com/minpeter/ai-sdk-tool-call-middleware) middleware extends tool call capabilities to models that don't natively support the OpenAI-style `tools` parameter. This includes many self-hosted and third-party models that lack native function calling features. <Note> Using this middleware on models that support native function calls may result in unintended performance degradation, so check whether your model supports native function calls before deciding to use it. </Note> This middleware enables function calling capabilities by converting function schemas into prompt instructions and parsing the model's responses into structured function calls. It works by transforming the JSON function definitions into natural language instructions the model can understand, then analyzing the generated text to extract function call attempts. This approach allows developers to use the same function calling API across different model providers, even with models that don't natively support the OpenAI-style function calling format, providing a consistent function calling experience regardless of the underlying model implementation. The `@ai-sdk-tool/parser` package offers three middleware variants: - `createToolMiddleware`: A flexible function for creating custom tool call middleware tailored to specific models - `hermesToolMiddleware`: Ready-to-use middleware for Hermes & Qwen format function calls - `gemmaToolMiddleware`: Pre-configured middleware for Gemma 3 model series function call format Here's how you can enable function calls with Gemma models that don't support them natively: ```ts import { wrapLanguageModel } from 'ai'; import { gemmaToolMiddleware } from '@ai-sdk-tool/parser'; const model = wrapLanguageModel({ model: openrouter('google/gemma-3-27b-it'), middleware: gemmaToolMiddleware, }); ``` Find more examples at this [link](https://github.com/minpeter/ai-sdk-tool-call-middleware/tree/main/examples/core/src). ## Implementing Language Model Middleware <Note> Implementing language model middleware is advanced functionality and requires a solid understanding of the [language model specification](https://github.com/vercel/ai/blob/v5/packages/provider/src/language-model/v2/language-model-v2.ts). </Note> You can implement any of the following three function to modify the behavior of the language model: 1. `transformParams`: Transforms the parameters before they are passed to the language model, for both `doGenerate` and `doStream`. 2. `wrapGenerate`: Wraps the `doGenerate` method of the [language model](https://github.com/vercel/ai/blob/v5/packages/provider/src/language-model/v2/language-model-v2.ts). You can modify the parameters, call the language model, and modify the result. 3. `wrapStream`: Wraps the `doStream` method of the [language model](https://github.com/vercel/ai/blob/v5/packages/provider/src/language-model/v2/language-model-v2.ts). You can modify the parameters, call the language model, and modify the result. Here are some examples of how to implement language model middleware: ## Examples <Note> These examples are not meant to be used in production. They are just to show how you can use middleware to enhance the behavior of language models. </Note> ### Logging This example shows how to log the parameters and generated text of a language model call. ```ts import type { LanguageModelV2Middleware, LanguageModelV2StreamPart, } from '@ai-sdk/provider'; export const yourLogMiddleware: LanguageModelV2Middleware = { wrapGenerate: async ({ doGenerate, params }) => { console.log('doGenerate called'); console.log(`params: ${JSON.stringify(params, null, 2)}`); const result = await doGenerate(); console.log('doGenerate finished'); console.log(`generated text: ${result.text}`); return result; }, wrapStream: async ({ doStream, params }) => { console.log('doStream called'); console.log(`params: ${JSON.stringify(params, null, 2)}`); const { stream, ...rest } = await doStream(); let generatedText = ''; const textBlocks = new Map<string, string>(); const transformStream = new TransformStream< LanguageModelV2StreamPart, LanguageModelV2StreamPart >({ transform(chunk, controller) { switch (chunk.type) { case 'text-start': { textBlocks.set(chunk.id, ''); break; } case 'text-delta': { const existing = textBlocks.get(chunk.id) || ''; textBlocks.set(chunk.id, existing + chunk.delta); generatedText += chunk.delta; break; } case 'text-end': { console.log( `Text block ${chunk.id} completed:`, textBlocks.get(chunk.id), ); break; } } controller.enqueue(chunk); }, flush() { console.log('doStream finished'); console.log(`generated text: ${generatedText}`); }, }); return { stream: stream.pipeThrough(transformStream), ...rest, }; }, }; ``` ### Caching This example shows how to build a simple cache for the generated text of a language model call. ```ts import type { LanguageModelV2Middleware } from '@ai-sdk/provider'; const cache = new Map<string, any>(); export const yourCacheMiddleware: LanguageModelV2Middleware = { wrapGenerate: async ({ doGenerate, params }) => { const cacheKey = JSON.stringify(params); if (cache.has(cacheKey)) { return cache.get(cacheKey); } const result = await doGenerate(); cache.set(cacheKey, result); return result; }, // here you would implement the caching logic for streaming }; ``` ### Retrieval Augmented Generation (RAG) This example shows how to use RAG as middleware. <Note> Helper functions like `getLastUserMessageText` and `findSources` are not part of the AI SDK. They are just used in this example to illustrate the concept of RAG. </Note> ```ts import type { LanguageModelV2Middleware } from '@ai-sdk/provider'; export const yourRagMiddleware: LanguageModelV2Middleware = { transformParams: async ({ params }) => { const lastUserMessageText = getLastUserMessageText({ prompt: params.prompt, }); if (lastUserMessageText == null) { return params; // do not use RAG (send unmodified parameters) } const instruction = 'Use the following information to answer the question:\n' + findSources({ text: lastUserMessageText }) .map(chunk => JSON.stringify(chunk)) .join('\n'); return addToLastUserMessage({ params, text: instruction }); }, }; ``` ### Guardrails Guard rails are a way to ensure that the generated text of a language model call is safe and appropriate. This example shows how to use guardrails as middleware. ```ts import type { LanguageModelV2Middleware } from '@ai-sdk/provider'; export const yourGuardrailMiddleware: LanguageModelV2Middleware = { wrapGenerate: async ({ doGenerate }) => { const { text, ...rest } = await doGenerate(); // filtering approach, e.g. for PII or other sensitive information: const cleanedText = text?.replace(/badword/g, '<REDACTED>'); return { text: cleanedText, ...rest }; }, // here you would implement the guardrail logic for streaming // Note: streaming guardrails are difficult to implement, because // you do not know the full content of the stream until it's finished. }; ``` ## Configuring Per Request Custom Metadata To send and access custom metadata in Middleware, you can use `providerOptions`. This is useful when building logging middleware where you want to pass additional context like user IDs, timestamps, or other contextual data that can help with tracking and debugging. ```ts import { openai } from '@ai-sdk/openai'; import { generateText, wrapLanguageModel } from 'ai'; import type { LanguageModelV2Middleware } from '@ai-sdk/provider'; export const yourLogMiddleware: LanguageModelV2Middleware = { wrapGenerate: async ({ doGenerate, params }) => { console.log('METADATA', params?.providerMetadata?.yourLogMiddleware); const result = await doGenerate(); return result; }, }; const { text } = await generateText({ model: wrapLanguageModel({ model: openai('gpt-4o'), middleware: yourLogMiddleware, }), prompt: 'Invent a new holiday and describe its traditions.', providerOptions: { yourLogMiddleware: { hello: 'world', }, }, }); console.log(text); ``` --- File: /ai/content/docs/03-ai-sdk-core/45-provider-management.mdx --- --- title: Provider & Model Management description: Learn how to work with multiple providers and models --- # Provider & Model Management When you work with multiple providers and models, it is often desirable to manage them in a central place and access the models through simple string ids. The AI SDK offers [custom providers](/docs/reference/ai-sdk-core/custom-provider) and a [provider registry](/docs/reference/ai-sdk-core/provider-registry) for this purpose: - With **custom providers**, you can pre-configure model settings, provide model name aliases, and limit the available models. - The **provider registry** lets you mix multiple providers and access them through simple string ids. You can mix and match custom providers, the provider registry, and [middleware](/docs/ai-sdk-core/middleware) in your application. ## Custom Providers You can create a [custom provider](/docs/reference/ai-sdk-core/custom-provider) using `customProvider`. ### Example: custom model settings You might want to override the default model settings for a provider or provide model name aliases with pre-configured settings. ```ts import { openai as originalOpenAI } from '@ai-sdk/openai'; import { customProvider, defaultSettingsMiddleware, wrapLanguageModel, } from 'ai'; // custom provider with different provider options: export const openai = customProvider({ languageModels: { // replacement model with custom provider options: 'gpt-4o': wrapLanguageModel({ model: originalOpenAI('gpt-4o'), middleware: defaultSettingsMiddleware({ settings: { providerOptions: { openai: { reasoningEffort: 'high', }, }, }, }), }), // alias model with custom provider options: 'gpt-4o-mini-high-reasoning': wrapLanguageModel({ model: originalOpenAI('gpt-4o-mini'), middleware: defaultSettingsMiddleware({ settings: { providerOptions: { openai: { reasoningEffort: 'high', }, }, }, }), }), }, fallbackProvider: originalOpenAI, }); ``` ### Example: model name alias You can also provide model name aliases, so you can update the model version in one place in the future: ```ts import { anthropic as originalAnthropic } from '@ai-sdk/anthropic'; import { customProvider } from 'ai'; // custom provider with alias names: export const anthropic = customProvider({ languageModels: { opus: originalAnthropic('claude-3-opus-20240229'), sonnet: originalAnthropic('claude-3-5-sonnet-20240620'), haiku: originalAnthropic('claude-3-haiku-20240307'), }, fallbackProvider: originalAnthropic, }); ``` ### Example: limit available models You can limit the available models in the system, even if you have multiple providers. ```ts import { anthropic } from '@ai-sdk/anthropic'; import { openai } from '@ai-sdk/openai'; import { customProvider, defaultSettingsMiddleware, wrapLanguageModel, } from 'ai'; export const myProvider = customProvider({ languageModels: { 'text-medium': anthropic('claude-3-5-sonnet-20240620'), 'text-small': openai('gpt-4o-mini'), 'reasoning-medium': wrapLanguageModel({ model: openai('gpt-4o'), middleware: defaultSettingsMiddleware({ settings: { providerOptions: { openai: { reasoningEffort: 'high', }, }, }, }), }), 'reasoning-fast': wrapLanguageModel({ model: openai('gpt-4o-mini'), middleware: defaultSettingsMiddleware({ settings: { providerOptions: { openai: { reasoningEffort: 'high', }, }, }, }), }), }, embeddingModels: { embedding: openai.textEmbeddingModel('text-embedding-3-small'), }, // no fallback provider }); ``` ## Provider Registry You can create a [provider registry](/docs/reference/ai-sdk-core/provider-registry) with multiple providers and models using `createProviderRegistry`. ### Setup ```ts filename={"registry.ts"} import { anthropic } from '@ai-sdk/anthropic'; import { createOpenAI } from '@ai-sdk/openai'; import { createProviderRegistry } from 'ai'; export const registry = createProviderRegistry({ // register provider with prefix and default setup: anthropic, // register provider with prefix and custom setup: openai: createOpenAI({ apiKey: process.env.OPENAI_API_KEY, }), }); ``` ### Setup with Custom Separator By default, the registry uses `:` as the separator between provider and model IDs. You can customize this separator: ```ts filename={"registry.ts"} import { createProviderRegistry } from 'ai'; import { anthropic } from '@ai-sdk/anthropic'; import { openai } from '@ai-sdk/openai'; export const customSeparatorRegistry = createProviderRegistry( { anthropic, openai, }, { separator: ' > ' }, ); ``` ### Example: Use language models You can access language models by using the `languageModel` method on the registry. The provider id will become the prefix of the model id: `providerId:modelId`. ```ts highlight={"5"} import { generateText } from 'ai'; import { registry } from './registry'; const { text } = await generateText({ model: registry.languageModel('openai:gpt-4.1'), // default separator // or with custom separator: // model: customSeparatorRegistry.languageModel('openai > gpt-4.1'), prompt: 'Invent a new holiday and describe its traditions.', }); ``` ### Example: Use text embedding models You can access text embedding models by using the `textEmbeddingModel` method on the registry. The provider id will become the prefix of the model id: `providerId:modelId`. ```ts highlight={"5"} import { embed } from 'ai'; import { registry } from './registry'; const { embedding } = await embed({ model: registry.textEmbeddingModel('openai:text-embedding-3-small'), value: 'sunny day at the beach', }); ``` ### Example: Use image models You can access image models by using the `imageModel` method on the registry. The provider id will become the prefix of the model id: `providerId:modelId`. ```ts highlight={"5"} import { generateImage } from 'ai'; import { registry } from './registry'; const { image } = await generateImage({ model: registry.imageModel('openai:dall-e-3'), prompt: 'A beautiful sunset over a calm ocean', }); ``` ## Combining Custom Providers, Provider Registry, and Middleware The central idea of provider management is to set up a file that contains all the providers and models you want to use. You may want to pre-configure model settings, provide model name aliases, limit the available models, and more. Here is an example that implements the following concepts: - pass through a full provider with a namespace prefix (here: `xai > *`) - setup an OpenAI-compatible provider with custom api key and base URL (here: `custom > *`) - setup model name aliases (here: `anthropic > fast`, `anthropic > writing`, `anthropic > reasoning`) - pre-configure model settings (here: `anthropic > reasoning`) - validate the provider-specific options (here: `AnthropicProviderOptions`) - use a fallback provider (here: `anthropic > *`) - limit a provider to certain models without a fallback (here: `groq > gemma2-9b-it`, `groq > qwen-qwq-32b`) - define a custom separator for the provider registry (here: `>`) ```ts import { anthropic, AnthropicProviderOptions } from '@ai-sdk/anthropic'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { xai } from '@ai-sdk/xai'; import { groq } from '@ai-sdk/groq'; import { createProviderRegistry, customProvider, defaultSettingsMiddleware, wrapLanguageModel, } from 'ai'; export const registry = createProviderRegistry( { // pass through a full provider with a namespace prefix xai, // access an OpenAI-compatible provider with custom setup custom: createOpenAICompatible({ name: 'provider-name', apiKey: process.env.CUSTOM_API_KEY, baseURL: 'https://api.custom.com/v1', }), // setup model name aliases anthropic: customProvider({ languageModels: { fast: anthropic('claude-3-haiku-20240307'), // simple model writing: anthropic('claude-3-7-sonnet-20250219'), // extended reasoning model configuration: reasoning: wrapLanguageModel({ model: anthropic('claude-3-7-sonnet-20250219'), middleware: defaultSettingsMiddleware({ settings: { maxOutputTokens: 100000, // example default setting providerOptions: { anthropic: { thinking: { type: 'enabled', budgetTokens: 32000, }, } satisfies AnthropicProviderOptions, }, }, }), }), }, fallbackProvider: anthropic, }), // limit a provider to certain models without a fallback groq: customProvider({ languageModels: { 'gemma2-9b-it': groq('gemma2-9b-it'), 'qwen-qwq-32b': groq('qwen-qwq-32b'), }, }), }, { separator: ' > ' }, ); // usage: const model = registry.languageModel('anthropic > reasoning'); ``` --- File: /ai/content/docs/03-ai-sdk-core/50-error-handling.mdx --- --- title: Error Handling description: Learn how to handle errors in the AI SDK Core --- # Error Handling ## Handling regular errors Regular errors are thrown and can be handled using the `try/catch` block. ```ts highlight="3,8-10" import { generateText } from 'ai'; try { const { text } = await generateText({ model: 'openai/gpt-4.1', prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); } catch (error) { // handle error } ``` See [Error Types](/docs/reference/ai-sdk-errors) for more information on the different types of errors that may be thrown. ## Handling streaming errors (simple streams) When errors occur during streams that do not support error chunks, the error is thrown as a regular error. You can handle these errors using the `try/catch` block. ```ts highlight="3,12-14" import { generateText } from 'ai'; try { const { textStream } = streamText({ model: 'openai/gpt-4.1', prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); for await (const textPart of textStream) { process.stdout.write(textPart); } } catch (error) { // handle error } ``` ## Handling streaming errors (streaming with `error` support) Full streams support error parts. You can handle those parts similar to other parts. It is recommended to also add a try-catch block for errors that happen outside of the streaming. ```ts highlight="13-21" import { generateText } from 'ai'; try { const { fullStream } = streamText({ model: 'openai/gpt-4.1', prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); for await (const part of fullStream) { switch (part.type) { // ... handle other part types case 'error': { const error = part.error; // handle error break; } case 'abort': { // handle stream abort break; } case 'tool-error': { const error = part.error; // handle error break; } } } } catch (error) { // handle error } ``` ## Handling stream aborts When streams are aborted (e.g., via chat stop button), you may want to perform cleanup operations like updating stored messages in your UI. Use the `onAbort` callback to handle these cases. The `onAbort` callback is called when a stream is aborted via `AbortSignal`, but `onFinish` is not called. This ensures you can still update your UI state appropriately. ```ts highlight="5-9" import { streamText } from 'ai'; const { textStream } = streamText({ model: 'openai/gpt-4.1', prompt: 'Write a vegetarian lasagna recipe for 4 people.', onAbort: ({ steps }) => { // Update stored messages or perform cleanup console.log('Stream aborted after', steps.length, 'steps'); }, onFinish: ({ steps, totalUsage }) => { // This is called on normal completion console.log('Stream completed normally'); }, }); for await (const textPart of textStream) { process.stdout.write(textPart); } ``` The `onAbort` callback receives: - `steps`: An array of all completed steps before the abort You can also handle abort events directly in the stream: ```ts highlight="10-13" import { streamText } from 'ai'; const { fullStream } = streamText({ model: 'openai/gpt-4.1', prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); for await (const chunk of fullStream) { switch (chunk.type) { case 'abort': { // Handle abort directly in stream console.log('Stream was aborted'); break; } // ... handle other part types } } ``` --- File: /ai/content/docs/03-ai-sdk-core/55-testing.mdx --- --- title: Testing description: Learn how to use AI SDK Core mock providers for testing. --- # Testing Testing language models can be challenging, because they are non-deterministic and calling them is slow and expensive. To enable you to unit test your code that uses the AI SDK, the AI SDK Core includes mock providers and test helpers. You can import the following helpers from `ai/test`: - `MockEmbeddingModelV2`: A mock embedding model using the [embedding model v2 specification](https://github.com/vercel/ai/blob/v5/packages/provider/src/embedding-model/v2/embedding-model-v2.ts). - `MockLanguageModelV2`: A mock language model using the [language model v2 specification](https://github.com/vercel/ai/blob/v5/packages/provider/src/language-model/v2/language-model-v2.ts). - `mockId`: Provides an incrementing integer ID. - `mockValues`: Iterates over an array of values with each call. Returns the last value when the array is exhausted. - [`simulateReadableStream`](/docs/reference/ai-sdk-core/simulate-readable-stream): Simulates a readable stream with delays. With mock providers and test helpers, you can control the output of the AI SDK and test your code in a repeatable and deterministic way without actually calling a language model provider. ## Examples You can use the test helpers with the AI Core functions in your unit tests: ### generateText ```ts import { generateText } from 'ai'; import { MockLanguageModelV2 } from 'ai/test'; const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ finishReason: 'stop', usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 }, content: [{ type: 'text', text: `Hello, world!` }], warnings: [], }), }), prompt: 'Hello, test!', }); ``` ### streamText ```ts import { streamText, simulateReadableStream } from 'ai'; import { MockLanguageModelV2 } from 'ai/test'; const result = streamText({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: simulateReadableStream({ chunks: [ { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'Hello' }, { type: 'text-delta', id: 'text-1', delta: ', ' }, { type: 'text-delta', id: 'text-1', delta: 'world!' }, { type: 'text-end', id: 'text-1' }, { type: 'finish', finishReason: 'stop', logprobs: undefined, usage: { inputTokens: 3, outputTokens: 10, totalTokens: 13 }, }, ], }), }), }), prompt: 'Hello, test!', }); ``` ### generateObject ```ts import { generateObject } from 'ai'; import { MockLanguageModelV2 } from 'ai/test'; import { z } from 'zod'; const result = await generateObject({ model: new MockLanguageModelV2({ doGenerate: async () => ({ finishReason: 'stop', usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 }, content: [{ type: 'text', text: `{"content":"Hello, world!"}` }], warnings: [], }), }), schema: z.object({ content: z.string() }), prompt: 'Hello, test!', }); ``` ### streamObject ```ts import { streamObject, simulateReadableStream } from 'ai'; import { MockLanguageModelV2 } from 'ai/test'; import { z } from 'zod'; const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: simulateReadableStream({ chunks: [ { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: '{ ' }, { type: 'text-delta', id: 'text-1', delta: '"content": ' }, { type: 'text-delta', id: 'text-1', delta: `"Hello, ` }, { type: 'text-delta', id: 'text-1', delta: `world` }, { type: 'text-delta', id: 'text-1', delta: `!"` }, { type: 'text-delta', id: 'text-1', delta: ' }' }, { type: 'text-end', id: 'text-1' }, { type: 'finish', finishReason: 'stop', logprobs: undefined, usage: { inputTokens: 3, outputTokens: 10, totalTokens: 13 }, }, ], }), }), }), schema: z.object({ content: z.string() }), prompt: 'Hello, test!', }); ``` ### Simulate UI Message Stream Responses You can also simulate [UI Message Stream](/docs/ai-sdk-ui/stream-protocol#ui-message-stream) responses for testing, debugging, or demonstration purposes. Here is a Next example: ```ts filename="route.ts" import { simulateReadableStream } from 'ai'; export async function POST(req: Request) { return new Response( simulateReadableStream({ initialDelayInMs: 1000, // Delay before the first chunk chunkDelayInMs: 300, // Delay between chunks chunks: [ `data: {"type":"start","messageId":"msg-123"}\n\n`, `data: {"type":"text-start","id":"text-1"}\n\n`, `data: {"type":"text-delta","id":"text-1","delta":"This"}\n\n`, `data: {"type":"text-delta","id":"text-1","delta":" is an"}\n\n`, `data: {"type":"text-delta","id":"text-1","delta":" example."}\n\n`, `data: {"type":"text-end","id":"text-1"}\n\n`, `data: {"type":"finish"}\n\n`, `data: [DONE]\n\n`, ], }).pipeThrough(new TextEncoderStream()), { status: 200, headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', 'x-vercel-ai-ui-message-stream': 'v1', }, }, ); } ``` --- File: /ai/content/docs/03-ai-sdk-core/60-telemetry.mdx --- --- title: Telemetry description: Using OpenTelemetry with AI SDK Core --- # Telemetry <Note type="warning"> AI SDK Telemetry is experimental and may change in the future. </Note> The AI SDK uses [OpenTelemetry](https://opentelemetry.io/) to collect telemetry data. OpenTelemetry is an open-source observability framework designed to provide standardized instrumentation for collecting telemetry data. Check out the [AI SDK Observability Integrations](/providers/observability) to see providers that offer monitoring and tracing for AI SDK applications. ## Enabling telemetry For Next.js applications, please follow the [Next.js OpenTelemetry guide](https://nextjs.org/docs/app/building-your-application/optimizing/open-telemetry) to enable telemetry first. You can then use the `experimental_telemetry` option to enable telemetry on specific function calls while the feature is experimental: ```ts highlight="4" const result = await generateText({ model: openai('gpt-4.1'), prompt: 'Write a short story about a cat.', experimental_telemetry: { isEnabled: true }, }); ``` When telemetry is enabled, you can also control if you want to record the input values and the output values for the function. By default, both are enabled. You can disable them by setting the `recordInputs` and `recordOutputs` options to `false`. Disabling the recording of inputs and outputs can be useful for privacy, data transfer, and performance reasons. You might for example want to disable recording inputs if they contain sensitive information. ## Telemetry Metadata You can provide a `functionId` to identify the function that the telemetry data is for, and `metadata` to include additional information in the telemetry data. ```ts highlight="6-10" const result = await generateText({ model: openai('gpt-4.1'), prompt: 'Write a short story about a cat.', experimental_telemetry: { isEnabled: true, functionId: 'my-awesome-function', metadata: { something: 'custom', someOtherThing: 'other-value', }, }, }); ``` ## Custom Tracer You may provide a `tracer` which must return an OpenTelemetry `Tracer`. This is useful in situations where you want your traces to use a `TracerProvider` other than the one provided by the `@opentelemetry/api` singleton. ```ts highlight="7" const tracerProvider = new NodeTracerProvider(); const result = await generateText({ model: openai('gpt-4.1'), prompt: 'Write a short story about a cat.', experimental_telemetry: { isEnabled: true, tracer: tracerProvider.getTracer('ai'), }, }); ``` ## Collected Data ### generateText function `generateText` records 3 types of spans: - `ai.generateText` (span): the full length of the generateText call. It contains 1 or more `ai.generateText.doGenerate` spans. It contains the [basic LLM span information](#basic-llm-span-information) and the following attributes: - `operation.name`: `ai.generateText` and the functionId that was set through `telemetry.functionId` - `ai.operationId`: `"ai.generateText"` - `ai.prompt`: the prompt that was used when calling `generateText` - `ai.response.text`: the text that was generated - `ai.response.toolCalls`: the tool calls that were made as part of the generation (stringified JSON) - `ai.response.finishReason`: the reason why the generation finished - `ai.settings.maxOutputTokens`: the maximum number of output tokens that were set - `ai.generateText.doGenerate` (span): a provider doGenerate call. It can contain `ai.toolCall` spans. It contains the [call LLM span information](#call-llm-span-information) and the following attributes: - `operation.name`: `ai.generateText.doGenerate` and the functionId that was set through `telemetry.functionId` - `ai.operationId`: `"ai.generateText.doGenerate"` - `ai.prompt.messages`: the messages that were passed into the provider - `ai.prompt.tools`: array of stringified tool definitions. The tools can be of type `function` or `provider-defined-client`. Function tools have a `name`, `description` (optional), and `inputSchema` (JSON schema). Provider-defined-client tools have a `name`, `id`, and `input` (Record). - `ai.prompt.toolChoice`: the stringified tool choice setting (JSON). It has a `type` property (`auto`, `none`, `required`, `tool`), and if the type is `tool`, a `toolName` property with the specific tool. - `ai.response.text`: the text that was generated - `ai.response.toolCalls`: the tool calls that were made as part of the generation (stringified JSON) - `ai.response.finishReason`: the reason why the generation finished - `ai.toolCall` (span): a tool call that is made as part of the generateText call. See [Tool call spans](#tool-call-spans) for more details. ### streamText function `streamText` records 3 types of spans and 2 types of events: - `ai.streamText` (span): the full length of the streamText call. It contains a `ai.streamText.doStream` span. It contains the [basic LLM span information](#basic-llm-span-information) and the following attributes: - `operation.name`: `ai.streamText` and the functionId that was set through `telemetry.functionId` - `ai.operationId`: `"ai.streamText"` - `ai.prompt`: the prompt that was used when calling `streamText` - `ai.response.text`: the text that was generated - `ai.response.toolCalls`: the tool calls that were made as part of the generation (stringified JSON) - `ai.response.finishReason`: the reason why the generation finished - `ai.settings.maxOutputTokens`: the maximum number of output tokens that were set - `ai.streamText.doStream` (span): a provider doStream call. This span contains an `ai.stream.firstChunk` event and `ai.toolCall` spans. It contains the [call LLM span information](#call-llm-span-information) and the following attributes: - `operation.name`: `ai.streamText.doStream` and the functionId that was set through `telemetry.functionId` - `ai.operationId`: `"ai.streamText.doStream"` - `ai.prompt.messages`: the messages that were passed into the provider - `ai.prompt.tools`: array of stringified tool definitions. The tools can be of type `function` or `provider-defined-client`. Function tools have a `name`, `description` (optional), and `inputSchema` (JSON schema). Provider-defined-client tools have a `name`, `id`, and `input` (Record). - `ai.prompt.toolChoice`: the stringified tool choice setting (JSON). It has a `type` property (`auto`, `none`, `required`, `tool`), and if the type is `tool`, a `toolName` property with the specific tool. - `ai.response.text`: the text that was generated - `ai.response.toolCalls`: the tool calls that were made as part of the generation (stringified JSON) - `ai.response.msToFirstChunk`: the time it took to receive the first chunk in milliseconds - `ai.response.msToFinish`: the time it took to receive the finish part of the LLM stream in milliseconds - `ai.response.avgCompletionTokensPerSecond`: the average number of completion tokens per second - `ai.response.finishReason`: the reason why the generation finished - `ai.toolCall` (span): a tool call that is made as part of the generateText call. See [Tool call spans](#tool-call-spans) for more details. - `ai.stream.firstChunk` (event): an event that is emitted when the first chunk of the stream is received. - `ai.response.msToFirstChunk`: the time it took to receive the first chunk - `ai.stream.finish` (event): an event that is emitted when the finish part of the LLM stream is received. It also records a `ai.stream.firstChunk` event when the first chunk of the stream is received. ### generateObject function `generateObject` records 2 types of spans: - `ai.generateObject` (span): the full length of the generateObject call. It contains 1 or more `ai.generateObject.doGenerate` spans. It contains the [basic LLM span information](#basic-llm-span-information) and the following attributes: - `operation.name`: `ai.generateObject` and the functionId that was set through `telemetry.functionId` - `ai.operationId`: `"ai.generateObject"` - `ai.prompt`: the prompt that was used when calling `generateObject` - `ai.schema`: Stringified JSON schema version of the schema that was passed into the `generateObject` function - `ai.schema.name`: the name of the schema that was passed into the `generateObject` function - `ai.schema.description`: the description of the schema that was passed into the `generateObject` function - `ai.response.object`: the object that was generated (stringified JSON) - `ai.settings.output`: the output type that was used, e.g. `object` or `no-schema` - `ai.generateObject.doGenerate` (span): a provider doGenerate call. It contains the [call LLM span information](#call-llm-span-information) and the following attributes: - `operation.name`: `ai.generateObject.doGenerate` and the functionId that was set through `telemetry.functionId` - `ai.operationId`: `"ai.generateObject.doGenerate"` - `ai.prompt.messages`: the messages that were passed into the provider - `ai.response.object`: the object that was generated (stringified JSON) - `ai.response.finishReason`: the reason why the generation finished ### streamObject function `streamObject` records 2 types of spans and 1 type of event: - `ai.streamObject` (span): the full length of the streamObject call. It contains 1 or more `ai.streamObject.doStream` spans. It contains the [basic LLM span information](#basic-llm-span-information) and the following attributes: - `operation.name`: `ai.streamObject` and the functionId that was set through `telemetry.functionId` - `ai.operationId`: `"ai.streamObject"` - `ai.prompt`: the prompt that was used when calling `streamObject` - `ai.schema`: Stringified JSON schema version of the schema that was passed into the `streamObject` function - `ai.schema.name`: the name of the schema that was passed into the `streamObject` function - `ai.schema.description`: the description of the schema that was passed into the `streamObject` function - `ai.response.object`: the object that was generated (stringified JSON) - `ai.settings.output`: the output type that was used, e.g. `object` or `no-schema` - `ai.streamObject.doStream` (span): a provider doStream call. This span contains an `ai.stream.firstChunk` event. It contains the [call LLM span information](#call-llm-span-information) and the following attributes: - `operation.name`: `ai.streamObject.doStream` and the functionId that was set through `telemetry.functionId` - `ai.operationId`: `"ai.streamObject.doStream"` - `ai.prompt.messages`: the messages that were passed into the provider - `ai.response.object`: the object that was generated (stringified JSON) - `ai.response.msToFirstChunk`: the time it took to receive the first chunk - `ai.response.finishReason`: the reason why the generation finished - `ai.stream.firstChunk` (event): an event that is emitted when the first chunk of the stream is received. - `ai.response.msToFirstChunk`: the time it took to receive the first chunk ### embed function `embed` records 2 types of spans: - `ai.embed` (span): the full length of the embed call. It contains 1 `ai.embed.doEmbed` spans. It contains the [basic embedding span information](#basic-embedding-span-information) and the following attributes: - `operation.name`: `ai.embed` and the functionId that was set through `telemetry.functionId` - `ai.operationId`: `"ai.embed"` - `ai.value`: the value that was passed into the `embed` function - `ai.embedding`: a JSON-stringified embedding - `ai.embed.doEmbed` (span): a provider doEmbed call. It contains the [basic embedding span information](#basic-embedding-span-information) and the following attributes: - `operation.name`: `ai.embed.doEmbed` and the functionId that was set through `telemetry.functionId` - `ai.operationId`: `"ai.embed.doEmbed"` - `ai.values`: the values that were passed into the provider (array) - `ai.embeddings`: an array of JSON-stringified embeddings ### embedMany function `embedMany` records 2 types of spans: - `ai.embedMany` (span): the full length of the embedMany call. It contains 1 or more `ai.embedMany.doEmbed` spans. It contains the [basic embedding span information](#basic-embedding-span-information) and the following attributes: - `operation.name`: `ai.embedMany` and the functionId that was set through `telemetry.functionId` - `ai.operationId`: `"ai.embedMany"` - `ai.values`: the values that were passed into the `embedMany` function - `ai.embeddings`: an array of JSON-stringified embedding - `ai.embedMany.doEmbed` (span): a provider doEmbed call. It contains the [basic embedding span information](#basic-embedding-span-information) and the following attributes: - `operation.name`: `ai.embedMany.doEmbed` and the functionId that was set through `telemetry.functionId` - `ai.operationId`: `"ai.embedMany.doEmbed"` - `ai.values`: the values that were sent to the provider - `ai.embeddings`: an array of JSON-stringified embeddings for each value ## Span Details ### Basic LLM span information Many spans that use LLMs (`ai.generateText`, `ai.generateText.doGenerate`, `ai.streamText`, `ai.streamText.doStream`, `ai.generateObject`, `ai.generateObject.doGenerate`, `ai.streamObject`, `ai.streamObject.doStream`) contain the following attributes: - `resource.name`: the functionId that was set through `telemetry.functionId` - `ai.model.id`: the id of the model - `ai.model.provider`: the provider of the model - `ai.request.headers.*`: the request headers that were passed in through `headers` - `ai.response.providerMetadata`: provider specific metadata returned with the generation response - `ai.settings.maxRetries`: the maximum number of retries that were set - `ai.telemetry.functionId`: the functionId that was set through `telemetry.functionId` - `ai.telemetry.metadata.*`: the metadata that was passed in through `telemetry.metadata` - `ai.usage.completionTokens`: the number of completion tokens that were used - `ai.usage.promptTokens`: the number of prompt tokens that were used ### Call LLM span information Spans that correspond to individual LLM calls (`ai.generateText.doGenerate`, `ai.streamText.doStream`, `ai.generateObject.doGenerate`, `ai.streamObject.doStream`) contain [basic LLM span information](#basic-llm-span-information) and the following attributes: - `ai.response.model`: the model that was used to generate the response. This can be different from the model that was requested if the provider supports aliases. - `ai.response.id`: the id of the response. Uses the ID from the provider when available. - `ai.response.timestamp`: the timestamp of the response. Uses the timestamp from the provider when available. - [Semantic Conventions for GenAI operations](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/) - `gen_ai.system`: the provider that was used - `gen_ai.request.model`: the model that was requested - `gen_ai.request.temperature`: the temperature that was set - `gen_ai.request.max_tokens`: the maximum number of tokens that were set - `gen_ai.request.frequency_penalty`: the frequency penalty that was set - `gen_ai.request.presence_penalty`: the presence penalty that was set - `gen_ai.request.top_k`: the topK parameter value that was set - `gen_ai.request.top_p`: the topP parameter value that was set - `gen_ai.request.stop_sequences`: the stop sequences - `gen_ai.response.finish_reasons`: the finish reasons that were returned by the provider - `gen_ai.response.model`: the model that was used to generate the response. This can be different from the model that was requested if the provider supports aliases. - `gen_ai.response.id`: the id of the response. Uses the ID from the provider when available. - `gen_ai.usage.input_tokens`: the number of prompt tokens that were used - `gen_ai.usage.output_tokens`: the number of completion tokens that were used ### Basic embedding span information Many spans that use embedding models (`ai.embed`, `ai.embed.doEmbed`, `ai.embedMany`, `ai.embedMany.doEmbed`) contain the following attributes: - `ai.model.id`: the id of the model - `ai.model.provider`: the provider of the model - `ai.request.headers.*`: the request headers that were passed in through `headers` - `ai.settings.maxRetries`: the maximum number of retries that were set - `ai.telemetry.functionId`: the functionId that was set through `telemetry.functionId` - `ai.telemetry.metadata.*`: the metadata that was passed in through `telemetry.metadata` - `ai.usage.tokens`: the number of tokens that were used - `resource.name`: the functionId that was set through `telemetry.functionId` ### Tool call spans Tool call spans (`ai.toolCall`) contain the following attributes: - `operation.name`: `"ai.toolCall"` - `ai.operationId`: `"ai.toolCall"` - `ai.toolCall.name`: the name of the tool - `ai.toolCall.id`: the id of the tool call - `ai.toolCall.args`: the input parameters of the tool call - `ai.toolCall.result`: the output result of the tool call. Only available if the tool call is successful and the result is serializable. --- File: /ai/content/docs/03-ai-sdk-core/index.mdx --- --- title: AI SDK Core description: Learn about AI SDK Core. --- # AI SDK Core <IndexCards cards={[ { title: 'Overview', description: 'Learn about AI SDK Core and how to work with Large Language Models (LLMs).', href: '/docs/ai-sdk-core/overview', }, { title: 'Generating Text', description: 'Learn how to generate text.', href: '/docs/ai-sdk-core/generating-text', }, { title: 'Generating Structured Data', description: 'Learn how to generate structured data.', href: '/docs/ai-sdk-core/generating-structured-data', }, { title: 'Tool Calling', description: 'Learn how to do tool calling with AI SDK Core.', href: '/docs/ai-sdk-core/tools-and-tool-calling', }, { title: 'Prompt Engineering', description: 'Learn how to write prompts with AI SDK Core.', href: '/docs/ai-sdk-core/prompt-engineering', }, { title: 'Settings', description: 'Learn how to set up settings for language models generations.', href: '/docs/ai-sdk-core/settings', }, { title: 'Embeddings', description: 'Learn how to use embeddings with AI SDK Core.', href: '/docs/ai-sdk-core/embeddings', }, { title: 'Image Generation', description: 'Learn how to generate images with AI SDK Core.', href: '/docs/ai-sdk-core/image-generation', }, { title: 'Transcription', description: 'Learn how to transcribe audio with AI SDK Core.', href: '/docs/ai-sdk-core/transcription', }, { title: 'Speech', description: 'Learn how to generate speech with AI SDK Core.', href: '/docs/ai-sdk-core/speech', }, { title: 'Provider Management', description: 'Learn how to work with multiple providers.', href: '/docs/ai-sdk-core/provider-management', }, { title: 'Middleware', description: 'Learn how to use middleware with AI SDK Core.', href: '/docs/ai-sdk-core/middleware', }, { title: 'Error Handling', description: 'Learn how to handle errors with AI SDK Core.', href: '/docs/ai-sdk-core/error-handling', }, { title: 'Testing', description: 'Learn how to test with AI SDK Core.', href: '/docs/ai-sdk-core/testing', }, { title: 'Telemetry', description: 'Learn how to use telemetry with AI SDK Core.', href: '/docs/ai-sdk-core/telemetry', }, ]} /> --- File: /ai/content/docs/04-ai-sdk-ui/01-overview.mdx --- --- title: Overview description: An overview of AI SDK UI. --- # AI SDK UI AI SDK UI is designed to help you build interactive chat, completion, and assistant applications with ease. It is a **framework-agnostic toolkit**, streamlining the integration of advanced AI functionalities into your applications. AI SDK UI provides robust abstractions that simplify the complex tasks of managing chat streams and UI updates on the frontend, enabling you to develop dynamic AI-driven interfaces more efficiently. With four main hooks — **`useChat`**, **`useCompletion`**, and **`useObject`** — you can incorporate real-time chat capabilities, text completions, streamed JSON, and interactive assistant features into your app. - **[`useChat`](/docs/ai-sdk-ui/chatbot)** offers real-time streaming of chat messages, abstracting state management for inputs, messages, loading, and errors, allowing for seamless integration into any UI design. - **[`useCompletion`](/docs/ai-sdk-ui/completion)** enables you to handle text completions in your applications, managing the prompt input and automatically updating the UI as new completions are streamed. - **[`useObject`](/docs/ai-sdk-ui/object-generation)** is a hook that allows you to consume streamed JSON objects, providing a simple way to handle and display structured data in your application. These hooks are designed to reduce the complexity and time required to implement AI interactions, letting you focus on creating exceptional user experiences. ## UI Framework Support AI SDK UI supports the following frameworks: [React](https://react.dev/), [Svelte](https://svelte.dev/), [Vue.js](https://vuejs.org/), and [Angular](https://angular.dev/). Here is a comparison of the supported functions across these frameworks: | Function | React | Svelte | Vue.js | Angular | | --------------------------------------------------------- | ------------------- | ------------------------------------ | ------------------- | ------------------------------------ | | [useChat](/docs/reference/ai-sdk-ui/use-chat) | <Check size={18} /> | <Check size={18} /> Chat | <Check size={18} /> | <Check size={18} /> Chat | | [useCompletion](/docs/reference/ai-sdk-ui/use-completion) | <Check size={18} /> | <Check size={18} /> Completion | <Check size={18} /> | <Check size={18} /> Completion | | [useObject](/docs/reference/ai-sdk-ui/use-object) | <Check size={18} /> | <Check size={18} /> StructuredObject | <Cross size={18} /> | <Check size={18} /> StructuredObject | <Note> [Contributions](https://github.com/vercel/ai/blob/main/CONTRIBUTING.md) are welcome to implement missing features for non-React frameworks. </Note> ## Framework Examples Explore these example implementations for different frameworks: - [**Next.js**](https://github.com/vercel/ai/tree/main/examples/next-openai) - [**Nuxt**](https://github.com/vercel/ai/tree/main/examples/nuxt-openai) - [**SvelteKit**](https://github.com/vercel/ai/tree/main/examples/sveltekit-openai) - [**Angular**](https://github.com/vercel/ai/tree/main/examples/angular) ## API Reference Please check out the [AI SDK UI API Reference](/docs/reference/ai-sdk-ui) for more details on each function. --- File: /ai/content/docs/04-ai-sdk-ui/02-chatbot.mdx --- --- title: Chatbot description: Learn how to use the useChat hook. --- # Chatbot The `useChat` hook makes it effortless to create a conversational user interface for your chatbot application. It enables the streaming of chat messages from your AI provider, manages the chat state, and updates the UI automatically as new messages arrive. To summarize, the `useChat` hook provides the following features: - **Message Streaming**: All the messages from the AI provider are streamed to the chat UI in real-time. - **Managed States**: The hook manages the states for input, messages, status, error and more for you. - **Seamless Integration**: Easily integrate your chat AI into any design or layout with minimal effort. In this guide, you will learn how to use the `useChat` hook to create a chatbot application with real-time message streaming. Check out our [chatbot with tools guide](/docs/ai-sdk-ui/chatbot-with-tool-calling) to learn how to use tools in your chatbot. Let's start with the following example first. ## Example ```tsx filename='app/page.tsx' 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; export default function Page() { const { messages, sendMessage, status } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', }), }); const [input, setInput] = useState(''); return ( <> {messages.map(message => ( <div key={message.id}> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, index) => part.type === 'text' ? <span key={index}>{part.text}</span> : null, )} </div> ))} <form onSubmit={e => { e.preventDefault(); if (input.trim()) { sendMessage({ text: input }); setInput(''); } }} > <input value={input} onChange={e => setInput(e.target.value)} disabled={status !== 'ready'} placeholder="Say something..." /> <button type="submit" disabled={status !== 'ready'}> Submit </button> </form> </> ); } ``` ```ts filename='app/api/chat/route.ts' import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4.1'), system: 'You are a helpful assistant.', messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } ``` <Note> The UI messages have a new `parts` property that contains the message parts. We recommend rendering the messages using the `parts` property instead of the `content` property. The parts property supports different message types, including text, tool invocation, and tool result, and allows for more flexible and complex chat UIs. </Note> In the `Page` component, the `useChat` hook will request to your AI provider endpoint whenever the user sends a message using `sendMessage`. The messages are then streamed back in real-time and displayed in the chat UI. This enables a seamless chat experience where the user can see the AI response as soon as it is available, without having to wait for the entire response to be received. ## Customized UI `useChat` also provides ways to manage the chat message states via code, show status, and update messages without being triggered by user interactions. ### Status The `useChat` hook returns a `status`. It has the following possible values: - `submitted`: The message has been sent to the API and we're awaiting the start of the response stream. - `streaming`: The response is actively streaming in from the API, receiving chunks of data. - `ready`: The full response has been received and processed; a new user message can be submitted. - `error`: An error occurred during the API request, preventing successful completion. You can use `status` for e.g. the following purposes: - To show a loading spinner while the chatbot is processing the user's message. - To show a "Stop" button to abort the current message. - To disable the submit button. ```tsx filename='app/page.tsx' highlight="6,22-29,36" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; export default function Page() { const { messages, sendMessage, status, stop } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', }), }); const [input, setInput] = useState(''); return ( <> {messages.map(message => ( <div key={message.id}> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, index) => part.type === 'text' ? <span key={index}>{part.text}</span> : null, )} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div> {status === 'submitted' && <Spinner />} <button type="button" onClick={() => stop()}> Stop </button> </div> )} <form onSubmit={e => { e.preventDefault(); if (input.trim()) { sendMessage({ text: input }); setInput(''); } }} > <input value={input} onChange={e => setInput(e.target.value)} disabled={status !== 'ready'} placeholder="Say something..." /> <button type="submit" disabled={status !== 'ready'}> Submit </button> </form> </> ); } ``` ### Error State Similarly, the `error` state reflects the error object thrown during the fetch request. It can be used to display an error message, disable the submit button, or show a retry button: <Note> We recommend showing a generic error message to the user, such as "Something went wrong." This is a good practice to avoid leaking information from the server. </Note> ```tsx file="app/page.tsx" highlight="6,20-27,33" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; export default function Chat() { const { messages, sendMessage, error, reload } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', }), }); const [input, setInput] = useState(''); return ( <div> {messages.map(m => ( <div key={m.id}> {m.role}:{' '} {m.parts.map((part, index) => part.type === 'text' ? <span key={index}>{part.text}</span> : null, )} </div> ))} {error && ( <> <div>An error occurred.</div> <button type="button" onClick={() => reload()}> Retry </button> </> )} <form onSubmit={e => { e.preventDefault(); if (input.trim()) { sendMessage({ text: input }); setInput(''); } }} > <input value={input} onChange={e => setInput(e.target.value)} disabled={error != null} /> </form> </div> ); } ``` Please also see the [error handling](/docs/ai-sdk-ui/error-handling) guide for more information. ### Modify messages Sometimes, you may want to directly modify some existing messages. For example, a delete button can be added to each message to allow users to remove them from the chat history. The `setMessages` function can help you achieve these tasks: ```tsx const { messages, setMessages } = useChat() const handleDelete = (id) => { setMessages(messages.filter(message => message.id !== id)) } return <> {messages.map(message => ( <div key={message.id}> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, index) => ( part.type === 'text' ? ( <span key={index}>{part.text}</span> ) : null ))} <button onClick={() => handleDelete(message.id)}>Delete</button> </div> ))} ... ``` You can think of `messages` and `setMessages` as a pair of `state` and `setState` in React. ### Cancellation and regeneration It's also a common use case to abort the response message while it's still streaming back from the AI provider. You can do this by calling the `stop` function returned by the `useChat` hook. ```tsx const { stop, status } = useChat() return <> <button onClick={stop} disabled={!(status === 'streaming' || status === 'submitted')}>Stop</button> ... ``` When the user clicks the "Stop" button, the fetch request will be aborted. This avoids consuming unnecessary resources and improves the UX of your chatbot application. Similarly, you can also request the AI provider to reprocess the last message by calling the `regenerate` function returned by the `useChat` hook: ```tsx const { regenerate, status } = useChat(); return ( <> <button onClick={regenerate} disabled={!(status === 'ready' || status === 'error')} > Regenerate </button> ... </> ); ``` When the user clicks the "Regenerate" button, the AI provider will regenerate the last message and replace the current one correspondingly. ### Throttling UI Updates <Note>This feature is currently only available for React.</Note> By default, the `useChat` hook will trigger a render every time a new chunk is received. You can throttle the UI updates with the `experimental_throttle` option. ```tsx filename="page.tsx" highlight="2-3" const { messages, ... } = useChat({ // Throttle the messages and data updates to 50ms: experimental_throttle: 50 }) ``` ## Event Callbacks `useChat` provides optional event callbacks that you can use to handle different stages of the chatbot lifecycle: - `onFinish`: Called when the assistant message is completed - `onError`: Called when an error occurs during the fetch request. - `onData`: Called whenever a data part is received. These callbacks can be used to trigger additional actions, such as logging, analytics, or custom UI updates. ```tsx import { UIMessage } from 'ai'; const { /* ... */ } = useChat({ onFinish: (message, { usage, finishReason }) => { console.log('Finished streaming message:', message); console.log('Token usage:', usage); console.log('Finish reason:', finishReason); }, onError: error => { console.error('An error occurred:', error); }, onData: data => { console.log('Received data part from server:', data); }, }); ``` It's worth noting that you can abort the processing by throwing an error in the `onData` callback. This will trigger the `onError` callback and stop the message from being appended to the chat UI. This can be useful for handling unexpected responses from the AI provider. ## Request Configuration ### Custom headers, body, and credentials By default, the `useChat` hook sends a HTTP POST request to the `/api/chat` endpoint with the message list as the request body. You can customize the request in two ways: #### Hook-Level Configuration (Applied to all requests) You can configure transport-level options that will be applied to all requests made by the hook: ```tsx import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/custom-chat', headers: { Authorization: 'your_token', }, body: { user_id: '123', }, credentials: 'same-origin', }), }); ``` #### Dynamic Hook-Level Configuration You can also provide functions that return configuration values. This is useful for authentication tokens that need to be refreshed, or for configuration that depends on runtime conditions: ```tsx import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/custom-chat', headers: () => ({ Authorization: `Bearer ${getAuthToken()}`, 'X-User-ID': getCurrentUserId(), }), body: () => ({ sessionId: getCurrentSessionId(), preferences: getUserPreferences(), }), credentials: () => 'include', }), }); ``` <Note> For component state that changes over time, use `useRef` to store the current value and reference `ref.current` in your configuration function, or prefer request-level options (see next section) for better reliability. </Note> #### Request-Level Configuration (Recommended) <Note> **Recommended**: Use request-level options for better flexibility and control. Request-level options take precedence over hook-level options and allow you to customize each request individually. </Note> ```tsx // Pass options as the second parameter to sendMessage sendMessage( { text: input }, { headers: { Authorization: 'Bearer token123', 'X-Custom-Header': 'custom-value', }, body: { temperature: 0.7, max_tokens: 100, user_id: '123', }, metadata: { userId: 'user123', sessionId: 'session456', }, }, ); ``` The request-level options are merged with hook-level options, with request-level options taking precedence. On your server side, you can handle the request with this additional information. ### Setting custom body fields per request You can configure custom `body` fields on a per-request basis using the second parameter of the `sendMessage` function. This is useful if you want to pass in additional information to your backend that is not part of the message list. ```tsx filename="app/page.tsx" highlight="20-25" 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Chat() { const { messages, sendMessage } = useChat(); const [input, setInput] = useState(''); return ( <div> {messages.map(m => ( <div key={m.id}> {m.role}:{' '} {m.parts.map((part, index) => part.type === 'text' ? <span key={index}>{part.text}</span> : null, )} </div> ))} <form onSubmit={event => { event.preventDefault(); if (input.trim()) { sendMessage( { text: input }, { body: { customKey: 'customValue', }, }, ); setInput(''); } }} > <input value={input} onChange={e => setInput(e.target.value)} /> </form> </div> ); } ``` You can retrieve these custom fields on your server side by destructuring the request body: ```ts filename="app/api/chat/route.ts" highlight="3,4" export async function POST(req: Request) { // Extract additional information ("customKey") from the body of the request: const { messages, customKey }: { messages: UIMessage[]; customKey: string } = await req.json(); //... } ``` ## Message Metadata You can attach custom metadata to messages for tracking information like timestamps, model details, and token usage. ```ts // Server: Send metadata about the message return result.toUIMessageStreamResponse({ messageMetadata: ({ part }) => { if (part.type === 'start') { return { createdAt: Date.now(), model: 'gpt-4o', }; } if (part.type === 'finish') { return { totalTokens: part.totalUsage.totalTokens, }; } }, }); ``` ```tsx // Client: Access metadata via message.metadata { messages.map(message => ( <div key={message.id}> {message.role}:{' '} {message.metadata?.createdAt && new Date(message.metadata.createdAt).toLocaleTimeString()} {/* Render message content */} {message.parts.map((part, index) => part.type === 'text' ? <span key={index}>{part.text}</span> : null, )} {/* Show token count if available */} {message.metadata?.totalTokens && ( <span>{message.metadata.totalTokens} tokens</span> )} </div> )); } ``` For complete examples with type safety and advanced use cases, see the [Message Metadata documentation](/docs/ai-sdk-ui/message-metadata). ## Transport Configuration You can configure custom transport behavior using the `transport` option to customize how messages are sent to your API: ```tsx filename="app/page.tsx" import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; export default function Chat() { const { messages, sendMessage } = useChat({ id: 'my-chat', transport: new DefaultChatTransport({ prepareSendMessagesRequest: ({ id, messages }) => { return { body: { id, message: messages[messages.length - 1], }, }; }, }), }); // ... rest of your component } ``` The corresponding API route receives the custom request format: ```ts filename="app/api/chat/route.ts" export async function POST(req: Request) { const { id, message } = await req.json(); // Load existing messages and add the new one const messages = await loadMessages(id); messages.push(message); const result = streamText({ model: openai('gpt-4.1'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } ``` ### Advanced: Trigger-based routing For more complex scenarios like message regeneration, you can use trigger-based routing: ```tsx filename="app/page.tsx" import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; export default function Chat() { const { messages, sendMessage, regenerate } = useChat({ id: 'my-chat', transport: new DefaultChatTransport({ prepareSendMessagesRequest: ({ id, messages, trigger, messageId }) => { if (trigger === 'submit-user-message') { return { body: { trigger: 'submit-user-message', id, message: messages[messages.length - 1], messageId, }, }; } else if (trigger === 'regenerate-assistant-message') { return { body: { trigger: 'regenerate-assistant-message', id, messageId, }, }; } throw new Error(`Unsupported trigger: ${trigger}`); }, }), }); // ... rest of your component } ``` The corresponding API route would handle different triggers: ```ts filename="app/api/chat/route.ts" export async function POST(req: Request) { const { trigger, id, message, messageId } = await req.json(); const chat = await readChat(id); let messages = chat.messages; if (trigger === 'submit-user-message') { // Handle new user message messages = [...messages, message]; } else if (trigger === 'regenerate-assistant-message') { // Handle message regeneration - remove messages after messageId const messageIndex = messages.findIndex(m => m.id === messageId); if (messageIndex !== -1) { messages = messages.slice(0, messageIndex); } } const result = streamText({ model: openai('gpt-4.1'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } ``` To learn more about building custom transports, refer to the [Transport API documentation](/docs/ai-sdk-ui/transport). ## Controlling the response stream With `streamText`, you can control how error messages and usage information are sent back to the client. ### Error Messages By default, the error message is masked for security reasons. The default error message is "An error occurred." You can forward error messages or send your own error message by providing a `getErrorMessage` function: ```ts filename="app/api/chat/route.ts" highlight="13-27" 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-4.1'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ onError: error => { if (error == null) { return 'unknown error'; } if (typeof error === 'string') { return error; } if (error instanceof Error) { return error.message; } return JSON.stringify(error); }, }); } ``` ### Usage Information By default, the usage information is sent back to the client. You can disable it by setting the `sendUsage` option to `false`: ```ts filename="app/api/chat/route.ts" highlight="13" 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-4.1'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ sendUsage: false, }); } ``` ### Text Streams `useChat` can handle plain text streams by setting the `streamProtocol` option to `text`: ```tsx filename="app/page.tsx" highlight="7" 'use client'; import { useChat } from '@ai-sdk/react'; import { TextStreamChatTransport } from 'ai'; export default function Chat() { const { messages } = useChat({ transport: new TextStreamChatTransport({ api: '/api/chat', }), }); return <>...</>; } ``` This configuration also works with other backend servers that stream plain text. Check out the [stream protocol guide](/docs/ai-sdk-ui/stream-protocol) for more information. <Note> When using `TextStreamChatTransport`, tool calls, usage information and finish reasons are not available. </Note> ## Reasoning Some models such as as DeepSeek `deepseek-reasoner` and Anthropic `claude-3-7-sonnet-20250219` support reasoning tokens. These tokens are typically sent before the message content. You can forward them to the client with the `sendReasoning` option: ```ts filename="app/api/chat/route.ts" highlight="13" import { deepseek } from '@ai-sdk/deepseek'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: deepseek('deepseek-reasoner'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ sendReasoning: true, }); } ``` On the client side, you can access the reasoning parts of the message object. Reasoning parts have a `text` property that contains the reasoning content. ```tsx filename="app/page.tsx" messages.map(message => ( <div key={message.id}> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, index) => { // text parts: if (part.type === 'text') { return <div key={index}>{part.text}</div>; } // reasoning parts: if (part.type === 'reasoning') { return <pre key={index}>{part.text}</pre>; } })} </div> )); ``` ## Sources Some providers such as [Perplexity](/providers/ai-sdk-providers/perplexity#sources) and [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai#sources) include sources in the response. Currently sources are limited to web pages that ground the response. You can forward them to the client with the `sendSources` option: ```ts filename="app/api/chat/route.ts" highlight="13" import { perplexity } from '@ai-sdk/perplexity'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: perplexity('sonar-pro'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ sendSources: true, }); } ``` On the client side, you can access source parts of the message object. There are two types of sources: `source-url` for web pages and `source-document` for documents. Here is an example that renders both types of sources: ```tsx filename="app/page.tsx" messages.map(message => ( <div key={message.id}> {message.role === 'user' ? 'User: ' : 'AI: '} {/* Render URL sources */} {message.parts .filter(part => part.type === 'source-url') .map(part => ( <span key={`source-${part.id}`}> [ <a href={part.url} target="_blank"> {part.title ?? new URL(part.url).hostname} </a> ] </span> ))} {/* Render document sources */} {message.parts .filter(part => part.type === 'source-document') .map(part => ( <span key={`source-${part.id}`}> [<span>{part.title ?? `Document ${part.id}`}</span>] </span> ))} </div> )); ``` ## Image Generation Some models such as Google `gemini-2.0-flash-exp` support image generation. When images are generated, they are exposed as files to the client. On the client side, you can access file parts of the message object and render them as images. ```tsx filename="app/page.tsx" messages.map(message => ( <div key={message.id}> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } else if (part.type === 'file' && part.mediaType.startsWith('image/')) { return <img key={index} src={part.url} alt="Generated image" />; } })} </div> )); ``` ## Attachments The `useChat` hook supports sending file attachments along with a message as well as rendering them on the client. This can be useful for building applications that involve sending images, files, or other media content to the AI provider. There are two ways to send files with a message: using a `FileList` object from file inputs or using an array of file objects. ### FileList By using `FileList`, you can send multiple files as attachments along with a message using the file input element. The `useChat` hook will automatically convert them into data URLs and send them to the AI provider. <Note> Currently, only `image/*` and `text/*` content types get automatically converted into [multi-modal content parts](/docs/foundations/prompts#multi-modal-messages). You will need to handle other content types manually. </Note> ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { useRef, useState } from 'react'; export default function Page() { const { messages, sendMessage, status } = useChat(); const [input, setInput] = useState(''); const [files, setFiles] = useState<FileList | undefined>(undefined); const fileInputRef = useRef<HTMLInputElement>(null); return ( <div> <div> {messages.map(message => ( <div key={message.id}> <div>{`${message.role}: `}</div> <div> {message.parts.map((part, index) => { if (part.type === 'text') { return <span key={index}>{part.text}</span>; } if ( part.type === 'file' && part.mediaType?.startsWith('image/') ) { return <img key={index} src={part.url} alt={part.filename} />; } return null; })} </div> </div> ))} </div> <form onSubmit={event => { event.preventDefault(); if (input.trim()) { sendMessage({ text: input, files, }); setInput(''); setFiles(undefined); if (fileInputRef.current) { fileInputRef.current.value = ''; } } }} > <input type="file" onChange={event => { if (event.target.files) { setFiles(event.target.files); } }} multiple ref={fileInputRef} /> <input value={input} placeholder="Send message..." onChange={e => setInput(e.target.value)} disabled={status !== 'ready'} /> </form> </div> ); } ``` ### File Objects You can also send files as objects along with a message. This can be useful for sending pre-uploaded files or data URLs. ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; import { FileUIPart } from 'ai'; export default function Page() { const { messages, sendMessage, status } = useChat(); const [input, setInput] = useState(''); const [files] = useState<FileUIPart[]>([ { type: 'file', filename: 'earth.png', mediaType: 'image/png', url: 'https://example.com/earth.png', }, { type: 'file', filename: 'moon.png', mediaType: 'image/png', url: 'data:image/png;base64,iVBORw0KGgo...', }, ]); return ( <div> <div> {messages.map(message => ( <div key={message.id}> <div>{`${message.role}: `}</div> <div> {message.parts.map((part, index) => { if (part.type === 'text') { return <span key={index}>{part.text}</span>; } if ( part.type === 'file' && part.mediaType?.startsWith('image/') ) { return <img key={index} src={part.url} alt={part.filename} />; } return null; })} </div> </div> ))} </div> <form onSubmit={event => { event.preventDefault(); if (input.trim()) { sendMessage({ text: input, files, }); setInput(''); } }} > <input value={input} placeholder="Send message..." onChange={e => setInput(e.target.value)} disabled={status !== 'ready'} /> </form> </div> ); } ``` ## Type Inference for Tools When working with tools in TypeScript, AI SDK UI provides type inference helpers to ensure type safety for your tool inputs and outputs. ### InferUITool The `InferUITool` type helper infers the input and output types of a single tool for use in UI messages: ```tsx import { InferUITool } from 'ai'; import { z } from 'zod'; const weatherTool = { description: 'Get the current weather', parameters: z.object({ location: z.string().describe('The city and state'), }), execute: async ({ location }) => { return `The weather in ${location} is sunny.`; }, }; // Infer the types from the tool type WeatherUITool = InferUITool<typeof weatherTool>; // This creates a type with: // { // input: { location: string }; // output: string; // } ``` ### InferUITools The `InferUITools` type helper infers the input and output types of a `ToolSet`: ```tsx import { InferUITools, ToolSet } from 'ai'; import { z } from 'zod'; const tools: ToolSet = { weather: { description: 'Get the current weather', parameters: z.object({ location: z.string().describe('The city and state'), }), execute: async ({ location }) => { return `The weather in ${location} is sunny.`; }, }, calculator: { description: 'Perform basic arithmetic', parameters: z.object({ operation: z.enum(['add', 'subtract', 'multiply', 'divide']), a: z.number(), b: z.number(), }), execute: async ({ operation, a, b }) => { switch (operation) { case 'add': return a + b; case 'subtract': return a - b; case 'multiply': return a * b; case 'divide': return a / b; } }, }, }; // Infer the types from the tool set type MyUITools = InferUITools<typeof tools>; // This creates a type with: // { // weather: { input: { location: string }; output: string }; // calculator: { input: { operation: 'add' | 'subtract' | 'multiply' | 'divide'; a: number; b: number }; output: number }; // } ``` ### Using Inferred Types You can use these inferred types to create a custom UIMessage type and pass it to various AI SDK UI functions: ```tsx import { InferUITools, UIMessage, UIDataTypes } from 'ai'; type MyUITools = InferUITools<typeof tools>; type MyUIMessage = UIMessage<never, UIDataTypes, MyUITools>; ``` Pass the custom type to `useChat` or `createUIMessageStream`: ```tsx import { useChat } from '@ai-sdk/react'; import { createUIMessageStream } from 'ai'; import { MyUIMessage } from './types'; // With useChat const { messages } = useChat<MyUIMessage>(); // With createUIMessageStream const stream = createUIMessageStream<MyUIMessage>(/* ... */); ``` This provides full type safety for tool inputs and outputs on the client and server. --- File: /ai/content/docs/04-ai-sdk-ui/03-chatbot-message-persistence.mdx --- --- title: Chatbot Message Persistence description: Learn how to store and load chat messages in a chatbot. --- # Chatbot Message Persistence Being able to store and load chat messages is crucial for most AI chatbots. In this guide, we'll show how to implement message persistence with `useChat` and `streamText`. <Note> This guide does not cover authorization, error handling, or other real-world considerations. It is intended to be a simple example of how to implement message persistence. </Note> ## Starting a new chat When the user navigates to the chat page without providing a chat ID, we need to create a new chat and redirect to the chat page with the new chat ID. ```tsx filename="app/chat/page.tsx" import { redirect } from 'next/navigation'; import { createChat } from '@util/chat-store'; export default async function Page() { const id = await createChat(); // create a new chat redirect(`/chat/${id}`); // redirect to chat page, see below } ``` Our example chat store implementation uses files to store the chat messages. In a real-world application, you would use a database or a cloud storage service, and get the chat ID from the database. That being said, the function interfaces are designed to be easily replaced with other implementations. ```tsx filename="util/chat-store.ts" import { generateId } from 'ai'; import { existsSync, mkdirSync } from 'fs'; import { writeFile } from 'fs/promises'; import path from 'path'; export async function createChat(): Promise<string> { const id = generateId(); // generate a unique chat ID await writeFile(getChatFile(id), '[]'); // create an empty chat file return id; } function getChatFile(id: string): string { const chatDir = path.join(process.cwd(), '.chats'); if (!existsSync(chatDir)) mkdirSync(chatDir, { recursive: true }); return path.join(chatDir, `${id}.json`); } ``` ## Loading an existing chat When the user navigates to the chat page with a chat ID, we need to load the chat messages and display them. ```tsx filename="app/chat/[id]/page.tsx" import { loadChat } from '@util/chat-store'; import Chat from '@ui/chat'; export default async function Page(props: { params: Promise<{ id: string }> }) { const { id } = await props.params; // get the chat ID from the URL const messages = await loadChat(id); // load the chat messages return <Chat id={id} initialMessages={messages} />; // display the chat } ``` The `loadChat` function in our file-based chat store is implemented as follows: ```tsx filename="util/chat-store.ts" import { UIMessage } from 'ai'; import { readFile } from 'fs/promises'; export async function loadChat(id: string): Promise<UIMessage[]> { return JSON.parse(await readFile(getChatFile(id), 'utf8')); } // ... rest of the file ``` The display component is a simple chat component that uses the `useChat` hook to send and receive messages: ```tsx filename="ui/chat.tsx" highlight="10-16" 'use client'; import { UIMessage, useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; export default function Chat({ id, initialMessages, }: { id?: string | undefined; initialMessages?: UIMessage[] } = {}) { const [input, setInput] = useState(''); const { sendMessage, messages } = useChat({ id, // use the provided chat ID messages: initialMessages, // load initial messages transport: new DefaultChatTransport({ api: '/api/chat', }), }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (input.trim()) { sendMessage({ text: input }); setInput(''); } }; // simplified rendering code, extend as needed: return ( <div> {messages.map(m => ( <div key={m.id}> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> ))} <form onSubmit={handleSubmit}> <input value={input} onChange={e => setInput(e.target.value)} placeholder="Type a message..." /> <button type="submit">Send</button> </form> </div> ); } ``` ## Storing messages `useChat` sends the chat id and the messages to the backend. <Note> The `useChat` message format is different from the `ModelMessage` format. The `useChat` message format is designed for frontend display, and contains additional fields such as `id` and `createdAt`. We recommend storing the messages in the `useChat` message format. </Note> Storing messages is done in the `onFinish` callback of the `toUIMessageStreamResponse` function. `onFinish` receives the complete messages including the new AI response as `UIMessage[]`. ```tsx filename="app/api/chat/route.ts" highlight="6,11-17" import { openai } from '@ai-sdk/openai'; import { saveChat } from '@util/chat-store'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; export async function POST(req: Request) { const { messages, chatId }: { messages: UIMessage[]; chatId: string } = await req.json(); const result = streamText({ model: openai('gpt-4o-mini'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ originalMessages: messages, onFinish: ({ messages }) => { saveChat({ chatId, messages }); }, }); } ``` The actual storage of the messages is done in the `saveChat` function, which in our file-based chat store is implemented as follows: ```tsx filename="util/chat-store.ts" import { UIMessage } from 'ai'; import { writeFile } from 'fs/promises'; export async function saveChat({ chatId, messages, }: { chatId: string; messages: UIMessage[]; }): Promise<void> { const content = JSON.stringify(messages, null, 2); await writeFile(getChatFile(chatId), content); } // ... rest of the file ``` ## Message IDs In addition to a chat ID, each message has an ID. You can use this message ID to e.g. manipulate individual messages. ### Client-side vs Server-side ID Generation By default, message IDs are generated client-side: - User message IDs are generated by the `useChat` hook on the client - AI response message IDs are generated by `streamText` on the server For applications without persistence, client-side ID generation works perfectly. However, **for persistence, you need server-side generated IDs** to ensure consistency across sessions and prevent ID conflicts when messages are stored and retrieved. ### Setting Up Server-side ID Generation When implementing persistence, you have two options for generating server-side IDs: 1. **Using `generateMessageId` in `toUIMessageStreamResponse`** 2. **Setting IDs in your start message part with `createUIMessageStream`** #### Option 1: Using `generateMessageId` in `toUIMessageStreamResponse` You can control the ID format by providing ID generators using [`createIdGenerator()`](/docs/reference/ai-sdk-core/create-id-generator): ```tsx filename="app/api/chat/route.ts" highlight="7-11" import { createIdGenerator, streamText } from 'ai'; export async function POST(req: Request) { // ... const result = streamText({ // ... }); return result.toUIMessageStreamResponse({ originalMessages: messages, // Generate consistent server-side IDs for persistence: generateMessageId: createIdGenerator({ prefix: 'msg', size: 16, }), onFinish: ({ messages }) => { saveChat({ chatId, messages }); }, }); } ``` #### Option 2: Setting IDs with `createUIMessageStream` Alternatively, you can use `createUIMessageStream` to control the message ID by writing a start message part: ```tsx filename="app/api/chat/route.ts" highlight="8-18" import { generateId, streamText, createUIMessageStream, createUIMessageStreamResponse, } from 'ai'; export async function POST(req: Request) { const { messages, chatId } = await req.json(); const stream = createUIMessageStream({ execute: ({ writer }) => { // Write start message part with custom ID writer.write({ type: 'start', messageId: generateId(), // Generate server-side ID for persistence }); const result = streamText({ model: openai('gpt-4o-mini'), messages: convertToModelMessages(messages), }); writer.merge(result.toUIMessageStream({ sendStart: false })); // omit start message part }, originalMessages: messages, onFinish: ({ responseMessage }) => { // save your chat here }, }); return createUIMessageStreamResponse({ stream }); } ``` <Note> For client-side applications that don't require persistence, you can still customize client-side ID generation: ```tsx filename="ui/chat.tsx" import { createIdGenerator } from 'ai'; import { useChat } from '@ai-sdk/react'; const { ... } = useChat({ generateId: createIdGenerator({ prefix: 'msgc', size: 16, }), // ... }); ``` </Note> ## Sending only the last message Once you have implemented message persistence, you might want to send only the last message to the server. This reduces the amount of data sent to the server on each request and can improve performance. To achieve this, you can provide a `prepareSendMessagesRequest` function to the transport. This function receives the messages and the chat ID, and returns the request body to be sent to the server. ```tsx filename="ui/chat.tsx" highlight="7-12" import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; const { // ... } = useChat({ // ... transport: new DefaultChatTransport({ api: '/api/chat', // only send the last message to the server: prepareSendMessagesRequest({ messages, id }) { return { body: { message: messages[messages.length - 1], id } }; }, }), }); ``` On the server, you can then load the previous messages and append the new message to the previous messages: ```tsx filename="app/api/chat/route.ts" highlight="2-11" import { convertToModelMessages, UIMessage } from 'ai'; export async function POST(req: Request) { // get the last message from the client: const { message, id } = await req.json(); // load the previous messages from the server: const previousMessages = await loadChat(id); // append the new message to the previous messages: const messages = [...previousMessages, message]; const result = streamText({ // ... messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ originalMessages: messages, onFinish: ({ messages }) => { saveChat({ chatId: id, messages }); }, }); } ``` ## Handling client disconnects By default, the AI SDK `streamText` function uses backpressure to the language model provider to prevent the consumption of tokens that are not yet requested. However, this means that when the client disconnects, e.g. by closing the browser tab or because of a network issue, the stream from the LLM will be aborted and the conversation may end up in a broken state. Assuming that you have a [storage solution](#storing-messages) in place, you can use the `consumeStream` method to consume the stream on the backend, and then save the result as usual. `consumeStream` effectively removes the backpressure, meaning that the result is stored even when the client has already disconnected. ```tsx filename="app/api/chat/route.ts" highlight="19-21" import { convertToModelMessages, streamText, UIMessage } from 'ai'; import { saveChat } from '@util/chat-store'; export async function POST(req: Request) { const { messages, chatId }: { messages: UIMessage[]; chatId: string } = await req.json(); const result = streamText({ model, messages: convertToModelMessages(messages), }); // consume the stream to ensure it runs to completion & triggers onFinish // even when the client response is aborted: result.consumeStream(); // no await return result.toUIMessageStreamResponse({ originalMessages: messages, onFinish: ({ messages }) => { saveChat({ chatId, messages }); }, }); } ``` When the client reloads the page after a disconnect, the chat will be restored from the storage solution. <Note> In production applications, you would also track the state of the request (in progress, complete) in your stored messages and use it on the client to cover the case where the client reloads the page after a disconnection, but the streaming is not yet complete. </Note> ## Resuming ongoing streams <Note>This feature is experimental and may change in future versions.</Note> The `useChat` hook has experimental support for resuming an ongoing chat generation stream by any client, either after a network disconnect or by reloading the chat page. This can be useful for building applications that involve long-running conversations or for ensuring that messages are not lost in case of network failures. The following are the pre-requisities for your chat application to support resumable streams: - Installing the [`resumable-stream`](https://www.npmjs.com/package/resumable-stream) package that helps create and manage the publisher/subscriber mechanism of the streams. - Creating a [Redis](https://vercel.com/marketplace/redis) instance to store the stream state. - Creating a table that tracks the stream IDs associated with a chat. To resume a chat stream, you will use the `resumeStream` function returned by the `useChat` hook. You will call this function during the initial mount of the hook inside the main chat component. ```tsx filename="ui/chat.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, type UIMessage } from 'ai'; import { useEffect } from 'react'; export function Chat({ chatId, autoResume, initialMessages = [], }: { chatId: string; autoResume: boolean; initialMessages: UIMessage[]; }) { const { resumeStream, // ... other useChat returns } = useChat({ id: chatId, messages: initialMessages, transport: new DefaultChatTransport({ api: '/api/chat', }), }); useEffect(() => { if (autoResume) { resumeStream(); } // We want to disable the exhaustive deps rule here because we only want to run this effect once // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return <div>{/* Your chat UI here */}</div>; } ``` The `resumeStream` function makes a `GET` request to your configured chat endpoint (or `/api/chat` by default) whenever your client calls it. If there’s an active stream, it will pick up where it left off, otherwise it simply finishes without error. The `GET` request automatically appends the `chatId` query parameter to the URL to help identify the chat the request belongs to. Using the `chatId`, you can look up the most recent stream ID from the database and resume the stream. ```bash GET /api/chat?chatId=<your-chat-id> ``` Earlier, you must've implemented the `POST` handler for the `/api/chat` route to create new chat generations. When using `resumeStream`, you must also implement the `GET` handler for `/api/chat` route to resume streams. ### 1. Implement the GET handler Add a `GET` method to `/api/chat` that: 1. Reads `chatId` from the query string 2. Validates it’s present 3. Loads any stored stream IDs for that chat 4. Returns the latest one to `streamContext.resumableStream()` 5. Falls back to an empty stream if it’s already closed ```ts filename="app/api/chat/route.ts" import { loadStreams } from '@util/chat-store'; import { createUIMessageStream, JsonToSseTransformStream } from 'ai'; import { after } from 'next/server'; import { createResumableStreamContext } from 'resumable-stream'; export async function GET(request: Request) { const streamContext = createResumableStreamContext({ waitUntil: after, }); const { searchParams } = new URL(request.url); const chatId = searchParams.get('chatId'); if (!chatId) { return new Response('id is required', { status: 400 }); } const streamIds = await loadStreams(chatId); if (!streamIds.length) { return new Response('No streams found', { status: 404 }); } const recentStreamId = streamIds.at(-1); if (!recentStreamId) { return new Response('No recent stream found', { status: 404 }); } const emptyDataStream = createUIMessageStream({ execute: () => {}, }); return new Response( await streamContext.resumableStream(recentStreamId, () => emptyDataStream.pipeThrough(new JsonToSseTransformStream()), ), ); } ``` After you've implemented the `GET` handler, you can update the `POST` handler to handle the creation of resumable streams. ### 2. Update the POST handler When you create a brand-new chat completion, you must: 1. Generate a fresh `streamId` 2. Persist it alongside your `chatId` 3. Kick off a `createUIMessageStream` that pipes tokens as they arrive 4. Hand that new stream to `streamContext.resumableStream()` ```ts filename="app/api/chat/route.ts" import { convertToModelMessages, createUIMessageStream, generateId, streamText, UIMessage, } from 'ai'; import { appendStreamId, saveChat } from '@util/chat-store'; import { createResumableStreamContext } from 'resumable-stream'; import { openai } from '@ai-sdk/openai'; const streamContext = createResumableStreamContext({ waitUntil: after, }); async function POST(request: Request) { const { chatId, messages }: { chatId: string; messages: UIMessage[] } = await request.json(); const streamId = generateId(); // Record this new stream so we can resume later await appendStreamId({ chatId, streamId }); // Build the data stream that will emit tokens const stream = createUIMessageStream({ execute: ({ writer }) => { const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), }); // Return a resumable stream to the client writer.merge(result.toUIMessageStream()); }, }); const resumableStream = await streamContext.resumableStream( streamId, () => stream, ); return resumableStream.toUIMessageStreamResponse({ originalMessages: messages, onFinish: ({ messages }) => { saveChat({ chatId, messages }); }, }); } ``` With both handlers, your clients can now gracefully resume ongoing streams. --- File: /ai/content/docs/04-ai-sdk-ui/03-chatbot-tool-usage.mdx --- --- title: Chatbot Tool Usage description: Learn how to use tools with the useChat hook. --- # Chatbot Tool Usage With [`useChat`](/docs/reference/ai-sdk-ui/use-chat) and [`streamText`](/docs/reference/ai-sdk-core/stream-text), you can use tools in your chatbot application. The AI SDK supports three types of tools in this context: 1. Automatically executed server-side tools 2. Automatically executed client-side tools 3. Tools that require user interaction, such as confirmation dialogs The flow is as follows: 1. The user enters a message in the chat UI. 1. The message is sent to the API route. 1. In your server side route, the language model generates tool calls during the `streamText` call. 1. All tool calls are forwarded to the client. 1. Server-side tools are executed using their `execute` method and their results are forwarded to the client. 1. Client-side tools that should be automatically executed are handled with the `onToolCall` callback. You must call `addToolResult` to provide the tool result. 1. Client-side tool that require user interactions can be displayed in the UI. The tool calls and results are available as tool invocation parts in the `parts` property of the last assistant message. 1. When the user interaction is done, `addToolResult` can be used to add the tool result to the chat. 1. The chat can be configured to automatically submit when all tool results are available using `sendAutomaticallyWhen`. This triggers another iteration of this flow. The tool calls and tool executions are integrated into the assistant message as typed tool parts. A tool part is at first a tool call, and then it becomes a tool result when the tool is executed. The tool result contains all information about the tool call as well as the result of the tool execution. <Note> Tool result submission can be configured using the `sendAutomaticallyWhen` option. You can use the `lastAssistantMessageIsCompleteWithToolCalls` helper to automatically submit when all tool results are available. This simplifies the client-side code while still allowing full control when needed. </Note> ## Example In this example, we'll use three tools: - `getWeatherInformation`: An automatically executed server-side tool that returns the weather in a given city. - `askForConfirmation`: A user-interaction client-side tool that asks the user for confirmation. - `getLocation`: An automatically executed client-side tool that returns a random city. ### API route ```tsx filename='app/api/chat/route.ts' import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; import { z } from 'zod'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), tools: { // server-side tool with execute function: getWeatherInformation: { description: 'show the weather in a given city to the user', inputSchema: z.object({ city: z.string() }), execute: async ({}: { city: string }) => { const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy']; return weatherOptions[ Math.floor(Math.random() * weatherOptions.length) ]; }, }, // client-side tool that starts user interaction: askForConfirmation: { description: 'Ask the user for confirmation.', inputSchema: z.object({ message: z.string().describe('The message to ask for confirmation.'), }), }, // client-side tool that is automatically executed on the client: getLocation: { description: 'Get the user location. Always ask for confirmation before using this tool.', inputSchema: z.object({}), }, }, }); return result.toUIMessageStreamResponse(); } ``` ### Client-side page The client-side page uses the `useChat` hook to create a chatbot application with real-time message streaming. Tool calls are displayed in the chat UI as typed tool parts. Please make sure to render the messages using the `parts` property of the message. There are three things worth mentioning: 1. The [`onToolCall`](/docs/reference/ai-sdk-ui/use-chat#on-tool-call) callback is used to handle client-side tools that should be automatically executed. In this example, the `getLocation` tool is a client-side tool that returns a random city. You call `addToolResult` to provide the result (without `await` to avoid potential deadlocks). 2. The [`sendAutomaticallyWhen`](/docs/reference/ai-sdk-ui/use-chat#send-automatically-when) option with `lastAssistantMessageIsCompleteWithToolCalls` helper automatically submits when all tool results are available. 3. The `parts` array of assistant messages contains tool parts with typed names like `tool-askForConfirmation`. The client-side tool `askForConfirmation` is displayed in the UI. It asks the user for confirmation and displays the result once the user confirms or denies the execution. The result is added to the chat using `addToolResult` with the `tool` parameter for type safety. ```tsx filename='app/page.tsx' highlight="2,6,10,14-20" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls, } from 'ai'; import { useState } from 'react'; export default function Chat() { const { messages, sendMessage, addToolResult } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', }), sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, // run client-side tools that are automatically executed: async onToolCall({ toolCall }) { if (toolCall.toolName === 'getLocation') { const cities = ['New York', 'Los Angeles', 'Chicago', 'San Francisco']; // No await - avoids potential deadlocks addToolResult({ tool: 'getLocation', toolCallId: toolCall.toolCallId, output: cities[Math.floor(Math.random() * cities.length)], }); } }, }); const [input, setInput] = useState(''); return ( <> {messages?.map(message => ( <div key={message.id}> <strong>{`${message.role}: `}</strong> {message.parts.map(part => { switch (part.type) { // render text parts as simple text: case 'text': return part.text; // for tool parts, use the typed tool part names: case 'tool-askForConfirmation': { const callId = part.toolCallId; switch (part.state) { case 'input-streaming': return ( <div key={callId}>Loading confirmation request...</div> ); case 'input-available': return ( <div key={callId}> {part.input.message} <div> <button onClick={() => addToolResult({ tool: 'askForConfirmation', toolCallId: callId, output: 'Yes, confirmed.', }) } > Yes </button> <button onClick={() => addToolResult({ tool: 'askForConfirmation', toolCallId: callId, output: 'No, denied', }) } > No </button> </div> </div> ); case 'output-available': return ( <div key={callId}> Location access allowed: {part.output} </div> ); case 'output-error': return <div key={callId}>Error: {part.errorText}</div>; } break; } case 'tool-getLocation': { const callId = part.toolCallId; switch (part.state) { case 'input-streaming': return ( <div key={callId}>Preparing location request...</div> ); case 'input-available': return <div key={callId}>Getting location...</div>; case 'output-available': return <div key={callId}>Location: {part.output}</div>; case 'output-error': return ( <div key={callId}> Error getting location: {part.errorText} </div> ); } break; } case 'tool-getWeatherInformation': { const callId = part.toolCallId; switch (part.state) { // example of pre-rendering streaming tool inputs: case 'input-streaming': return ( <pre key={callId}>{JSON.stringify(part, null, 2)}</pre> ); case 'input-available': return ( <div key={callId}> Getting weather information for {part.input.city}... </div> ); case 'output-available': return ( <div key={callId}> Weather in {part.input.city}: {part.output} </div> ); case 'output-error': return ( <div key={callId}> Error getting weather for {part.input.city}:{' '} {part.errorText} </div> ); } break; } } })} <br /> </div> ))} <form onSubmit={e => { e.preventDefault(); if (input.trim()) { sendMessage({ text: input }); setInput(''); } }} > <input value={input} onChange={e => setInput(e.target.value)} /> </form> </> ); } ``` ## Dynamic Tools When using dynamic tools (tools with unknown types at compile time), the UI parts use a generic `dynamic-tool` type instead of specific tool types: ```tsx filename='app/page.tsx' { message.parts.map((part, index) => { switch (part.type) { // Static tools with specific (`tool-${toolName}`) types case 'tool-getWeatherInformation': return <WeatherDisplay part={part} />; // Dynamic tools use generic `dynamic-tool` type case 'dynamic-tool': return ( <div key={index}> <h4>Tool: {part.toolName}</h4> {part.state === 'input-streaming' && ( <pre>{JSON.stringify(part.input, null, 2)}</pre> )} {part.state === 'output-available' && ( <pre>{JSON.stringify(part.output, null, 2)}</pre> )} {part.state === 'output-error' && ( <div>Error: {part.errorText}</div> )} </div> ); } }); } ``` Dynamic tools are useful when integrating with: - MCP (Model Context Protocol) tools without schemas - User-defined functions loaded at runtime - External tool providers ## Tool call streaming Tool call streaming is **enabled by default** in AI SDK 5.0, allowing you to stream tool calls while they are being generated. This provides a better user experience by showing tool inputs as they are generated in real-time. ```tsx filename='app/api/chat/route.ts' export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), // toolCallStreaming is enabled by default in v5 // ... }); return result.toUIMessageStreamResponse(); } ``` With tool call streaming enabled, partial tool calls are streamed as part of the data stream. They are available through the `useChat` hook. The typed tool parts of assistant messages will also contain partial tool calls. You can use the `state` property of the tool part to render the correct UI. ```tsx filename='app/page.tsx' highlight="9,10" export default function Chat() { // ... return ( <> {messages?.map(message => ( <div key={message.id}> {message.parts.map(part => { switch (part.type) { case 'tool-askForConfirmation': case 'tool-getLocation': case 'tool-getWeatherInformation': switch (part.state) { case 'input-streaming': return <pre>{JSON.stringify(part.input, null, 2)}</pre>; case 'input-available': return <pre>{JSON.stringify(part.input, null, 2)}</pre>; case 'output-available': return <pre>{JSON.stringify(part.output, null, 2)}</pre>; case 'output-error': return <div>Error: {part.errorText}</div>; } } })} </div> ))} </> ); } ``` ## Step start parts When you are using multi-step tool calls, the AI SDK will add step start parts to the assistant messages. If you want to display boundaries between tool calls, you can use the `step-start` parts as follows: ```tsx filename='app/page.tsx' // ... // where you render the message parts: message.parts.map((part, index) => { switch (part.type) { case 'step-start': // show step boundaries as horizontal lines: return index > 0 ? ( <div key={index} className="text-gray-500"> <hr className="my-2 border-gray-300" /> </div> ) : null; case 'text': // ... case 'tool-askForConfirmation': case 'tool-getLocation': case 'tool-getWeatherInformation': // ... } }); // ... ``` ## Server-side Multi-Step Calls You can also use multi-step calls on the server-side with `streamText`. This works when all invoked tools have an `execute` function on the server side. ```tsx filename='app/api/chat/route.ts' highlight="15-21,24" import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, UIMessage, stepCountIs } from 'ai'; import { z } from 'zod'; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), tools: { getWeatherInformation: { description: 'show the weather in a given city to the user', inputSchema: z.object({ city: z.string() }), // tool has execute function: execute: async ({}: { city: string }) => { const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy']; return weatherOptions[ Math.floor(Math.random() * weatherOptions.length) ]; }, }, }, stopWhen: stepCountIs(5), }); return result.toUIMessageStreamResponse(); } ``` ## Errors Language models can make errors when calling tools. By default, these errors are masked for security reasons, and show up as "An error occurred" in the UI. To surface the errors, you can use the `onError` function when calling `toUIMessageResponse`. ```tsx export function errorHandler(error: unknown) { if (error == null) { return 'unknown error'; } if (typeof error === 'string') { return error; } if (error instanceof Error) { return error.message; } return JSON.stringify(error); } ``` ```tsx const result = streamText({ // ... }); return result.toUIMessageStreamResponse({ onError: errorHandler, }); ``` In case you are using `createUIMessageResponse`, you can use the `onError` function when calling `toUIMessageResponse`: ```tsx const response = createUIMessageResponse({ // ... async execute(dataStream) { // ... }, onError: error => `Custom error: ${error.message}`, }); ``` --- File: /ai/content/docs/04-ai-sdk-ui/04-generative-user-interfaces.mdx --- --- title: Generative User Interfaces description: Learn how to build Generative UI with AI SDK UI. --- # Generative User Interfaces Generative user interfaces (generative UI) is the process of allowing a large language model (LLM) to go beyond text and "generate UI". This creates a more engaging and AI-native experience for users. <WeatherSearch /> At the core of generative UI are [ tools ](/docs/ai-sdk-core/tools-and-tool-calling), which are functions you provide to the model to perform specialized tasks like getting the weather in a location. The model can decide when and how to use these tools based on the context of the conversation. Generative UI is the process of connecting the results of a tool call to a React component. Here's how it works: 1. You provide the model with a prompt or conversation history, along with a set of tools. 2. Based on the context, the model may decide to call a tool. 3. If a tool is called, it will execute and return data. 4. This data can then be passed to a React component for rendering. By passing the tool results to React components, you can create a generative UI experience that's more engaging and adaptive to your needs. ## Build a Generative UI Chat Interface Let's create a chat interface that handles text-based conversations and incorporates dynamic UI elements based on model responses. ### Basic Chat Implementation Start with a basic chat implementation using the `useChat` hook: ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }; return ( <div> {messages.map(message => ( <div key={message.id}> <div>{message.role === 'user' ? 'User: ' : 'AI: '}</div> <div> {message.parts.map((part, index) => { if (part.type === 'text') { return <span key={index}>{part.text}</span>; } return null; })} </div> </div> ))} <form onSubmit={handleSubmit}> <input value={input} onChange={e => setInput(e.target.value)} placeholder="Type a message..." /> <button type="submit">Send</button> </form> </div> ); } ``` To handle the chat requests and model responses, set up an API route: ```ts filename="app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { streamText, convertToModelMessages, UIMessage, stepCountIs } from 'ai'; export async function POST(request: Request) { const { messages }: { messages: UIMessage[] } = await request.json(); const result = streamText({ model: openai('gpt-4o'), system: 'You are a friendly assistant!', messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), }); return result.toUIMessageStreamResponse(); } ``` This API route uses the `streamText` function to process chat messages and stream the model's responses back to the client. ### Create a Tool Before enhancing your chat interface with dynamic UI elements, you need to create a tool and corresponding React component. A tool will allow the model to perform a specific action, such as fetching weather information. Create a new file called `ai/tools.ts` with the following content: ```ts filename="ai/tools.ts" import { tool as createTool } from 'ai'; import { z } from 'zod'; export const weatherTool = createTool({ description: 'Display the weather for a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async function ({ location }) { await new Promise(resolve => setTimeout(resolve, 2000)); return { weather: 'Sunny', temperature: 75, location }; }, }); export const tools = { displayWeather: weatherTool, }; ``` In this file, you've created a tool called `weatherTool`. This tool simulates fetching weather information for a given location. This tool will return simulated data after a 2-second delay. In a real-world application, you would replace this simulation with an actual API call to a weather service. ### Update the API Route Update the API route to include the tool you've defined: ```ts filename="app/api/chat/route.ts" highlight="3,8,14" import { openai } from '@ai-sdk/openai'; import { streamText, convertToModelMessages, UIMessage, stepCountIs } from 'ai'; import { tools } from '@/ai/tools'; export async function POST(request: Request) { const { messages }: { messages: UIMessage[] } = await request.json(); const result = streamText({ model: openai('gpt-4o'), system: 'You are a friendly assistant!', messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), tools, }); return result.toUIMessageStreamResponse(); } ``` Now that you've defined the tool and added it to your `streamText` call, let's build a React component to display the weather information it returns. ### Create UI Components Create a new file called `components/weather.tsx`: ```tsx filename="components/weather.tsx" type WeatherProps = { temperature: number; weather: string; location: string; }; export const Weather = ({ temperature, weather, location }: WeatherProps) => { return ( <div> <h2>Current Weather for {location}</h2> <p>Condition: {weather}</p> <p>Temperature: {temperature}°C</p> </div> ); }; ``` This component will display the weather information for a given location. It takes three props: `temperature`, `weather`, and `location` (exactly what the `weatherTool` returns). ### Render the Weather Component Now that you have your tool and corresponding React component, let's integrate them into your chat interface. You'll render the Weather component when the model calls the weather tool. To check if the model has called a tool, you can check the `parts` array of the UIMessage object for tool-specific parts. In AI SDK 5.0, tool parts use typed naming: `tool-${toolName}` instead of generic types. Update your `page.tsx` file: ```tsx filename="app/page.tsx" highlight="4,9,14-15,19-46" 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; import { Weather } from '@/components/weather'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }; return ( <div> {messages.map(message => ( <div key={message.id}> <div>{message.role === 'user' ? 'User: ' : 'AI: '}</div> <div> {message.parts.map((part, index) => { if (part.type === 'text') { return <span key={index}>{part.text}</span>; } if (part.type === 'tool-displayWeather') { switch (part.state) { case 'input-available': return <div key={index}>Loading weather...</div>; case 'output-available': return ( <div key={index}> <Weather {...part.output} /> </div> ); case 'output-error': return <div key={index}>Error: {part.errorText}</div>; default: return null; } } return null; })} </div> </div> ))} <form onSubmit={handleSubmit}> <input value={input} onChange={e => setInput(e.target.value)} placeholder="Type a message..." /> <button type="submit">Send</button> </form> </div> ); } ``` In this updated code snippet, you: 1. Use manual input state management with `useState` instead of the built-in `input` and `handleInputChange`. 2. Use `sendMessage` instead of `handleSubmit` to send messages. 3. Check the `parts` array of each message for different content types. 4. Handle tool parts with type `tool-displayWeather` and their different states (`input-available`, `output-available`, `output-error`). This approach allows you to dynamically render UI components based on the model's responses, creating a more interactive and context-aware chat experience. ## Expanding Your Generative UI Application You can enhance your chat application by adding more tools and components, creating a richer and more versatile user experience. Here's how you can expand your application: ### Adding More Tools To add more tools, simply define them in your `ai/tools.ts` file: ```ts // Add a new stock tool export const stockTool = createTool({ description: 'Get price for a stock', inputSchema: z.object({ symbol: z.string().describe('The stock symbol to get the price for'), }), execute: async function ({ symbol }) { // Simulated API call await new Promise(resolve => setTimeout(resolve, 2000)); return { symbol, price: 100 }; }, }); // Update the tools object export const tools = { displayWeather: weatherTool, getStockPrice: stockTool, }; ``` Now, create a new file called `components/stock.tsx`: ```tsx type StockProps = { price: number; symbol: string; }; export const Stock = ({ price, symbol }: StockProps) => { return ( <div> <h2>Stock Information</h2> <p>Symbol: {symbol}</p> <p>Price: ${price}</p> </div> ); }; ``` Finally, update your `page.tsx` file to include the new Stock component: ```tsx 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; import { Weather } from '@/components/weather'; import { Stock } from '@/components/stock'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }; return ( <div> {messages.map(message => ( <div key={message.id}> <div>{message.role}</div> <div> {message.parts.map((part, index) => { if (part.type === 'text') { return <span key={index}>{part.text}</span>; } if (part.type === 'tool-displayWeather') { switch (part.state) { case 'input-available': return <div key={index}>Loading weather...</div>; case 'output-available': return ( <div key={index}> <Weather {...part.output} /> </div> ); case 'output-error': return <div key={index}>Error: {part.errorText}</div>; default: return null; } } if (part.type === 'tool-getStockPrice') { switch (part.state) { case 'input-available': return <div key={index}>Loading stock price...</div>; case 'output-available': return ( <div key={index}> <Stock {...part.output} /> </div> ); case 'output-error': return <div key={index}>Error: {part.errorText}</div>; default: return null; } } return null; })} </div> </div> ))} <form onSubmit={handleSubmit}> <input type="text" value={input} onChange={e => setInput(e.target.value)} /> <button type="submit">Send</button> </form> </div> ); } ``` By following this pattern, you can continue to add more tools and components, expanding the capabilities of your Generative UI application. --- File: /ai/content/docs/04-ai-sdk-ui/05-completion.mdx --- --- title: Completion description: Learn how to use the useCompletion hook. --- # Completion The `useCompletion` hook allows you to create a user interface to handle text completions in your application. It enables the streaming of text completions from your AI provider, manages the state for chat input, and updates the UI automatically as new messages are received. <Note> The `useCompletion` hook is now part of the `@ai-sdk/react` package. </Note> In this guide, you will learn how to use the `useCompletion` hook in your application to generate text completions and stream them in real-time to your users. ## Example ```tsx filename='app/page.tsx' 'use client'; import { useCompletion } from '@ai-sdk/react'; export default function Page() { const { completion, input, handleInputChange, handleSubmit } = useCompletion({ api: '/api/completion', }); return ( <form onSubmit={handleSubmit}> <input name="prompt" value={input} onChange={handleInputChange} id="input" /> <button type="submit">Submit</button> <div>{completion}</div> </form> ); } ``` ```ts filename='app/api/completion/route.ts' import { streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { prompt }: { prompt: string } = await req.json(); const result = streamText({ model: openai('gpt-3.5-turbo'), prompt, }); return result.toUIMessageStreamResponse(); } ``` In the `Page` component, the `useCompletion` hook will request to your AI provider endpoint whenever the user submits a message. The completion is then streamed back in real-time and displayed in the UI. This enables a seamless text completion experience where the user can see the AI response as soon as it is available, without having to wait for the entire response to be received. ## Customized UI `useCompletion` also provides ways to manage the prompt via code, show loading and error states, and update messages without being triggered by user interactions. ### Loading and error states To show a loading spinner while the chatbot is processing the user's message, you can use the `isLoading` state returned by the `useCompletion` hook: ```tsx const { isLoading, ... } = useCompletion() return( <> {isLoading ? <Spinner /> : null} </> ) ``` Similarly, the `error` state reflects the error object thrown during the fetch request. It can be used to display an error message, or show a toast notification: ```tsx const { error, ... } = useCompletion() useEffect(() => { if (error) { toast.error(error.message) } }, [error]) // Or display the error message in the UI: return ( <> {error ? <div>{error.message}</div> : null} </> ) ``` ### Controlled input In the initial example, we have `handleSubmit` and `handleInputChange` callbacks that manage the input changes and form submissions. These are handy for common use cases, but you can also use uncontrolled APIs for more advanced scenarios such as form validation or customized components. The following example demonstrates how to use more granular APIs like `setInput` with your custom input and submit button components: ```tsx const { input, setInput } = useCompletion(); return ( <> <MyCustomInput value={input} onChange={value => setInput(value)} /> </> ); ``` ### Cancelation It's also a common use case to abort the response message while it's still streaming back from the AI provider. You can do this by calling the `stop` function returned by the `useCompletion` hook. ```tsx const { stop, isLoading, ... } = useCompletion() return ( <> <button onClick={stop} disabled={!isLoading}>Stop</button> </> ) ``` When the user clicks the "Stop" button, the fetch request will be aborted. This avoids consuming unnecessary resources and improves the UX of your application. ### Throttling UI Updates <Note>This feature is currently only available for React.</Note> By default, the `useCompletion` hook will trigger a render every time a new chunk is received. You can throttle the UI updates with the `experimental_throttle` option. ```tsx filename="page.tsx" highlight="2-3" const { completion, ... } = useCompletion({ // Throttle the completion and data updates to 50ms: experimental_throttle: 50 }) ``` ## Event Callbacks `useCompletion` also provides optional event callbacks that you can use to handle different stages of the chatbot lifecycle. These callbacks can be used to trigger additional actions, such as logging, analytics, or custom UI updates. ```tsx const { ... } = useCompletion({ onResponse: (response: Response) => { console.log('Received response from server:', response) }, onFinish: (prompt: string, completion: string) => { console.log('Finished streaming completion:', completion) }, onError: (error: Error) => { console.error('An error occurred:', error) }, }) ``` It's worth noting that you can abort the processing by throwing an error in the `onResponse` callback. This will trigger the `onError` callback and stop the message from being appended to the chat UI. This can be useful for handling unexpected responses from the AI provider. ## Configure Request Options By default, the `useCompletion` hook sends a HTTP POST request to the `/api/completion` endpoint with the prompt as part of the request body. You can customize the request by passing additional options to the `useCompletion` hook: ```tsx const { messages, input, handleInputChange, handleSubmit } = useCompletion({ api: '/api/custom-completion', headers: { Authorization: 'your_token', }, body: { user_id: '123', }, credentials: 'same-origin', }); ``` In this example, the `useCompletion` hook sends a POST request to the `/api/completion` endpoint with the specified headers, additional body fields, and credentials for that fetch request. On your server side, you can handle the request with these additional information. --- File: /ai/content/docs/04-ai-sdk-ui/08-object-generation.mdx --- --- title: Object Generation description: Learn how to use the useObject hook. --- # Object Generation <Note>`useObject` is an experimental feature and only available in React.</Note> The [`useObject`](/docs/reference/ai-sdk-ui/use-object) hook allows you to create interfaces that represent a structured JSON object that is being streamed. In this guide, you will learn how to use the `useObject` hook in your application to generate UIs for structured data on the fly. ## Example The example shows a small notifications demo app that generates fake notifications in real-time. ### Schema It is helpful to set up the schema in a separate file that is imported on both the client and server. ```ts filename='app/api/notifications/schema.ts' import { z } from 'zod'; // define a schema for the notifications export const notificationSchema = z.object({ notifications: z.array( z.object({ name: z.string().describe('Name of a fictional person.'), message: z.string().describe('Message. Do not use emojis or links.'), }), ), }); ``` ### Client The client uses [`useObject`](/docs/reference/ai-sdk-ui/use-object) to stream the object generation process. The results are partial and are displayed as they are received. Please note the code for handling `undefined` values in the JSX. ```tsx filename='app/page.tsx' 'use client'; import { experimental_useObject as useObject } from '@ai-sdk/react'; import { notificationSchema } from './api/notifications/schema'; export default function Page() { const { object, submit } = useObject({ api: '/api/notifications', schema: notificationSchema, }); return ( <> <button onClick={() => submit('Messages during finals week.')}> Generate notifications </button> {object?.notifications?.map((notification, index) => ( <div key={index}> <p>{notification?.name}</p> <p>{notification?.message}</p> </div> ))} </> ); } ``` ### Server On the server, we use [`streamObject`](/docs/reference/ai-sdk-core/stream-object) to stream the object generation process. ```typescript filename='app/api/notifications/route.ts' import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import { notificationSchema } from './schema'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const context = await req.json(); const result = streamObject({ model: openai('gpt-4.1'), schema: notificationSchema, prompt: `Generate 3 notifications for a messages app in this context:` + context, }); return result.toTextStreamResponse(); } ``` ## Enum Output Mode When you need to classify or categorize input into predefined options, you can use the `enum` output mode with `useObject`. This requires a specific schema structure where the object has `enum` as a key with `z.enum` containing your possible values. ### Example: Text Classification This example shows how to build a simple text classifier that categorizes statements as true or false. #### Client When using `useObject` with enum output mode, your schema must be an object with `enum` as the key: ```tsx filename='app/classify/page.tsx' 'use client'; import { experimental_useObject as useObject } from '@ai-sdk/react'; import { z } from 'zod'; export default function ClassifyPage() { const { object, submit, isLoading } = useObject({ api: '/api/classify', schema: z.object({ enum: z.enum(['true', 'false']) }), }); return ( <> <button onClick={() => submit('The earth is flat')} disabled={isLoading}> Classify statement </button> {object && <div>Classification: {object.enum}</div>} </> ); } ``` #### Server On the server, use `streamObject` with `output: 'enum'` to stream the classification result: ```typescript filename='app/api/classify/route.ts' import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; export async function POST(req: Request) { const context = await req.json(); const result = streamObject({ model: openai('gpt-4.1'), output: 'enum', enum: ['true', 'false'], prompt: `Classify this statement as true or false: ${context}`, }); return result.toTextStreamResponse(); } ``` ## Customized UI `useObject` also provides ways to show loading and error states: ### Loading State The `isLoading` state returned by the `useObject` hook can be used for several purposes: - To show a loading spinner while the object is generated. - To disable the submit button. ```tsx filename='app/page.tsx' highlight="6,13-20,24" 'use client'; import { useObject } from '@ai-sdk/react'; export default function Page() { const { isLoading, object, submit } = useObject({ api: '/api/notifications', schema: notificationSchema, }); return ( <> {isLoading && <Spinner />} <button onClick={() => submit('Messages during finals week.')} disabled={isLoading} > Generate notifications </button> {object?.notifications?.map((notification, index) => ( <div key={index}> <p>{notification?.name}</p> <p>{notification?.message}</p> </div> ))} </> ); } ``` ### Stop Handler The `stop` function can be used to stop the object generation process. This can be useful if the user wants to cancel the request or if the server is taking too long to respond. ```tsx filename='app/page.tsx' highlight="6,14-16" 'use client'; import { useObject } from '@ai-sdk/react'; export default function Page() { const { isLoading, stop, object, submit } = useObject({ api: '/api/notifications', schema: notificationSchema, }); return ( <> {isLoading && ( <button type="button" onClick={() => stop()}> Stop </button> )} <button onClick={() => submit('Messages during finals week.')}> Generate notifications </button> {object?.notifications?.map((notification, index) => ( <div key={index}> <p>{notification?.name}</p> <p>{notification?.message}</p> </div> ))} </> ); } ``` ### Error State Similarly, the `error` state reflects the error object thrown during the fetch request. It can be used to display an error message, or to disable the submit button: <Note> We recommend showing a generic error message to the user, such as "Something went wrong." This is a good practice to avoid leaking information from the server. </Note> ```tsx file="app/page.tsx" highlight="6,13" 'use client'; import { useObject } from '@ai-sdk/react'; export default function Page() { const { error, object, submit } = useObject({ api: '/api/notifications', schema: notificationSchema, }); return ( <> {error && <div>An error occurred.</div>} <button onClick={() => submit('Messages during finals week.')}> Generate notifications </button> {object?.notifications?.map((notification, index) => ( <div key={index}> <p>{notification?.name}</p> <p>{notification?.message}</p> </div> ))} </> ); } ``` ## Event Callbacks `useObject` provides optional event callbacks that you can use to handle life-cycle events. - `onFinish`: Called when the object generation is completed. - `onError`: Called when an error occurs during the fetch request. These callbacks can be used to trigger additional actions, such as logging, analytics, or custom UI updates. ```tsx filename='app/page.tsx' highlight="10-20" 'use client'; import { experimental_useObject as useObject } from '@ai-sdk/react'; import { notificationSchema } from './api/notifications/schema'; export default function Page() { const { object, submit } = useObject({ api: '/api/notifications', schema: notificationSchema, onFinish({ object, error }) { // typed object, undefined if schema validation fails: console.log('Object generation completed:', object); // error, undefined if schema validation succeeds: console.log('Schema validation error:', error); }, onError(error) { // error during fetch request: console.error('An error occurred:', error); }, }); return ( <div> <button onClick={() => submit('Messages during finals week.')}> Generate notifications </button> {object?.notifications?.map((notification, index) => ( <div key={index}> <p>{notification?.name}</p> <p>{notification?.message}</p> </div> ))} </div> ); } ``` ## Configure Request Options You can configure the API endpoint, optional headers and credentials using the `api`, `headers` and `credentials` settings. ```tsx highlight="2-5" const { submit, object } = useObject({ api: '/api/use-object', headers: { 'X-Custom-Header': 'CustomValue', }, credentials: 'include', schema: yourSchema, }); ``` --- File: /ai/content/docs/04-ai-sdk-ui/20-streaming-data.mdx --- --- title: Streaming Custom Data description: Learn how to stream custom data from the server to the client. --- # Streaming Custom Data It is often useful to send additional data alongside the model's response. For example, you may want to send status information, the message ids after storing them, or references to content that the language model is referring to. The AI SDK provides several helpers that allows you to stream additional data to the client and attach it to the `UIMessage` parts array: - `createUIMessageStream`: creates a data stream - `createUIMessageStreamResponse`: creates a response object that streams data - `pipeUIMessageStreamToResponse`: pipes a data stream to a server response object The data is streamed as part of the response stream using Server-Sent Events. ## Setting Up Type-Safe Data Streaming First, define your custom message type with data part schemas for type safety: ```tsx filename="ai/types.ts" import { UIMessage } from 'ai'; // Define your custom message type with data part schemas export type MyUIMessage = UIMessage< never, // metadata type { weather: { city: string; weather?: string; status: 'loading' | 'success'; }; notification: { message: string; level: 'info' | 'warning' | 'error'; }; } // data parts type >; ``` ## Streaming Data from the Server In your server-side route handler, you can create a `UIMessageStream` and then pass it to `createUIMessageStreamResponse`: ```tsx filename="route.ts" import { openai } from '@ai-sdk/openai'; import { createUIMessageStream, createUIMessageStreamResponse, streamText, convertToModelMessages, } from 'ai'; import { MyUIMessage } from '@/ai/types'; export async function POST(req: Request) { const { messages } = await req.json(); const stream = createUIMessageStream<MyUIMessage>({ execute: ({ writer }) => { // 1. Send initial status (transient - won't be added to message history) writer.write({ type: 'data-notification', data: { message: 'Processing your request...', level: 'info' }, transient: true, // This part won't be added to message history }); // 2. Send sources (useful for RAG use cases) writer.write({ type: 'source', value: { type: 'source', sourceType: 'url', id: 'source-1', url: 'https://weather.com', title: 'Weather Data Source', }, }); // 3. Send data parts with loading state writer.write({ type: 'data-weather', id: 'weather-1', data: { city: 'San Francisco', status: 'loading' }, }); const result = streamText({ model: openai('gpt-4.1'), messages: convertToModelMessages(messages), onFinish() { // 4. Update the same data part (reconciliation) writer.write({ type: 'data-weather', id: 'weather-1', // Same ID = update existing part data: { city: 'San Francisco', weather: 'sunny', status: 'success', }, }); // 5. Send completion notification (transient) writer.write({ type: 'data-notification', data: { message: 'Request completed', level: 'info' }, transient: true, // Won't be added to message history }); }, }); writer.merge(result.toUIMessageStream()); }, }); return createUIMessageStreamResponse({ stream }); } ``` <Note> You can also send stream data from custom backends, e.g. Python / FastAPI, using the [UI Message Stream Protocol](/docs/ai-sdk-ui/stream-protocol#ui-message-stream-protocol). </Note> ## Types of Streamable Data ### Data Parts (Persistent) Regular data parts are added to the message history and appear in `message.parts`: ```tsx writer.write({ type: 'data-weather', id: 'weather-1', // Optional: enables reconciliation data: { city: 'San Francisco', status: 'loading' }, }); ``` ### Sources Sources are useful for RAG implementations where you want to show which documents or URLs were referenced: ```tsx writer.write({ type: 'source', value: { type: 'source', sourceType: 'url', id: 'source-1', url: 'https://example.com', title: 'Example Source', }, }); ``` ### Transient Data Parts (Ephemeral) Transient parts are sent to the client but not added to the message history. They are only accessible via the `onData` useChat handler: ```tsx // server writer.write({ type: 'data-notification', data: { message: 'Processing...', level: 'info' }, transient: true, // Won't be added to message history }); // client const [notification, setNotification] = useState(); const { messages } = useChat({ onData: ({ data, type }) => { if (type === 'data-notification') { setNotification({ message: data.message, level: data.level }); } }, }); ``` ## Data Part Reconciliation When you write to a data part with the same ID, the client automatically reconciles and updates that part. This enables powerful dynamic experiences like: - **Collaborative artifacts** - Update code, documents, or designs in real-time - **Progressive data loading** - Show loading states that transform into final results - **Live status updates** - Update progress bars, counters, or status indicators - **Interactive components** - Build UI elements that evolve based on user interaction The reconciliation happens automatically - simply use the same `id` when writing to the stream. ## Processing Data on the Client ### Using the onData Callback The `onData` callback is essential for handling streaming data, especially transient parts: ```tsx filename="page.tsx" import { useChat } from '@ai-sdk/react'; import { MyUIMessage } from '@/ai/types'; const { messages } = useChat<MyUIMessage>({ api: '/api/chat', onData: dataPart => { // Handle all data parts as they arrive (including transient parts) console.log('Received data part:', dataPart); // Handle different data part types if (dataPart.type === 'data-weather') { console.log('Weather update:', dataPart.data); } // Handle transient notifications (ONLY available here, not in message.parts) if (dataPart.type === 'data-notification') { showToast(dataPart.data.message, dataPart.data.level); } }, }); ``` **Important:** Transient data parts are **only** available through the `onData` callback. They will not appear in the `message.parts` array since they're not added to message history. ### Rendering Persistent Data Parts You can filter and render data parts from the message parts array: ```tsx filename="page.tsx" const result = ( <> {messages?.map(message => ( <div key={message.id}> {/* Render weather data parts */} {message.parts .filter(part => part.type === 'data-weather') .map((part, index) => ( <div key={index} className="weather-widget"> {part.data.status === 'loading' ? ( <>Getting weather for {part.data.city}...</> ) : ( <> Weather in {part.data.city}: {part.data.weather} </> )} </div> ))} {/* Render text content */} {message.parts .filter(part => part.type === 'text') .map((part, index) => ( <div key={index}>{part.text}</div> ))} {/* Render sources */} {message.parts .filter(part => part.type === 'source') .map((part, index) => ( <div key={index} className="source"> Source: <a href={part.url}>{part.title}</a> </div> ))} </div> ))} </> ); ``` ### Complete Example ```tsx filename="page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; import { MyUIMessage } from '@/ai/types'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat<MyUIMessage>({ api: '/api/chat', onData: dataPart => { // Handle transient notifications if (dataPart.type === 'data-notification') { console.log('Notification:', dataPart.data.message); } }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }; return ( <> {messages?.map(message => ( <div key={message.id}> {message.role === 'user' ? 'User: ' : 'AI: '} {/* Render weather data */} {message.parts .filter(part => part.type === 'data-weather') .map((part, index) => ( <span key={index} className="weather-update"> {part.data.status === 'loading' ? ( <>Getting weather for {part.data.city}...</> ) : ( <> Weather in {part.data.city}: {part.data.weather} </> )} </span> ))} {/* Render text content */} {message.parts .filter(part => part.type === 'text') .map((part, index) => ( <div key={index}>{part.text}</div> ))} </div> ))} <form onSubmit={handleSubmit}> <input value={input} onChange={e => setInput(e.target.value)} placeholder="Ask about the weather..." /> <button type="submit">Send</button> </form> </> ); } ``` ## Use Cases - **RAG Applications** - Stream sources and retrieved documents - **Real-time Status** - Show loading states and progress updates - **Collaborative Tools** - Stream live updates to shared artifacts - **Analytics** - Send usage data without cluttering message history - **Notifications** - Display temporary alerts and status messages ## Message Metadata vs Data Parts Both [message metadata](/docs/ai-sdk-ui/message-metadata) and data parts allow you to send additional information alongside messages, but they serve different purposes: ### Message Metadata Message metadata is best for **message-level information** that describes the message as a whole: - Attached at the message level via `message.metadata` - Sent using the `messageMetadata` callback in `toUIMessageStreamResponse` - Ideal for: timestamps, model info, token usage, user context - Type-safe with custom metadata types ```ts // Server: Send metadata about the message return result.toUIMessageStreamResponse({ messageMetadata: ({ part }) => { if (part.type === 'finish') { return { model: part.response.modelId, totalTokens: part.totalUsage.totalTokens, createdAt: Date.now(), }; } }, }); ``` ### Data Parts Data parts are best for streaming **dynamic arbitrary data**: - Added to the message parts array via `message.parts` - Streamed using `createUIMessageStream` and `writer.write()` - Can be reconciled/updated using the same ID - Support transient parts that don't persist - Ideal for: dynamic content, loading states, interactive components ```ts // Server: Stream data as part of message content writer.write({ type: 'data-weather', id: 'weather-1', data: { city: 'San Francisco', status: 'loading' }, }); ``` For more details on message metadata, see the [Message Metadata documentation](/docs/ai-sdk-ui/message-metadata). --- File: /ai/content/docs/04-ai-sdk-ui/21-error-handling.mdx --- --- title: Error Handling description: Learn how to handle errors in the AI SDK UI --- # Error Handling ### Error Helper Object Each AI SDK UI hook also returns an [error](/docs/reference/ai-sdk-ui/use-chat#error) object that you can use to render the error in your UI. You can use the error object to show an error message, disable the submit button, or show a retry button. <Note> We recommend showing a generic error message to the user, such as "Something went wrong." This is a good practice to avoid leaking information from the server. </Note> ```tsx file="app/page.tsx" highlight="7,18-25,31" 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage, error, regenerate } = useChat(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }; return ( <div> {messages.map(m => ( <div key={m.id}> {m.role}:{' '} {m.parts .filter(part => part.type === 'text') .map(part => part.text) .join('')} </div> ))} {error && ( <> <div>An error occurred.</div> <button type="button" onClick={() => regenerate()}> Retry </button> </> )} <form onSubmit={handleSubmit}> <input value={input} onChange={e => setInput(e.target.value)} disabled={error != null} /> </form> </div> ); } ``` #### Alternative: replace last message Alternatively you can write a custom submit handler that replaces the last message when an error is present. ```tsx file="app/page.tsx" highlight="17-23,35" 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { sendMessage, error, messages, setMessages } = useChat(); function customSubmit(event: React.FormEvent<HTMLFormElement>) { event.preventDefault(); if (error != null) { setMessages(messages.slice(0, -1)); // remove last message } sendMessage({ text: input }); setInput(''); } return ( <div> {messages.map(m => ( <div key={m.id}> {m.role}:{' '} {m.parts .filter(part => part.type === 'text') .map(part => part.text) .join('')} </div> ))} {error && <div>An error occurred.</div>} <form onSubmit={customSubmit}> <input value={input} onChange={e => setInput(e.target.value)} /> </form> </div> ); } ``` ### Error Handling Callback Errors can be processed by passing an [`onError`](/docs/reference/ai-sdk-ui/use-chat#on-error) callback function as an option to the [`useChat`](/docs/reference/ai-sdk-ui/use-chat) or [`useCompletion`](/docs/reference/ai-sdk-ui/use-completion) hooks. The callback function receives an error object as an argument. ```tsx file="app/page.tsx" highlight="6-9" import { useChat } from '@ai-sdk/react'; export default function Page() { const { /* ... */ } = useChat({ // handle error: onError: error => { console.error(error); }, }); } ``` ### Injecting Errors for Testing You might want to create errors for testing. You can easily do so by throwing an error in your route handler: ```ts file="app/api/chat/route.ts" export async function POST(req: Request) { throw new Error('This is a test error'); } ``` --- File: /ai/content/docs/04-ai-sdk-ui/21-transport.mdx --- --- title: Transport description: Learn how to use custom transports with useChat. --- # Transport The `useChat` transport system provides fine-grained control over how messages are sent to your API endpoints and how responses are processed. This is particularly useful for alternative communication protocols like WebSockets, custom authentication patterns, or specialized backend integrations. ## Default Transport By default, `useChat` uses HTTP POST requests to send messages to `/api/chat`: ```tsx import { useChat } from '@ai-sdk/react'; // Uses default HTTP transport const { messages, sendMessage } = useChat(); ``` This is equivalent to: ```tsx import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', }), }); ``` ## Custom Transport Configuration Configure the default transport with custom options: ```tsx import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/custom-chat', headers: { Authorization: 'Bearer your-token', 'X-API-Version': '2024-01', }, credentials: 'include', }), }); ``` ### Dynamic Configuration You can also provide functions that return configuration values. This is useful for authentication tokens that need to be refreshed, or for configuration that depends on runtime conditions: ```tsx const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', headers: () => ({ Authorization: `Bearer ${getAuthToken()}`, 'X-User-ID': getCurrentUserId(), }), body: () => ({ sessionId: getCurrentSessionId(), preferences: getUserPreferences(), }), credentials: () => 'include', }), }); ``` ### Request Transformation Transform requests before sending to your API: ```tsx const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', prepareSendMessagesRequest: ({ id, messages, trigger, messageId }) => { return { headers: { 'X-Session-ID': id, }, body: { messages: messages.slice(-10), // Only send last 10 messages trigger, messageId, }, }; }, }), }); ``` ## Building Custom Transports To understand how to build your own transport, refer to the source code of the default implementation: - **[DefaultChatTransport](https://github.com/vercel/ai/blob/main/packages/ai/src/ui/default-chat-transport.ts)** - The complete default HTTP transport implementation - **[HttpChatTransport](https://github.com/vercel/ai/blob/main/packages/ai/src/ui/http-chat-transport.ts)** - Base HTTP transport with request handling - **[ChatTransport Interface](https://github.com/vercel/ai/blob/main/packages/ai/src/ui/chat-transport.ts)** - The transport interface you need to implement These implementations show you exactly how to: - Handle the `sendMessages` method - Process UI message streams - Transform requests and responses - Handle errors and connection management The transport system gives you complete control over how your chat application communicates, enabling integration with any backend protocol or service. --- File: /ai/content/docs/04-ai-sdk-ui/24-reading-ui-message-streams.mdx --- --- title: Reading UIMessage Streams description: Learn how to read UIMessage streams. --- # Reading UI Message Streams `UIMessage` streams are useful outside of traditional chat use cases. You can consume them for terminal UIs, custom stream processing on the client, or React Server Components (RSC). The `readUIMessageStream` helper transforms a stream of `UIMessageChunk` objects into an `AsyncIterableStream` of `UIMessage` objects, allowing you to process messages as they're being constructed. ## Basic Usage ```tsx import { openai } from '@ai-sdk/openai'; import { readUIMessageStream, streamText } from 'ai'; async function main() { const result = streamText({ model: openai('gpt-4o'), prompt: 'Write a short story about a robot.', }); for await (const uiMessage of readUIMessageStream({ stream: result.toUIMessageStream(), })) { console.log('Current message state:', uiMessage); } } ``` ## Tool Calls Integration Handle streaming responses that include tool calls: ```tsx import { openai } from '@ai-sdk/openai'; import { readUIMessageStream, streamText, tool } from 'ai'; import { z } from 'zod'; async function handleToolCalls() { const result = streamText({ model: openai('gpt-4o'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, prompt: 'What is the weather in Tokyo?', }); for await (const uiMessage of readUIMessageStream({ stream: result.toUIMessageStream(), })) { // Handle different part types uiMessage.parts.forEach(part => { switch (part.type) { case 'text': console.log('Text:', part.text); break; case 'tool-call': console.log('Tool called:', part.toolName, 'with args:', part.args); break; case 'tool-result': console.log('Tool result:', part.result); break; } }); } } ``` ## Resuming Conversations Resume streaming from a previous message state: ```tsx import { readUIMessageStream, streamText } from 'ai'; async function resumeConversation(lastMessage: UIMessage) { const result = streamText({ model: openai('gpt-4o'), messages: [ { role: 'user', content: 'Continue our previous conversation.' }, ], }); // Resume from the last message for await (const uiMessage of readUIMessageStream({ stream: result.toUIMessageStream(), message: lastMessage, // Resume from this message })) { console.log('Resumed message:', uiMessage); } } ``` --- File: /ai/content/docs/04-ai-sdk-ui/25-message-metadata.mdx --- --- title: Message Metadata description: Learn how to attach and use metadata with messages in AI SDK UI --- # Message Metadata Message metadata allows you to attach custom information to messages at the message level. This is useful for tracking timestamps, model information, token usage, user context, and other message-level data. ## Overview Message metadata differs from [data parts](/docs/ai-sdk-ui/streaming-data) in that it's attached at the message level rather than being part of the message content. While data parts are ideal for dynamic content that forms part of the message, metadata is perfect for information about the message itself. ## Getting Started Here's a simple example of using message metadata to track timestamps and model information: ### Defining Metadata Types First, define your metadata type for type safety: ```tsx filename="app/types.ts" import { UIMessage } from 'ai'; import { z } from 'zod'; // Define your metadata schema export const messageMetadataSchema = z.object({ createdAt: z.number().optional(), model: z.string().optional(), totalTokens: z.number().optional(), }); export type MessageMetadata = z.infer<typeof messageMetadataSchema>; // Create a typed UIMessage export type MyUIMessage = UIMessage<MessageMetadata>; ``` ### Sending Metadata from the Server Use the `messageMetadata` callback in `toUIMessageStreamResponse` to send metadata at different streaming stages: ```ts filename="app/api/chat/route.ts" highlight="11-20" import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText } from 'ai'; import { MyUIMessage } from '@/types'; export async function POST(req: Request) { const { messages }: { messages: MyUIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ originalMessages: messages, // pass this in for type-safe return objects messageMetadata: ({ part }) => { // Send metadata when streaming starts if (part.type === 'start') { return { createdAt: Date.now(), model: 'gpt-4o', }; } // Send additional metadata when streaming completes if (part.type === 'finish') { return { totalTokens: part.totalUsage.totalTokens, }; } }, }); } ``` <Note> To enable type-safe metadata return object in `messageMetadata`, pass in the `originalMessages` parameter typed to your UIMessage type. </Note> ### Accessing Metadata on the Client Access metadata through the `message.metadata` property: ```tsx filename="app/page.tsx" highlight="8,18-23" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { MyUIMessage } from '@/types'; export default function Chat() { const { messages } = useChat<MyUIMessage>({ transport: new DefaultChatTransport({ api: '/api/chat', }), }); return ( <div> {messages.map(message => ( <div key={message.id}> <div> {message.role === 'user' ? 'User: ' : 'AI: '} {message.metadata?.createdAt && ( <span className="text-sm text-gray-500"> {new Date(message.metadata.createdAt).toLocaleTimeString()} </span> )} </div> {/* Render message content */} {message.parts.map((part, index) => part.type === 'text' ? <div key={index}>{part.text}</div> : null, )} {/* Display additional metadata */} {message.metadata?.totalTokens && ( <div className="text-xs text-gray-400"> {message.metadata.totalTokens} tokens </div> )} </div> ))} </div> ); } ``` <Note> For streaming arbitrary data that changes during generation, consider using [data parts](/docs/ai-sdk-ui/streaming-data) instead. </Note> ## Common Use Cases Message metadata is ideal for: - **Timestamps**: When messages were created or completed - **Model Information**: Which AI model was used - **Token Usage**: Track costs and usage limits - **User Context**: User IDs, session information - **Performance Metrics**: Generation time, time to first token - **Quality Indicators**: Finish reason, confidence scores ## See Also - [Chatbot Guide](/docs/ai-sdk-ui/chatbot#message-metadata) - Message metadata in the context of building chatbots - [Streaming Data](/docs/ai-sdk-ui/streaming-data#message-metadata-vs-data-parts) - Comparison with data parts - [UIMessage Reference](/docs/reference/ai-sdk-core/ui-message) - Complete UIMessage type reference --- File: /ai/content/docs/04-ai-sdk-ui/50-stream-protocol.mdx --- --- title: Stream Protocols description: Learn more about the supported stream protocols in the AI SDK. --- # Stream Protocols AI SDK UI functions such as `useChat` and `useCompletion` support both text streams and data streams. The stream protocol defines how the data is streamed to the frontend on top of the HTTP protocol. This page describes both protocols and how to use them in the backend and frontend. You can use this information to develop custom backends and frontends for your use case, e.g., to provide compatible API endpoints that are implemented in a different language such as Python. For instance, here's an example using [FastAPI](https://github.com/vercel/ai/tree/main/examples/next-fastapi) as a backend. ## Text Stream Protocol A text stream contains chunks in plain text, that are streamed to the frontend. Each chunk is then appended together to form a full text response. Text streams are supported by `useChat`, `useCompletion`, and `useObject`. When you use `useChat` or `useCompletion`, you need to enable text streaming by setting the `streamProtocol` options to `text`. You can generate text streams with `streamText` in the backend. When you call `toTextStreamResponse()` on the result object, a streaming HTTP response is returned. <Note> Text streams only support basic text data. If you need to stream other types of data such as tool calls, use data streams. </Note> ### Text Stream Example Here is a Next.js example that uses the text stream protocol: ```tsx filename='app/page.tsx' 'use client'; import { useChat } from '@ai-sdk/react'; import { TextStreamChatTransport } from 'ai'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat({ transport: new TextStreamChatTransport({ api: '/api/chat' }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, i) => { switch (part.type) { case 'text': return <div key={`${message.id}-${i}`}>{part.text}</div>; } })} </div> ))} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` ```ts filename='app/api/chat/route.ts' import { streamText, UIMessage, convertToModelMessages } from 'ai'; import { openai } from '@ai-sdk/openai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; 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.toTextStreamResponse(); } ``` ## Data Stream Protocol A data stream follows a special protocol that the AI SDK provides to send information to the frontend. The data stream protocol uses Server-Sent Events (SSE) format for improved standardization, keep-alive through ping, reconnect capabilities, and better cache handling. <Note> When you provide data streams from a custom backend, you need to set the `x-vercel-ai-ui-message-stream` header to `v1`. </Note> The following stream parts are currently supported: ### Message Start Part Indicates the beginning of a new message with metadata. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"start","messageId":"..."} ``` ### Text Parts Text content is streamed using a start/delta/end pattern with unique IDs for each text block. #### Text Start Part Indicates the beginning of a text block. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"text-start","id":"msg_68679a454370819ca74c8eb3d04379630dd1afb72306ca5d"} ``` #### Text Delta Part Contains incremental text content for the text block. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"text-delta","id":"msg_68679a454370819ca74c8eb3d04379630dd1afb72306ca5d","delta":"Hello"} ``` #### Text End Part Indicates the completion of a text block. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"text-end","id":"msg_68679a454370819ca74c8eb3d04379630dd1afb72306ca5d"} ``` ### Reasoning Parts Reasoning content is streamed using a start/delta/end pattern with unique IDs for each reasoning block. #### Reasoning Start Part Indicates the beginning of a reasoning block. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"reasoning-start","id":"reasoning_123"} ``` #### Reasoning Delta Part Contains incremental reasoning content for the reasoning block. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"reasoning-delta","id":"reasoning_123","delta":"This is some reasoning"} ``` #### Reasoning End Part Indicates the completion of a reasoning block. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"reasoning-end","id":"reasoning_123"} ``` ### Source Parts Source parts provide references to external content sources. #### Source URL Part References to external URLs. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"source-url","sourceId":"https://example.com","url":"https://example.com"} ``` #### Source Document Part References to documents or files. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"source-document","sourceId":"https://example.com","mediaType":"file","title":"Title"} ``` ### File Part The file parts contain references to files with their media type. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"file","url":"https://example.com/file.png","mediaType":"image/png"} ``` ### Data Parts Custom data parts allow streaming of arbitrary structured data with type-specific handling. Format: Server-Sent Event with JSON object where the type includes a custom suffix Example: ``` data: {"type":"data-weather","data":{"location":"SF","temperature":100}} ``` The `data-*` type pattern allows you to define custom data types that your frontend can handle specifically. ### Error Part The error parts are appended to the message as they are received. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"error","errorText":"error message"} ``` ### Tool Input Start Part Indicates the beginning of tool input streaming. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"tool-input-start","toolCallId":"call_fJdQDqnXeGxTmr4E3YPSR7Ar","toolName":"getWeatherInformation"} ``` ### Tool Input Delta Part Incremental chunks of tool input as it's being generated. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"tool-input-delta","toolCallId":"call_fJdQDqnXeGxTmr4E3YPSR7Ar","inputTextDelta":"San Francisco"} ``` ### Tool Input Available Part Indicates that tool input is complete and ready for execution. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"tool-input-available","toolCallId":"call_fJdQDqnXeGxTmr4E3YPSR7Ar","toolName":"getWeatherInformation","input":{"city":"San Francisco"}} ``` ### Tool Output Available Part Contains the result of tool execution. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"tool-output-available","toolCallId":"call_fJdQDqnXeGxTmr4E3YPSR7Ar","output":{"city":"San Francisco","weather":"sunny"}} ``` ### Start Step Part A part indicating the start of a step. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"start-step"} ``` ### Finish Step Part A part indicating that a step (i.e., one LLM API call in the backend) has been completed. This part is necessary to correctly process multiple stitched assistant calls, e.g. when calling tools in the backend, and using steps in `useChat` at the same time. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"finish-step"} ``` ### Finish Message Part A part indicating the completion of a message. Format: Server-Sent Event with JSON object Example: ``` data: {"type":"finish"} ``` ### Stream Termination The stream ends with a special `[DONE]` marker. Format: Server-Sent Event with literal `[DONE]` Example: ``` data: [DONE] ``` The data stream protocol is supported by `useChat` and `useCompletion` on the frontend and used by default. `useCompletion` only supports the `text` and `data` stream parts. On the backend, you can use `toUIMessageStreamResponse()` from the `streamText` result object to return a streaming HTTP response. ### UI Message Stream Example Here is a Next.js example that uses the UI message stream protocol: ```tsx filename='app/page.tsx' 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat(); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, i) => { switch (part.type) { case 'text': return <div key={`${message.id}-${i}`}>{part.text}</div>; } })} </div> ))} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` ```ts filename='app/api/chat/route.ts' import { openai } from '@ai-sdk/openai'; import { streamText, UIMessage, convertToModelMessages } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; 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(); } ``` --- File: /ai/content/docs/04-ai-sdk-ui/index.mdx --- --- title: AI SDK UI description: Learn about the AI SDK UI. --- # AI SDK UI <IndexCards cards={[ { title: 'Overview', description: 'Get an overview about the AI SDK UI.', href: '/docs/ai-sdk-ui/overview' }, { title: 'Chatbot', description: 'Learn how to integrate an interface for a chatbot.', href: '/docs/ai-sdk-ui/chatbot' }, { title: 'Chatbot Message Persistence', description: 'Learn how to store and load chat messages in a chatbot.', href: '/docs/ai-sdk-ui/chatbot-message-persistence' }, { title: 'Chatbot Tool Usage', description: 'Learn how to integrate an interface for a chatbot with tool calling.', href: '/docs/ai-sdk-ui/chatbot-tool-usage' }, { title: 'Completion', description: 'Learn how to integrate an interface for text completion.', href: '/docs/ai-sdk-ui/completion' }, { title: 'Object Generation', description: 'Learn how to integrate an interface for object generation.', href: '/docs/ai-sdk-ui/object-generation' }, { title: 'Streaming Data', description: 'Learn how to stream data.', href: '/docs/ai-sdk-ui/streaming-data' }, { title: 'Reading UI Message Streams', description: 'Learn how to read UIMessage streams for terminal UIs, custom clients, and server components.', href: '/docs/ai-sdk-ui/reading-ui-message-streams' }, { title: 'Error Handling', description: 'Learn how to handle errors.', href: '/docs/ai-sdk-ui/error-handling' }, { title: 'Stream Protocol', description: 'The stream protocol defines how data is sent from the backend to the AI SDK UI frontend.', href: '/docs/ai-sdk-ui/stream-protocol' } ]} /> --- File: /ai/content/docs/05-ai-sdk-rsc/01-overview.mdx --- --- title: Overview description: An overview of AI SDK RSC. --- # AI SDK RSC <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> <Note> The `@ai-sdk/rsc` package is compatible with frameworks that support React Server Components. </Note> [React Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components) (RSC) allow you to write UI that can be rendered on the server and streamed to the client. RSCs enable [ Server Actions ](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#with-client-components), a new way to call server-side code directly from the client just like any other function with end-to-end type-safety. This combination opens the door to a new way of building AI applications, allowing the large language model (LLM) to generate and stream UI directly from the server to the client. ## AI SDK RSC Functions AI SDK RSC has various functions designed to help you build AI-native applications with React Server Components. These functions: 1. Provide abstractions for building Generative UI applications. - [`streamUI`](/docs/reference/ai-sdk-rsc/stream-ui): calls a model and allows it to respond with React Server Components. - [`useUIState`](/docs/reference/ai-sdk-rsc/use-ui-state): returns the current UI state and a function to update the UI State (like React's `useState`). UI State is the visual representation of the AI state. - [`useAIState`](/docs/reference/ai-sdk-rsc/use-ai-state): returns the current AI state and a function to update the AI State (like React's `useState`). The AI state is intended to contain context and information shared with the AI model, such as system messages, function responses, and other relevant data. - [`useActions`](/docs/reference/ai-sdk-rsc/use-actions): provides access to your Server Actions from the client. This is particularly useful for building interfaces that require user interactions with the server. - [`createAI`](/docs/reference/ai-sdk-rsc/create-ai): creates a client-server context provider that can be used to wrap parts of your application tree to easily manage both UI and AI states of your application. 2. Make it simple to work with streamable values between the server and client. - [`createStreamableValue`](/docs/reference/ai-sdk-rsc/create-streamable-value): creates a stream that sends values from the server to the client. The value can be any serializable data. - [`readStreamableValue`](/docs/reference/ai-sdk-rsc/read-streamable-value): reads a streamable value from the client that was originally created using `createStreamableValue`. - [`createStreamableUI`](/docs/reference/ai-sdk-rsc/create-streamable-ui): creates a stream that sends UI from the server to the client. - [`useStreamableValue`](/docs/reference/ai-sdk-rsc/use-streamable-value): accepts a streamable value created using `createStreamableValue` and returns the current value, error, and pending state. ## Templates Check out the following templates to see AI SDK RSC in action. <Templates type="generative-ui" /> ## API Reference Please check out the [AI SDK RSC API Reference](/docs/reference/ai-sdk-rsc) for more details on each function. --- File: /ai/content/docs/05-ai-sdk-rsc/02-streaming-react-components.mdx --- --- title: Streaming React Components description: Overview of streaming RSCs --- import { UIPreviewCard, Card } from '@/components/home/card'; import { EventPlanning } from '@/components/home/event-planning'; import { Searching } from '@/components/home/searching'; import { Weather } from '@/components/home/weather'; # Streaming React Components <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> The RSC API allows you to stream React components from the server to the client with the [`streamUI`](/docs/reference/ai-sdk-rsc/stream-ui) function. This is useful when you want to go beyond raw text and stream components to the client in real-time. Similar to [ AI SDK Core ](/docs/ai-sdk-core/overview) APIs (like [ `streamText` ](/docs/reference/ai-sdk-core/stream-text) and [ `streamObject` ](/docs/reference/ai-sdk-core/stream-object)), `streamUI` provides a single function to call a model and allow it to respond with React Server Components. It supports the same model interfaces as AI SDK Core APIs. ### Concepts To give the model the ability to respond to a user's prompt with a React component, you can leverage [tools](/docs/ai-sdk-core/tools-and-tool-calling). <Note> Remember, tools are like programs you can give to the model, and the model can decide as and when to use based on the context of the conversation. </Note> With the `streamUI` function, **you provide tools that return React components**. With the ability to stream components, the model is akin to a dynamic router that is able to understand the user's intention and display relevant UI. At a high level, the `streamUI` works like other AI SDK Core functions: you can provide the model with a prompt or some conversation history and, optionally, some tools. If the model decides, based on the context of the conversation, to call a tool, it will generate a tool call. The `streamUI` function will then run the respective tool, returning a React component. If the model doesn't have a relevant tool to use, it will return a text generation, which will be passed to the `text` function, for you to handle (render and return as a React component). <Note>Remember, the `streamUI` function must return a React component. </Note> ```tsx const result = await streamUI({ model: openai('gpt-4o'), prompt: 'Get the weather for San Francisco', text: ({ content }) => <div>{content}</div>, tools: {}, }); ``` This example calls the `streamUI` function using OpenAI's `gpt-4o` model, passes a prompt, specifies how the model's plain text response (`content`) should be rendered, and then provides an empty object for tools. Even though this example does not define any tools, it will stream the model's response as a `div` rather than plain text. ### Adding A Tool Using tools with `streamUI` is similar to how you use tools with `generateText` and `streamText`. A tool is an object that has: - `description`: a string telling the model what the tool does and when to use it - `parameters`: a Zod schema describing what the tool needs in order to run - `generate`: an asynchronous function that will be run if the model calls the tool. This must return a React component Let's expand the previous example to add a tool. ```tsx highlight="6-14" const result = await streamUI({ model: openai('gpt-4o'), prompt: 'Get the weather for San Francisco', text: ({ content }) => <div>{content}</div>, tools: { getWeather: { description: 'Get the weather for a location', parameters: z.object({ location: z.string() }), generate: async function* ({ location }) { yield <LoadingComponent />; const weather = await getWeather(location); return <WeatherComponent weather={weather} location={location} />; }, }, }, }); ``` This tool would be run if the user asks for the weather for their location. If the user hasn't specified a location, the model will ask for it before calling the tool. When the model calls the tool, the generate function will initially return a loading component. This component will show until the awaited call to `getWeather` is resolved, at which point, the model will stream the `<WeatherComponent />` to the user. <Note> Note: This example uses a [ generator function ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) (`function*`), which allows you to pause its execution and return a value, then resume from where it left off on the next call. This is useful for handling data streams, as you can fetch and return data from an asynchronous source like an API, then resume the function to fetch the next chunk when needed. By yielding values one at a time, generator functions enable efficient processing of streaming data without blocking the main thread. </Note> ## Using `streamUI` with Next.js Let's see how you can use the example above in a Next.js application. To use `streamUI` in a Next.js application, you will need two things: 1. A Server Action (where you will call `streamUI`) 2. A page to call the Server Action and render the resulting components ### Step 1: Create a Server Action <Note> Server Actions are server-side functions that you can call directly from the frontend. For more info, see [the documentation](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#with-client-components). </Note> Create a Server Action at `app/actions.tsx` and add the following code: ```tsx filename="app/actions.tsx" 'use server'; import { streamUI } from '@ai-sdk/rsc'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const LoadingComponent = () => ( <div className="animate-pulse p-4">getting weather...</div> ); const getWeather = async (location: string) => { await new Promise(resolve => setTimeout(resolve, 2000)); return '82°F️ ☀️'; }; interface WeatherProps { location: string; weather: string; } const WeatherComponent = (props: WeatherProps) => ( <div className="border border-neutral-200 p-4 rounded-lg max-w-fit"> The weather in {props.location} is {props.weather} </div> ); export async function streamComponent() { const result = await streamUI({ model: openai('gpt-4o'), prompt: 'Get the weather for San Francisco', text: ({ content }) => <div>{content}</div>, tools: { getWeather: { description: 'Get the weather for a location', parameters: z.object({ location: z.string(), }), generate: async function* ({ location }) { yield <LoadingComponent />; const weather = await getWeather(location); return <WeatherComponent weather={weather} location={location} />; }, }, }, }); return result.value; } ``` The `getWeather` tool should look familiar as it is identical to the example in the previous section. In order for this tool to work: 1. First define a `LoadingComponent`, which renders a pulsing `div` that will show some loading text. 2. Next, define a `getWeather` function that will timeout for 2 seconds (to simulate fetching the weather externally) before returning the "weather" for a `location`. Note: you could run any asynchronous TypeScript code here. 3. Finally, define a `WeatherComponent` which takes in `location` and `weather` as props, which are then rendered within a `div`. Your Server Action is an asynchronous function called `streamComponent` that takes no inputs, and returns a `ReactNode`. Within the action, you call the `streamUI` function, specifying the model (`gpt-4o`), the prompt, the component that should be rendered if the model chooses to return text, and finally, your `getWeather` tool. Last but not least, you return the resulting component generated by the model with `result.value`. To call this Server Action and display the resulting React Component, you will need a page. ### Step 2: Create a Page Create or update your root page (`app/page.tsx`) with the following code: ```tsx filename="app/page.tsx" 'use client'; import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { streamComponent } from './actions'; export default function Page() { const [component, setComponent] = useState<React.ReactNode>(); return ( <div> <form onSubmit={async e => { e.preventDefault(); setComponent(await streamComponent()); }} > <Button>Stream Component</Button> </form> <div>{component}</div> </div> ); } ``` This page is first marked as a client component with the `"use client";` directive given it will be using hooks and interactivity. On the page, you render a form. When that form is submitted, you call the `streamComponent` action created in the previous step (just like any other function). The `streamComponent` action returns a `ReactNode` that you can then render on the page using React state (`setComponent`). ## Going beyond a single prompt You can now allow the model to respond to your prompt with a React component. However, this example is limited to a static prompt that is set within your Server Action. You could make this example interactive by turning it into a chatbot. Learn how to stream React components with the Next.js App Router using `streamUI` with this [example](/examples/next-app/interface/route-components). --- File: /ai/content/docs/05-ai-sdk-rsc/03-generative-ui-state.mdx --- --- title: Managing Generative UI State description: Overview of the AI and UI states --- # Managing Generative UI State <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> State is an essential part of any application. State is particularly important in AI applications as it is passed to large language models (LLMs) on each request to ensure they have the necessary context to produce a great generation. Traditional chatbots are text-based and have a structure that mirrors that of any chat application. For example, in a chatbot, state is an array of `messages` where each `message` has: - `id`: a unique identifier - `role`: who sent the message (user/assistant/system/tool) - `content`: the content of the message This state can be rendered in the UI and sent to the model without any modifications. With Generative UI, the model can now return a React component, rather than a plain text message. The client can render that component without issue, but that state can't be sent back to the model because React components aren't serialisable. So, what can you do? **The solution is to split the state in two, where one (AI State) becomes a proxy for the other (UI State)**. One way to understand this concept is through a Lego analogy. Imagine a 10,000 piece Lego model that, once built, cannot be easily transported because it is fragile. By taking the model apart, it can be easily transported, and then rebuilt following the steps outlined in the instructions pamphlet. In this way, the instructions pamphlet is a proxy to the physical structure. Similarly, AI State provides a serialisable (JSON) representation of your UI that can be passed back and forth to the model. ## What is AI and UI State? The RSC API simplifies how you manage AI State and UI State, providing a robust way to keep them in sync between your database, server and client. ### AI State AI State refers to the state of your application in a serialisable format that will be used on the server and can be shared with the language model. For a chat app, the AI State is the conversation history (messages) between the user and the assistant. Components generated by the model would be represented in a JSON format as a tool alongside any necessary props. AI State can also be used to store other values and meta information such as `createdAt` for each message and `chatId` for each conversation. The LLM reads this history so it can generate the next message. This state serves as the source of truth for the current application state. <Note> **Note**: AI state can be accessed/modified from both the server and the client. </Note> ### UI State UI State refers to the state of your application that is rendered on the client. It is a fully client-side state (similar to `useState`) that can store anything from Javascript values to React elements. UI state is a list of actual UI elements that are rendered on the client. <Note>**Note**: UI State can only be accessed client-side.</Note> ## Using AI / UI State ### Creating the AI Context AI SDK RSC simplifies managing AI and UI state across your application by providing several hooks. These hooks are powered by [ React context ](https://react.dev/reference/react/hooks#context-hooks) under the hood. Notably, this means you do not have to pass the message history to the server explicitly for each request. You also can access and update your application state in any child component of the context provider. As you begin building [multistep generative interfaces](/docs/ai-sdk-rsc/multistep-interfaces), this will be particularly helpful. To use `@ai-sdk/rsc` to manage AI and UI State in your application, you can create a React context using [`createAI`](/docs/reference/ai-sdk-rsc/create-ai): ```tsx filename='app/actions.tsx' // Define the AI state and UI state types export type ServerMessage = { role: 'user' | 'assistant'; content: string; }; export type ClientMessage = { id: string; role: 'user' | 'assistant'; display: ReactNode; }; export const sendMessage = async (input: string): Promise<ClientMessage> => { "use server" ... } ``` ```tsx filename='app/ai.ts' import { createAI } from '@ai-sdk/rsc'; import { ClientMessage, ServerMessage, sendMessage } from './actions'; export type AIState = ServerMessage[]; export type UIState = ClientMessage[]; // Create the AI provider with the initial states and allowed actions export const AI = createAI<AIState, UIState>({ initialAIState: [], initialUIState: [], actions: { sendMessage, }, }); ``` <Note>You must pass Server Actions to the `actions` object.</Note> In this example, you define types for AI State and UI State, respectively. Next, wrap your application with your newly created context. With that, you can get and set AI and UI State across your entire application. ```tsx filename='app/layout.tsx' import { type ReactNode } from 'react'; import { AI } from './ai'; export default function RootLayout({ children, }: Readonly<{ children: ReactNode }>) { return ( <AI> <html lang="en"> <body>{children}</body> </html> </AI> ); } ``` ## Reading UI State in Client The UI state can be accessed in Client Components using the [`useUIState`](/docs/reference/ai-sdk-rsc/use-ui-state) hook provided by the RSC API. The hook returns the current UI state and a function to update the UI state like React's `useState`. ```tsx filename='app/page.tsx' 'use client'; import { useUIState } from '@ai-sdk/rsc'; export default function Page() { const [messages, setMessages] = useUIState(); return ( <ul> {messages.map(message => ( <li key={message.id}>{message.display}</li> ))} </ul> ); } ``` ## Reading AI State in Client The AI state can be accessed in Client Components using the [`useAIState`](/docs/reference/ai-sdk-rsc/use-ai-state) hook provided by the RSC API. The hook returns the current AI state. ```tsx filename='app/page.tsx' 'use client'; import { useAIState } from '@ai-sdk/rsc'; export default function Page() { const [messages, setMessages] = useAIState(); return ( <ul> {messages.map(message => ( <li key={message.id}>{message.content}</li> ))} </ul> ); } ``` ## Reading AI State on Server The AI State can be accessed within any Server Action provided to the `createAI` context using the [`getAIState`](/docs/reference/ai-sdk-rsc/get-ai-state) function. It returns the current AI state as a read-only value: ```tsx filename='app/actions.ts' import { getAIState } from '@ai-sdk/rsc'; export async function sendMessage(message: string) { 'use server'; const history = getAIState(); const response = await generateText({ model: openai('gpt-3.5-turbo'), messages: [...history, { role: 'user', content: message }], }); return response; } ``` <Note> Remember, you can only access state within actions that have been passed to the `createAI` context within the `actions` key. </Note> ## Updating AI State on Server The AI State can also be updated from within your Server Action with the [`getMutableAIState`](/docs/reference/ai-sdk-rsc/get-mutable-ai-state) function. This function is similar to `getAIState`, but it returns the state with methods to read and update it: ```tsx filename='app/actions.ts' import { getMutableAIState } from '@ai-sdk/rsc'; export async function sendMessage(message: string) { 'use server'; const history = getMutableAIState(); // Update the AI state with the new user message. history.update([...history.get(), { role: 'user', content: message }]); const response = await generateText({ model: openai('gpt-3.5-turbo'), messages: history.get(), }); // Update the AI state again with the response from the model. history.done([...history.get(), { role: 'assistant', content: response }]); return response; } ``` <Note> It is important to update the AI State with new responses using `.update()` and `.done()` to keep the conversation history in sync. </Note> ## Calling Server Actions from the Client To call the `sendMessage` action from the client, you can use the [`useActions`](/docs/reference/ai-sdk-rsc/use-actions) hook. The hook returns all the available Actions that were provided to `createAI`: ```tsx filename='app/page.tsx' 'use client'; import { useActions, useUIState } from '@ai-sdk/rsc'; import { AI } from './ai'; export default function Page() { const { sendMessage } = useActions<typeof AI>(); const [messages, setMessages] = useUIState(); const handleSubmit = async event => { event.preventDefault(); setMessages([ ...messages, { id: Date.now(), role: 'user', display: event.target.message.value }, ]); const response = await sendMessage(event.target.message.value); setMessages([ ...messages, { id: Date.now(), role: 'assistant', display: response }, ]); }; return ( <> <ul> {messages.map(message => ( <li key={message.id}>{message.display}</li> ))} </ul> <form onSubmit={handleSubmit}> <input type="text" name="message" /> <button type="submit">Send</button> </form> </> ); } ``` When the user submits a message, the `sendMessage` action is called with the message content. The response from the action is then added to the UI state, updating the displayed messages. <Note> Important! Don't forget to update the UI State after you call your Server Action otherwise the streamed component will not show in the UI. </Note> To learn more, check out this [example](/examples/next-app/state-management/ai-ui-states) on managing AI and UI state using `@ai-sdk/rsc`. --- Next, you will learn how you can save and restore state with `@ai-sdk/rsc`. --- File: /ai/content/docs/05-ai-sdk-rsc/03-saving-and-restoring-states.mdx --- --- title: Saving and Restoring States description: Saving and restoring AI and UI states with onGetUIState and onSetAIState --- # Saving and Restoring States <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> AI SDK RSC provides convenient methods for saving and restoring AI and UI state. This is useful for saving the state of your application after every model generation, and restoring it when the user revisits the generations. ## AI State ### Saving AI state The AI state can be saved using the [`onSetAIState`](/docs/reference/ai-sdk-rsc/create-ai#on-set-ai-state) callback, which gets called whenever the AI state is updated. In the following example, you save the chat history to a database whenever the generation is marked as done. ```tsx filename='app/ai.ts' export const AI = createAI<ServerMessage[], ClientMessage[]>({ actions: { continueConversation, }, onSetAIState: async ({ state, done }) => { 'use server'; if (done) { saveChatToDB(state); } }, }); ``` ### Restoring AI state The AI state can be restored using the [`initialAIState`](/docs/reference/ai-sdk-rsc/create-ai#initial-ai-state) prop passed to the context provider created by the [`createAI`](/docs/reference/ai-sdk-rsc/create-ai) function. In the following example, you restore the chat history from a database when the component is mounted. ```tsx file='app/layout.tsx' import { ReactNode } from 'react'; import { AI } from './ai'; export default async function RootLayout({ children, }: Readonly<{ children: ReactNode }>) { const chat = await loadChatFromDB(); return ( <html lang="en"> <body> <AI initialAIState={chat}>{children}</AI> </body> </html> ); } ``` ## UI State ### Saving UI state The UI state cannot be saved directly, since the contents aren't yet serializable. Instead, you can use the AI state as proxy to store details about the UI state and use it to restore the UI state when needed. ### Restoring UI state The UI state can be restored using the AI state as a proxy. In the following example, you restore the chat history from the AI state when the component is mounted. You use the [`onGetUIState`](/docs/reference/ai-sdk-rsc/create-ai#on-get-ui-state) callback to listen for SSR events and restore the UI state. ```tsx filename='app/ai.ts' export const AI = createAI<ServerMessage[], ClientMessage[]>({ actions: { continueConversation, }, onGetUIState: async () => { 'use server'; const historyFromDB: ServerMessage[] = await loadChatFromDB(); const historyFromApp: ServerMessage[] = getAIState(); // If the history from the database is different from the // history in the app, they're not in sync so return the UIState // based on the history from the database if (historyFromDB.length !== historyFromApp.length) { return historyFromDB.map(({ role, content }) => ({ id: generateId(), role, display: role === 'function' ? ( <Component {...JSON.parse(content)} /> ) : ( content ), })); } }, }); ``` To learn more, check out this [example](/examples/next-app/state-management/save-and-restore-states) that persists and restores states in your Next.js application. --- Next, you will learn how you can use `@ai-sdk/rsc` functions like `useActions` and `useUIState` to create interactive, multistep interfaces. --- File: /ai/content/docs/05-ai-sdk-rsc/04-multistep-interfaces.mdx --- --- title: Multistep Interfaces description: Overview of Building Multistep Interfaces with AI SDK RSC --- # Designing Multistep Interfaces <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> Multistep interfaces refer to user interfaces that require multiple independent steps to be executed in order to complete a specific task. For example, if you wanted to build a Generative UI chatbot capable of booking flights, it could have three steps: - Search all flights - Pick flight - Check availability To build this kind of application you will leverage two concepts, **tool composition** and **application context**. **Tool composition** is the process of combining multiple [tools](/docs/ai-sdk-core/tools-and-tool-calling) to create a new tool. This is a powerful concept that allows you to break down complex tasks into smaller, more manageable steps. In the example above, _"search all flights"_, _"pick flight"_, and _"check availability"_ come together to create a holistic _"book flight"_ tool. **Application context** refers to the state of the application at any given point in time. This includes the user's input, the output of the language model, and any other relevant information. In the example above, the flight selected in _"pick flight"_ would be used as context necessary to complete the _"check availability"_ task. ## Overview In order to build a multistep interface with `@ai-sdk/rsc`, you will need a few things: - A Server Action that calls and returns the result from the `streamUI` function - Tool(s) (sub-tasks necessary to complete your overall task) - React component(s) that should be rendered when the tool is called - A page to render your chatbot The general flow that you will follow is: - User sends a message (calls your Server Action with `useActions`, passing the message as an input) - Message is appended to the AI State and then passed to the model alongside a number of tools - Model can decide to call a tool, which will render the `<SomeTool />` component - Within that component, you can add interactivity by using `useActions` to call the model with your Server Action and `useUIState` to append the model's response (`<SomeOtherTool />`) to the UI State - And so on... ## Implementation The turn-by-turn implementation is the simplest form of multistep interfaces. In this implementation, the user and the model take turns during the conversation. For every user input, the model generates a response, and the conversation continues in this turn-by-turn fashion. In the following example, you specify two tools (`searchFlights` and `lookupFlight`) that the model can use to search for flights and lookup details for a specific flight. ```tsx filename="app/actions.tsx" import { streamUI } from '@ai-sdk/rsc'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const searchFlights = async ( source: string, destination: string, date: string, ) => { return [ { id: '1', flightNumber: 'AA123', }, { id: '2', flightNumber: 'AA456', }, ]; }; const lookupFlight = async (flightNumber: string) => { return { flightNumber: flightNumber, departureTime: '10:00 AM', arrivalTime: '12:00 PM', }; }; export async function submitUserMessage(input: string) { 'use server'; const ui = await streamUI({ model: openai('gpt-4o'), system: 'you are a flight booking assistant', prompt: input, text: async ({ content }) => <div>{content}</div>, tools: { searchFlights: { description: 'search for flights', parameters: z.object({ source: z.string().describe('The origin of the flight'), destination: z.string().describe('The destination of the flight'), date: z.string().describe('The date of the flight'), }), generate: async function* ({ source, destination, date }) { yield `Searching for flights from ${source} to ${destination} on ${date}...`; const results = await searchFlights(source, destination, date); return ( <div> {results.map(result => ( <div key={result.id}> <div>{result.flightNumber}</div> </div> ))} </div> ); }, }, lookupFlight: { description: 'lookup details for a flight', parameters: z.object({ flightNumber: z.string().describe('The flight number'), }), generate: async function* ({ flightNumber }) { yield `Looking up details for flight ${flightNumber}...`; const details = await lookupFlight(flightNumber); return ( <div> <div>Flight Number: {details.flightNumber}</div> <div>Departure Time: {details.departureTime}</div> <div>Arrival Time: {details.arrivalTime}</div> </div> ); }, }, }, }); return ui.value; } ``` Next, create an AI context that will hold the UI State and AI State. ```ts filename='app/ai.ts' import { createAI } from '@ai-sdk/rsc'; import { submitUserMessage } from './actions'; export const AI = createAI<any[], React.ReactNode[]>({ initialUIState: [], initialAIState: [], actions: { submitUserMessage, }, }); ``` Next, wrap your application with your newly created context. ```tsx filename='app/layout.tsx' import { type ReactNode } from 'react'; import { AI } from './ai'; export default function RootLayout({ children, }: Readonly<{ children: ReactNode }>) { return ( <AI> <html lang="en"> <body>{children}</body> </html> </AI> ); } ``` To call your Server Action, update your root page with the following: ```tsx filename="app/page.tsx" 'use client'; import { useState } from 'react'; import { AI } from './ai'; import { useActions, useUIState } from '@ai-sdk/rsc'; export default function Page() { const [input, setInput] = useState<string>(''); const [conversation, setConversation] = useUIState<typeof AI>(); const { submitUserMessage } = useActions(); const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); setInput(''); setConversation(currentConversation => [ ...currentConversation, <div>{input}</div>, ]); const message = await submitUserMessage(input); setConversation(currentConversation => [...currentConversation, message]); }; return ( <div> <div> {conversation.map((message, i) => ( <div key={i}>{message}</div> ))} </div> <div> <form onSubmit={handleSubmit}> <input type="text" value={input} onChange={e => setInput(e.target.value)} /> <button>Send Message</button> </form> </div> </div> ); } ``` This page pulls in the current UI State using the `useUIState` hook, which is then mapped over and rendered in the UI. To access the Server Action, you use the `useActions` hook which will return all actions that were passed to the `actions` key of the `createAI` function in your `actions.tsx` file. Finally, you call the `submitUserMessage` function like any other TypeScript function. This function returns a React component (`message`) that is then rendered in the UI by updating the UI State with `setConversation`. In this example, to call the next tool, the user must respond with plain text. **Given you are streaming a React component, you can add a button to trigger the next step in the conversation**. To add user interaction, you will have to convert the component into a client component and use the `useAction` hook to trigger the next step in the conversation. ```tsx filename="components/flights.tsx" 'use client'; import { useActions, useUIState } from '@ai-sdk/rsc'; import { ReactNode } from 'react'; interface FlightsProps { flights: { id: string; flightNumber: string }[]; } export const Flights = ({ flights }: FlightsProps) => { const { submitUserMessage } = useActions(); const [_, setMessages] = useUIState(); return ( <div> {flights.map(result => ( <div key={result.id}> <div onClick={async () => { const display = await submitUserMessage( `lookupFlight ${result.flightNumber}`, ); setMessages((messages: ReactNode[]) => [...messages, display]); }} > {result.flightNumber} </div> </div> ))} </div> ); }; ``` Now, update your `searchFlights` tool to render the new `<Flights />` component. ```tsx filename="actions.tsx" ... searchFlights: { description: 'search for flights', parameters: z.object({ source: z.string().describe('The origin of the flight'), destination: z.string().describe('The destination of the flight'), date: z.string().describe('The date of the flight'), }), generate: async function* ({ source, destination, date }) { yield `Searching for flights from ${source} to ${destination} on ${date}...`; const results = await searchFlights(source, destination, date); return (<Flights flights={results} />); }, } ... ``` In the above example, the `Flights` component is used to display the search results. When the user clicks on a flight number, the `lookupFlight` tool is called with the flight number as a parameter. The `submitUserMessage` action is then called to trigger the next step in the conversation. Learn more about tool calling in Next.js App Router by checking out examples [here](/examples/next-app/tools). --- File: /ai/content/docs/05-ai-sdk-rsc/05-streaming-values.mdx --- --- title: Streaming Values description: Overview of streaming RSCs --- import { UIPreviewCard, Card } from '@/components/home/card'; import { EventPlanning } from '@/components/home/event-planning'; import { Searching } from '@/components/home/searching'; import { Weather } from '@/components/home/weather'; # Streaming Values <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> The RSC API provides several utility functions to allow you to stream values from the server to the client. This is useful when you need more granular control over what you are streaming and how you are streaming it. <Note> These utilities can also be paired with [AI SDK Core](/docs/ai-sdk-core) functions like [`streamText`](/docs/reference/ai-sdk-core/stream-text) and [`streamObject`](/docs/reference/ai-sdk-core/stream-object) to easily stream LLM generations from the server to the client. </Note> There are two functions provided by the RSC API that allow you to create streamable values: - [`createStreamableValue`](/docs/reference/ai-sdk-rsc/create-streamable-value) - creates a streamable (serializable) value, with full control over how you create, update, and close the stream. - [`createStreamableUI`](/docs/reference/ai-sdk-rsc/create-streamable-ui) - creates a streamable React component, with full control over how you create, update, and close the stream. ## `createStreamableValue` The RSC API allows you to stream serializable Javascript values from the server to the client using [`createStreamableValue`](/docs/reference/ai-sdk-rsc/create-streamable-value), such as strings, numbers, objects, and arrays. This is useful when you want to stream: - Text generations from the language model in real-time. - Buffer values of image and audio generations from multi-modal models. - Progress updates from multi-step agent runs. ## Creating a Streamable Value You can import `createStreamableValue` from `@ai-sdk/rsc` and use it to create a streamable value. ```tsx file='app/actions.ts' 'use server'; import { createStreamableValue } from '@ai-sdk/rsc'; export const runThread = async () => { const streamableStatus = createStreamableValue('thread.init'); setTimeout(() => { streamableStatus.update('thread.run.create'); streamableStatus.update('thread.run.update'); streamableStatus.update('thread.run.end'); streamableStatus.done('thread.end'); }, 1000); return { status: streamableStatus.value, }; }; ``` ## Reading a Streamable Value You can read streamable values on the client using `readStreamableValue`. It returns an async iterator that yields the value of the streamable as it is updated: ```tsx file='app/page.tsx' import { readStreamableValue } from '@ai-sdk/rsc'; import { runThread } from '@/actions'; export default function Page() { return ( <button onClick={async () => { const { status } = await runThread(); for await (const value of readStreamableValue(status)) { console.log(value); } }} > Ask </button> ); } ``` Learn how to stream a text generation (with `streamText`) using the Next.js App Router and `createStreamableValue` in this [example](/examples/next-app/basics/streaming-text-generation). ## `createStreamableUI` `createStreamableUI` creates a stream that holds a React component. Unlike AI SDK Core APIs, this function does not call a large language model. Instead, it provides a primitive that can be used to have granular control over streaming a React component. ## Using `createStreamableUI` Let's look at how you can use the `createStreamableUI` function with a Server Action. ```tsx filename='app/actions.tsx' 'use server'; import { createStreamableUI } from '@ai-sdk/rsc'; export async function getWeather() { const weatherUI = createStreamableUI(); weatherUI.update(<div style={{ color: 'gray' }}>Loading...</div>); setTimeout(() => { weatherUI.done(<div>It&apos;s a sunny day!</div>); }, 1000); return weatherUI.value; } ``` First, you create a streamable UI with an empty state and then update it with a loading message. After 1 second, you mark the stream as done passing in the actual weather information as its final value. The `.value` property contains the actual UI that can be sent to the client. ## Reading a Streamable UI On the client side, you can call the `getWeather` Server Action and render the returned UI like any other React component. ```tsx filename='app/page.tsx' 'use client'; import { useState } from 'react'; import { readStreamableValue } from '@ai-sdk/rsc'; import { getWeather } from '@/actions'; export default function Page() { const [weather, setWeather] = useState<React.ReactNode | null>(null); return ( <div> <button onClick={async () => { const weatherUI = await getWeather(); setWeather(weatherUI); }} > What&apos;s the weather? </button> {weather} </div> ); } ``` When the button is clicked, the `getWeather` function is called, and the returned UI is set to the `weather` state and rendered on the page. Users will see the loading message first and then the actual weather information after 1 second. Learn more about handling multiple streams in a single request in the [Multiple Streamables](/docs/advanced/multiple-streamables) guide. Learn more about handling state for more complex use cases with [ AI/UI State ](/docs/ai-sdk-rsc/generative-ui-state). --- File: /ai/content/docs/05-ai-sdk-rsc/06-loading-state.mdx --- --- title: Handling Loading State description: Overview of handling loading state with AI SDK RSC --- # Handling Loading State <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> Given that responses from language models can often take a while to complete, it's crucial to be able to show loading state to users. This provides visual feedback that the system is working on their request and helps maintain a positive user experience. There are three approaches you can take to handle loading state with the AI SDK RSC: - Managing loading state similar to how you would in a traditional Next.js application. This involves setting a loading state variable in the client and updating it when the response is received. - Streaming loading state from the server to the client. This approach allows you to track loading state on a more granular level and provide more detailed feedback to the user. - Streaming loading component from the server to the client. This approach allows you to stream a React Server Component to the client while awaiting the model's response. ## Handling Loading State on the Client ### Client Let's create a simple Next.js page that will call the `generateResponse` function when the form is submitted. The function will take in the user's prompt (`input`) and then generate a response (`response`). To handle the loading state, use the `loading` state variable. When the form is submitted, set `loading` to `true`, and when the response is received, set it back to `false`. While the response is being streamed, the input field will be disabled. ```tsx filename='app/page.tsx' 'use client'; import { useState } from 'react'; import { generateResponse } from './actions'; import { readStreamableValue } from '@ai-sdk/rsc'; // Force the page to be dynamic and allow streaming responses up to 30 seconds export const maxDuration = 30; export default function Home() { const [input, setInput] = useState<string>(''); const [generation, setGeneration] = useState<string>(''); const [loading, setLoading] = useState<boolean>(false); return ( <div> <div>{generation}</div> <form onSubmit={async e => { e.preventDefault(); setLoading(true); const response = await generateResponse(input); let textContent = ''; for await (const delta of readStreamableValue(response)) { textContent = `${textContent}${delta}`; setGeneration(textContent); } setInput(''); setLoading(false); }} > <input type="text" value={input} disabled={loading} className="disabled:opacity-50" onChange={event => { setInput(event.target.value); }} /> <button>Send Message</button> </form> </div> ); } ``` ### Server Now let's implement the `generateResponse` function. Use the `streamText` function to generate a response to the input. ```typescript filename='app/actions.ts' 'use server'; import { streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; import { createStreamableValue } from '@ai-sdk/rsc'; export async function generateResponse(prompt: string) { const stream = createStreamableValue(); (async () => { const { textStream } = streamText({ model: openai('gpt-4o'), prompt, }); for await (const text of textStream) { stream.update(text); } stream.done(); })(); return stream.value; } ``` ## Streaming Loading State from the Server If you are looking to track loading state on a more granular level, you can create a new streamable value to store a custom variable and then read this on the frontend. Let's update the example to create a new streamable value for tracking loading state: ### Server ```typescript filename='app/actions.ts' highlight='9,22,25' 'use server'; import { streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; import { createStreamableValue } from '@ai-sdk/rsc'; export async function generateResponse(prompt: string) { const stream = createStreamableValue(); const loadingState = createStreamableValue({ loading: true }); (async () => { const { textStream } = streamText({ model: openai('gpt-4o'), prompt, }); for await (const text of textStream) { stream.update(text); } stream.done(); loadingState.done({ loading: false }); })(); return { response: stream.value, loadingState: loadingState.value }; } ``` ### Client ```tsx filename='app/page.tsx' highlight="22,30-34" 'use client'; import { useState } from 'react'; import { generateResponse } from './actions'; import { readStreamableValue } from '@ai-sdk/rsc'; // Force the page to be dynamic and allow streaming responses up to 30 seconds export const maxDuration = 30; export default function Home() { const [input, setInput] = useState<string>(''); const [generation, setGeneration] = useState<string>(''); const [loading, setLoading] = useState<boolean>(false); return ( <div> <div>{generation}</div> <form onSubmit={async e => { e.preventDefault(); setLoading(true); const { response, loadingState } = await generateResponse(input); let textContent = ''; for await (const responseDelta of readStreamableValue(response)) { textContent = `${textContent}${responseDelta}`; setGeneration(textContent); } for await (const loadingDelta of readStreamableValue(loadingState)) { if (loadingDelta) { setLoading(loadingDelta.loading); } } setInput(''); setLoading(false); }} > <input type="text" value={input} disabled={loading} className="disabled:opacity-50" onChange={event => { setInput(event.target.value); }} /> <button>Send Message</button> </form> </div> ); } ``` This allows you to provide more detailed feedback about the generation process to your users. ## Streaming Loading Components with `streamUI` If you are using the [ `streamUI` ](/docs/reference/ai-sdk-rsc/stream-ui) function, you can stream the loading state to the client in the form of a React component. `streamUI` supports the usage of [ JavaScript generator functions ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*), which allow you to yield some value (in this case a React component) while some other blocking work completes. ## Server ```ts 'use server'; import { openai } from '@ai-sdk/openai'; import { streamUI } from '@ai-sdk/rsc'; export async function generateResponse(prompt: string) { const result = await streamUI({ model: openai('gpt-4o'), prompt, text: async function* ({ content }) { yield <div>loading...</div>; return <div>{content}</div>; }, }); return result.value; } ``` <Note> Remember to update the file from `.ts` to `.tsx` because you are defining a React component in the `streamUI` function. </Note> ## Client ```tsx 'use client'; import { useState } from 'react'; import { generateResponse } from './actions'; import { readStreamableValue } from '@ai-sdk/rsc'; // Force the page to be dynamic and allow streaming responses up to 30 seconds export const maxDuration = 30; export default function Home() { const [input, setInput] = useState<string>(''); const [generation, setGeneration] = useState<React.ReactNode>(); return ( <div> <div>{generation}</div> <form onSubmit={async e => { e.preventDefault(); const result = await generateResponse(input); setGeneration(result); setInput(''); }} > <input type="text" value={input} onChange={event => { setInput(event.target.value); }} /> <button>Send Message</button> </form> </div> ); } ``` --- File: /ai/content/docs/05-ai-sdk-rsc/08-error-handling.mdx --- --- title: Error Handling description: Learn how to handle errors with the AI SDK. --- # Error Handling <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> Two categories of errors can occur when working with the RSC API: errors while streaming user interfaces and errors while streaming other values. ## Handling UI Errors To handle errors while generating UI, the [`streamableUI`](/docs/reference/ai-sdk-rsc/create-streamable-ui) object exposes an `error()` method. ```tsx filename='app/actions.tsx' 'use server'; import { createStreamableUI } from '@ai-sdk/rsc'; export async function getStreamedUI() { const ui = createStreamableUI(); (async () => { ui.update(<div>loading</div>); const data = await fetchData(); ui.done(<div>{data}</div>); })().catch(e => { ui.error(<div>Error: {e.message}</div>); }); return ui.value; } ``` With this method, you can catch any error with the stream, and return relevant UI. On the client, you can also use a [React Error Boundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) to wrap the streamed component and catch any additional errors. ```tsx filename='app/page.tsx' import { getStreamedUI } from '@/actions'; import { useState } from 'react'; import { ErrorBoundary } from './ErrorBoundary'; export default function Page() { const [streamedUI, setStreamedUI] = useState(null); return ( <div> <button onClick={async () => { const newUI = await getStreamedUI(); setStreamedUI(newUI); }} > What does the new UI look like? </button> <ErrorBoundary>{streamedUI}</ErrorBoundary> </div> ); } ``` ## Handling Other Errors To handle other errors while streaming, you can return an error object that the receiver can use to determine why the failure occurred. ```tsx filename='app/actions.tsx' 'use server'; import { createStreamableValue } from '@ai-sdk/rsc'; import { fetchData, emptyData } from '../utils/data'; export const getStreamedData = async () => { const streamableData = createStreamableValue<string>(emptyData); try { (() => { const data1 = await fetchData(); streamableData.update(data1); const data2 = await fetchData(); streamableData.update(data2); const data3 = await fetchData(); streamableData.done(data3); })(); return { data: streamableData.value }; } catch (e) { return { error: e.message }; } }; ``` --- File: /ai/content/docs/05-ai-sdk-rsc/09-authentication.mdx --- --- title: Handling Authentication description: Learn how to authenticate with the AI SDK. --- # Authentication <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> The RSC API makes extensive use of [`Server Actions`](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations) to power streaming values and UI from the server. Server Actions are exposed as public, unprotected endpoints. As a result, you should treat Server Actions as you would public-facing API endpoints and ensure that the user is authorized to perform the action before returning any data. ```tsx filename="app/actions.tsx" 'use server'; import { cookies } from 'next/headers'; import { createStremableUI } from '@ai-sdk/rsc'; import { validateToken } from '../utils/auth'; export const getWeather = async () => { const token = cookies().get('token'); if (!token || !validateToken(token)) { return { error: 'This action requires authentication', }; } const streamableDisplay = createStreamableUI(null); streamableDisplay.update(<Skeleton />); streamableDisplay.done(<Weather />); return { display: streamableDisplay.value, }; }; ``` --- File: /ai/content/docs/05-ai-sdk-rsc/10-migrating-to-ui.mdx --- --- title: Migrating from RSC to UI description: Learn how to migrate from AI SDK RSC to AI SDK UI. --- # Migrating from RSC to UI This guide helps you migrate from AI SDK RSC to AI SDK UI. ## Background The AI SDK has two packages that help you build the frontend for your applications – [AI SDK UI](/docs/ai-sdk-ui) and [AI SDK RSC](/docs/ai-sdk-rsc). We introduced support for using [React Server Components](https://react.dev/reference/rsc/server-components) (RSC) within the AI SDK to simplify building generative user interfaces for frameworks that support RSC. However, given we're pushing the boundaries of this technology, AI SDK RSC currently faces significant limitations that make it unsuitable for stable production use. - It is not possible to abort a stream using server actions. This will be improved in future releases of React and Next.js [(1122)](https://github.com/vercel/ai/issues/1122). - When using `createStreamableUI` and `streamUI`, components remount on `.done()`, causing them to flicker [(2939)](https://github.com/vercel/ai/issues/2939). - Many suspense boundaries can lead to crashes [(2843)](https://github.com/vercel/ai/issues/2843). - Using `createStreamableUI` can lead to quadratic data transfer. You can avoid this using createStreamableValue instead, and rendering the component client-side. - Closed RSC streams cause update issues [(3007)](https://github.com/vercel/ai/issues/3007). Due to these limitations, AI SDK RSC is marked as experimental, and we do not recommend using it for stable production environments. As a result, we strongly recommend migrating to AI SDK UI, which has undergone extensive development to provide a more stable and production grade experience. In building [v0](https://v0.dev), we have invested considerable time exploring how to create the best chat experience on the web. AI SDK UI ships with many of these best practices and commonly used patterns like [language model middleware](/docs/ai-sdk-core/middleware), [multi-step tool calls](/docs/ai-sdk-core/tools-and-tool-calling#multi-step-calls), [attachments](/docs/ai-sdk-ui/chatbot#attachments-experimental), [telemetry](/docs/ai-sdk-core/telemetry), [provider registry](/docs/ai-sdk-core/provider-management#provider-registry), and many more. These features have been considerately designed into a neat abstraction that you can use to reliably integrate AI into your applications. ## Streaming Chat Completions ### Basic Setup The `streamUI` function executes as part of a server action as illustrated below. #### Before: Handle generation and rendering in a single server action ```tsx filename="@/app/actions.tsx" import { openai } from '@ai-sdk/openai'; import { getMutableAIState, streamUI } from '@ai-sdk/rsc'; export async function sendMessage(message: string) { 'use server'; const messages = getMutableAIState('messages'); messages.update([...messages.get(), { role: 'user', content: message }]); const { value: stream } = await streamUI({ model: openai('gpt-4o'), system: 'you are a friendly assistant!', messages: messages.get(), text: async function* ({ content, done }) { // process text }, tools: { // tool definitions }, }); return stream; } ``` #### Before: Call server action and update UI state The chat interface calls the server action. The response is then saved using the `useUIState` hook. ```tsx filename="@/app/page.tsx" 'use client'; import { useState, ReactNode } from 'react'; import { useActions, useUIState } from '@ai-sdk/rsc'; export default function Page() { const { sendMessage } = useActions(); const [input, setInput] = useState(''); const [messages, setMessages] = useUIState(); return ( <div> {messages.map(message => message)} <form onSubmit={async () => { const response: ReactNode = await sendMessage(input); setMessages(msgs => [...msgs, response]); }} > <input type="text" /> <button type="submit">Submit</button> </form> </div> ); } ``` The `streamUI` function combines generating text and rendering the user interface. To migrate to AI SDK UI, you need to **separate these concerns** – streaming generations with `streamText` and rendering the UI with `useChat`. #### After: Replace server action with route handler The `streamText` function executes as part of a route handler and streams the response to the client. The `useChat` hook on the client decodes this stream and renders the response within the chat interface. ```ts filename="@/app/api/chat/route.ts" import { streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; export async function POST(request) { const { messages } = await request.json(); const result = streamText({ model: openai('gpt-4o'), system: 'you are a friendly assistant!', messages, tools: { // tool definitions }, }); return result.toUIMessageStreamResponse(); } ``` #### After: Update client to use chat hook ```tsx filename="@/app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; export default function Page() { const { messages, input, setInput, handleSubmit } = useChat(); return ( <div> {messages.map(message => ( <div key={message.id}> <div>{message.role}</div> <div>{message.content}</div> </div> ))} <form onSubmit={handleSubmit}> <input type="text" value={input} onChange={event => { setInput(event.target.value); }} /> <button type="submit">Send</button> </form> </div> ); } ``` ### Parallel Tool Calls In AI SDK RSC, `streamUI` does not support parallel tool calls. You will have to use a combination of `streamText`, `createStreamableUI` and `createStreamableValue`. With AI SDK UI, `useChat` comes with built-in support for parallel tool calls. You can define multiple tools in the `streamText` and have them called them in parallel. The `useChat` hook will then handle the parallel tool calls for you automatically. ### Multi-Step Tool Calls In AI SDK RSC, `streamUI` does not support multi-step tool calls. You will have to use a combination of `streamText`, `createStreamableUI` and `createStreamableValue`. With AI SDK UI, `useChat` comes with built-in support for multi-step tool calls. You can set `maxSteps` in the `streamText` function to define the number of steps the language model can make in a single call. The `useChat` hook will then handle the multi-step tool calls for you automatically. ### Generative User Interfaces The `streamUI` function uses `tools` as a way to execute functions based on user input and renders React components based on the function output to go beyond text in the chat interface. #### Before: Render components within the server action and stream to client ```tsx filename="@/app/actions.tsx" import { z } from 'zod'; import { streamUI } from '@ai-sdk/rsc'; import { openai } from '@ai-sdk/openai'; import { getWeather } from '@/utils/queries'; import { Weather } from '@/components/weather'; const { value: stream } = await streamUI({ model: openai('gpt-4o'), system: 'you are a friendly assistant!', messages, text: async function* ({ content, done }) { // process text }, tools: { displayWeather: { description: 'Display the weather for a location', parameters: z.object({ latitude: z.number(), longitude: z.number(), }), generate: async function* ({ latitude, longitude }) { yield <div>Loading weather...</div>; const { value, unit } = await getWeather({ latitude, longitude }); return <Weather value={value} unit={unit} />; }, }, }, }); ``` As mentioned earlier, `streamUI` generates text and renders the React component in a single server action call. #### After: Replace with route handler and stream props data to client The `streamText` function streams the props data as response to the client, while `useChat` decode the stream as `toolInvocations` and renders the chat interface. ```ts filename="@/app/api/chat/route.ts" import { z } from 'zod'; import { openai } from '@ai-sdk/openai'; import { getWeather } from '@/utils/queries'; import { streamText } from 'ai'; export async function POST(request) { const { messages } = await request.json(); const result = streamText({ model: openai('gpt-4o'), system: 'you are a friendly assistant!', messages, tools: { displayWeather: { description: 'Display the weather for a location', parameters: z.object({ latitude: z.number(), longitude: z.number(), }), execute: async function ({ latitude, longitude }) { const props = await getWeather({ latitude, longitude }); return props; }, }, }, }); return result.toUIMessageStreamResponse(); } ``` #### After: Update client to use chat hook and render components using tool invocations ```tsx filename="@/app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { Weather } from '@/components/weather'; export default function Page() { const { messages, input, setInput, handleSubmit } = useChat(); return ( <div> {messages.map(message => ( <div key={message.id}> <div>{message.role}</div> <div>{message.content}</div> <div> {message.toolInvocations.map(toolInvocation => { const { toolName, toolCallId, state } = toolInvocation; if (state === 'result') { const { result } = toolInvocation; return ( <div key={toolCallId}> {toolName === 'displayWeather' ? ( <Weather weatherAtLocation={result} /> ) : null} </div> ); } else { return ( <div key={toolCallId}> {toolName === 'displayWeather' ? ( <div>Loading weather...</div> ) : null} </div> ); } })} </div> </div> ))} <form onSubmit={handleSubmit}> <input type="text" value={input} onChange={event => { setInput(event.target.value); }} /> <button type="submit">Send</button> </form> </div> ); } ``` ### Handling Client Interactions With AI SDK RSC, components streamed to the client can trigger subsequent generations by calling the relevant server action using the `useActions` hooks. This is possible as long as the component is a descendant of the `<AI/>` context provider. #### Before: Use actions hook to send messages ```tsx filename="@/app/components/list-flights.tsx" 'use client'; import { useActions, useUIState } from '@ai-sdk/rsc'; export function ListFlights({ flights }) { const { sendMessage } = useActions(); const [_, setMessages] = useUIState(); return ( <div> {flights.map(flight => ( <div key={flight.id} onClick={async () => { const response = await sendMessage( `I would like to choose flight ${flight.id}!`, ); setMessages(msgs => [...msgs, response]); }} > {flight.name} </div> ))} </div> ); } ``` #### After: Use another chat hook with same ID from the component After switching to AI SDK UI, these messages are synced by initializing the `useChat` hook in the component with the same `id` as the parent component. ```tsx filename="@/app/components/list-flights.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; export function ListFlights({ chatId, flights }) { const { append } = useChat({ id: chatId, body: { id: chatId }, maxSteps: 5, }); return ( <div> {flights.map(flight => ( <div key={flight.id} onClick={async () => { await append({ role: 'user', content: `I would like to choose flight ${flight.id}!`, }); }} > {flight.name} </div> ))} </div> ); } ``` ### Loading Indicators In AI SDK RSC, you can use the `initial` parameter of `streamUI` to define the component to display while the generation is in progress. #### Before: Use `loading` to show loading indicator ```tsx filename="@/app/actions.tsx" import { openai } from '@ai-sdk/openai'; import { streamUI } from '@ai-sdk/rsc'; const { value: stream } = await streamUI({ model: openai('gpt-4o'), system: 'you are a friendly assistant!', messages, initial: <div>Loading...</div>, text: async function* ({ content, done }) { // process text }, tools: { // tool definitions }, }); return stream; ``` With AI SDK UI, you can use the tool invocation state to show a loading indicator while the tool is executing. #### After: Use tool invocation state to show loading indicator ```tsx filename="@/app/components/message.tsx" 'use client'; export function Message({ role, content, toolInvocations }) { return ( <div> <div>{role}</div> <div>{content}</div> {toolInvocations && ( <div> {toolInvocations.map(toolInvocation => { const { toolName, toolCallId, state } = toolInvocation; if (state === 'result') { const { result } = toolInvocation; return ( <div key={toolCallId}> {toolName === 'getWeather' ? ( <Weather weatherAtLocation={result} /> ) : null} </div> ); } else { return ( <div key={toolCallId}> {toolName === 'getWeather' ? ( <Weather isLoading={true} /> ) : ( <div>Loading...</div> )} </div> ); } })} </div> )} </div> ); } ``` ### Saving Chats Before implementing `streamUI` as a server action, you should create an `<AI/>` provider and wrap your application at the root layout to sync the AI and UI states. During initialization, you typically use the `onSetAIState` callback function to track updates to the AI state and save it to the database when `done(...)` is called. #### Before: Save chats using callback function of context provider ```ts filename="@/app/actions.ts" import { createAI } from '@ai-sdk/rsc'; import { saveChat } from '@/utils/queries'; export const AI = createAI({ initialAIState: {}, initialUIState: {}, actions: { // server actions }, onSetAIState: async ({ state, done }) => { 'use server'; if (done) { await saveChat(state); } }, }); ``` #### After: Save chats using callback function of `streamText` With AI SDK UI, you will save chats using the `onFinish` callback function of `streamText` in your route handler. ```ts filename="@/app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { saveChat } from '@/utils/queries'; import { streamText, convertToModelMessages } from 'ai'; export async function POST(request) { const { id, messages } = await request.json(); const coreMessages = convertToModelMessages(messages); const result = streamText({ model: openai('gpt-4o'), system: 'you are a friendly assistant!', messages: coreMessages, onFinish: async ({ response }) => { try { await saveChat({ id, messages: [...coreMessages, ...response.messages], }); } catch (error) { console.error('Failed to save chat'); } }, }); return result.toUIMessageStreamResponse(); } ``` ### Restoring Chats When using AI SDK RSC, the `useUIState` hook contains the UI state of the chat. When restoring a previously saved chat, the UI state needs to be loaded with messages. Similar to how you typically save chats in AI SDK RSC, you should use the `onGetUIState` callback function to retrieve the chat from the database, convert it into UI state, and return it to be accessible through `useUIState`. #### Before: Load chat from database using callback function of context provider ```ts filename="@/app/actions.ts" import { createAI } from '@ai-sdk/rsc'; import { loadChatFromDB, convertToUIState } from '@/utils/queries'; export const AI = createAI({ actions: { // server actions }, onGetUIState: async () => { 'use server'; const chat = await loadChatFromDB(); const uiState = convertToUIState(chat); return uiState; }, }); ``` AI SDK UI uses the `messages` field of `useChat` to store messages. To load messages when `useChat` is mounted, you should use `initialMessages`. As messages are typically loaded from the database, we can use a server actions inside a Page component to fetch an older chat from the database during static generation and pass the messages as props to the `<Chat/>` component. #### After: Load chat from database during static generation of page ```tsx filename="@/app/chat/[id]/page.tsx" import { Chat } from '@/app/components/chat'; import { getChatById } from '@/utils/queries'; // link to example implementation: https://github.com/vercel/ai-chatbot/blob/00b125378c998d19ef60b73fe576df0fe5a0e9d4/lib/utils.ts#L87-L127 import { convertToUIMessages } from '@/utils/functions'; export default async function Page({ params }: { params: any }) { const { id } = params; const chatFromDb = await getChatById({ id }); const chat: Chat = { ...chatFromDb, messages: convertToUIMessages(chatFromDb.messages), }; return <Chat key={id} id={chat.id} initialMessages={chat.messages} />; } ``` #### After: Pass chat messages as props and load into chat hook ```tsx filename="@/app/components/chat.tsx" 'use client'; import { Message } from 'ai'; import { useChat } from '@ai-sdk/react'; export function Chat({ id, initialMessages, }: { id; initialMessages: Array<Message>; }) { const { messages } = useChat({ id, initialMessages, }); return ( <div> {messages.map(message => ( <div key={message.id}> <div>{message.role}</div> <div>{message.content}</div> </div> ))} </div> ); } ``` ## Streaming Object Generation The `createStreamableValue` function streams any serializable data from the server to the client. As a result, this function allows you to stream object generations from the server to the client when paired with `streamObject`. #### Before: Use streamable value to stream object generations ```ts filename="@/app/actions.ts" import { streamObject } from 'ai'; import { openai } from '@ai-sdk/openai'; import { createStreamableValue } from '@ai-sdk/rsc'; import { notificationsSchema } from '@/utils/schemas'; export async function generateSampleNotifications() { 'use server'; const stream = createStreamableValue(); (async () => { const { partialObjectStream } = streamObject({ model: openai('gpt-4o'), system: 'generate sample ios messages for testing', prompt: 'messages from a family group chat during diwali, max 4', schema: notificationsSchema, }); for await (const partialObject of partialObjectStream) { stream.update(partialObject); } })(); stream.done(); return { partialNotificationsStream: stream.value }; } ``` #### Before: Read streamable value and update object ```tsx filename="@/app/page.tsx" 'use client'; import { useState } from 'react'; import { readStreamableValue } from '@ai-sdk/rsc'; import { generateSampleNotifications } from '@/app/actions'; export default function Page() { const [notifications, setNotifications] = useState(null); return ( <div> <button onClick={async () => { const { partialNotificationsStream } = await generateSampleNotifications(); for await (const partialNotifications of readStreamableValue( partialNotificationsStream, )) { if (partialNotifications) { setNotifications(partialNotifications.notifications); } } }} > Generate </button> </div> ); } ``` To migrate to AI SDK UI, you should use the `useObject` hook and implement `streamObject` within your route handler. #### After: Replace with route handler and stream text response ```ts filename="@/app/api/object/route.ts" import { streamObject } from 'ai'; import { openai } from '@ai-sdk/openai'; import { notificationSchema } from '@/utils/schemas'; export async function POST(req: Request) { const context = await req.json(); const result = streamObject({ model: openai('gpt-4.1'), schema: notificationSchema, prompt: `Generate 3 notifications for a messages app in this context:` + context, }); return result.toTextStreamResponse(); } ``` #### After: Use object hook to decode stream and update object ```tsx filename="@/app/page.tsx" 'use client'; import { useObject } from '@ai-sdk/react'; import { notificationSchema } from '@/utils/schemas'; export default function Page() { const { object, submit } = useObject({ api: '/api/object', schema: notificationSchema, }); return ( <div> <button onClick={() => submit('Messages during finals week.')}> Generate notifications </button> {object?.notifications?.map((notification, index) => ( <div key={index}> <p>{notification?.name}</p> <p>{notification?.message}</p> </div> ))} </div> ); } ``` --- File: /ai/content/docs/05-ai-sdk-rsc/index.mdx --- --- title: AI SDK RSC description: Learn about AI SDK RSC. collapsed: true --- # AI SDK RSC <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> <IndexCards cards={[ { title: 'Overview', description: 'Learn about AI SDK RSC.', href: '/docs/ai-sdk-rsc/overview', }, { title: 'Streaming React Components', description: 'Learn how to stream React components.', href: '/docs/ai-sdk-rsc/streaming-react-components', }, { title: 'Managing Generative UI State', description: 'Learn how to manage generative UI state.', href: '/docs/ai-sdk-rsc/generative-ui-state', }, { title: 'Saving and Restoring States', description: 'Learn how to save and restore states.', href: '/docs/ai-sdk-rsc/saving-and-restoring-states', }, { title: 'Multi-step Interfaces', description: 'Learn how to build multi-step interfaces.', href: '/docs/ai-sdk-rsc/multistep-interfaces', }, { title: 'Streaming Values', description: 'Learn how to stream values with AI SDK RSC.', href: '/docs/ai-sdk-rsc/streaming-values', }, { title: 'Error Handling', description: 'Learn how to handle errors.', href: '/docs/ai-sdk-rsc/error-handling', }, { title: 'Authentication', description: 'Learn how to authenticate users.', href: '/docs/ai-sdk-rsc/authentication', }, ]} /> --- File: /ai/content/docs/06-advanced/01-prompt-engineering.mdx --- --- title: Prompt Engineering description: Learn how to engineer prompts for LLMs with the AI SDK --- # Prompt Engineering ## What is a Large Language Model (LLM)? A Large Language Model is essentially a prediction engine that takes a sequence of words as input and aims to predict the most likely sequence to follow. It does this by assigning probabilities to potential next sequences and then selecting one. The model continues to generate sequences until it meets a specified stopping criterion. These models learn by training on massive text corpuses, which means they will be better suited to some use cases than others. For example, a model trained on GitHub data would understand the probabilities of sequences in source code particularly well. However, it's crucial to understand that the generated sequences, while often seeming plausible, can sometimes be random and not grounded in reality. As these models become more accurate, many surprising abilities and applications emerge. ## What is a prompt? Prompts are the starting points for LLMs. They are the inputs that trigger the model to generate text. The scope of prompt engineering involves not just crafting these prompts but also understanding related concepts such as hidden prompts, tokens, token limits, and the potential for prompt hacking, which includes phenomena like jailbreaks and leaks. ## Why is prompt engineering needed? Prompt engineering currently plays a pivotal role in shaping the responses of LLMs. It allows us to tweak the model to respond more effectively to a broader range of queries. This includes the use of techniques like semantic search, command grammars, and the ReActive model architecture. The performance, context window, and cost of LLMs varies between models and model providers which adds further constraints to the mix. For example, the GPT-4 model is more expensive than GPT-3.5-turbo and significantly slower, but it can also be more effective at certain tasks. And so, like many things in software engineering, there is a trade-offs between cost and performance. To assist with comparing and tweaking LLMs, we've built an AI playground that allows you to compare the performance of different models side-by-side online. When you're ready, you can even generate code with the AI SDK to quickly use your prompt and your selected model into your own applications. ## Example: Build a Slogan Generator ### Start with an instruction Imagine you want to build a slogan generator for marketing campaigns. Creating catchy slogans isn't always straightforward! First, you'll need a prompt that makes it clear what you want. Let's start with an instruction. Submit this prompt to generate your first completion. <InlinePrompt initialInput="Create a slogan for a coffee shop." /> Not bad! Now, try making your instruction more specific. <InlinePrompt initialInput="Create a slogan for an organic coffee shop." /> Introducing a single descriptive term to our prompt influences the completion. Essentially, crafting your prompt is the means by which you "instruct" or "program" the model. ### Include examples Clear instructions are key for quality outcomes, but that might not always be enough. Let's try to enhance your instruction further. <InlinePrompt initialInput="Create three slogans for a coffee shop with live music." /> These slogans are fine, but could be even better. It appears the model overlooked the 'live' part in our prompt. Let's change it slightly to generate more appropriate suggestions. Often, it's beneficial to both demonstrate and tell the model your requirements. Incorporating examples in your prompt can aid in conveying patterns or subtleties. Test this prompt that carries a few examples. <InlinePrompt initialInput={`Create three slogans for a business with unique features. Business: Bookstore with cats Slogans: "Purr-fect Pages", "Books and Whiskers", "Novels and Nuzzles" Business: Gym with rock climbing Slogans: "Peak Performance", "Reach New Heights", "Climb Your Way Fit" Business: Coffee shop with live music Slogans:`} /> Great! Incorporating examples of expected output for a certain input prompted the model to generate the kind of names we aimed for. ### Tweak your settings Apart from designing prompts, you can influence completions by tweaking model settings. A crucial setting is the **temperature**. You might have seen that the same prompt, when repeated, yielded the same or nearly the same completions. This happens when your temperature is at 0. Attempt to re-submit the identical prompt a few times with temperature set to 1. <InlinePrompt initialInput={`Create three slogans for a business with unique features. Business: Bookstore with cats Slogans: "Purr-fect Pages", "Books and Whiskers", "Novels and Nuzzles" Business: Gym with rock climbing Slogans: "Peak Performance", "Reach New Heights", "Climb Your Way Fit" Business: Coffee shop with live music Slogans:`} showTemp={true} initialTemperature={1} /> Notice the difference? With a temperature above 0, the same prompt delivers varied completions each time. Keep in mind that the model forecasts the text most likely to follow the preceding text. Temperature, a value from 0 to 1, essentially governs the model's confidence level in making these predictions. A lower temperature implies lesser risks, leading to more precise and deterministic completions. A higher temperature yields a broader range of completions. For your slogan generator, you might want a large pool of name suggestions. A moderate temperature of 0.6 should serve well. ## Recommended Resources Prompt Engineering is evolving rapidly, with new methods and research papers surfacing every week. Here are some resources that we've found useful for learning about and experimenting with prompt engineering: - [The Vercel AI Playground](/playground) - [Brex Prompt Engineering](https://github.com/brexhq/prompt-engineering) - [Prompt Engineering Guide by Dair AI](https://www.promptingguide.ai/) --- File: /ai/content/docs/06-advanced/02-stopping-streams.mdx --- --- title: Stopping Streams description: Learn how to cancel streams with the AI SDK --- # Stopping Streams Cancelling ongoing streams is often needed. For example, users might want to stop a stream when they realize that the response is not what they want. The different parts of the AI SDK support cancelling streams in different ways. ## AI SDK Core The AI SDK functions have an `abortSignal` argument that you can use to cancel a stream. You would use this if you want to cancel a stream from the server side to the LLM API, e.g. by forwarding the `abortSignal` from the request. ```tsx highlight="10,11,12-16" import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; export async function POST(req: Request) { const { prompt } = await req.json(); const result = streamText({ model: openai('gpt-4.1'), prompt, // forward the abort signal: abortSignal: req.signal, onAbort: ({ steps }) => { // Handle cleanup when stream is aborted console.log('Stream aborted after', steps.length, 'steps'); // Persist partial results to database }, }); return result.toTextStreamResponse(); } ``` ## AI SDK UI The hooks, e.g. `useChat` or `useCompletion`, provide a `stop` helper function that can be used to cancel a stream. This will cancel the stream from the client side to the server. ```tsx file="app/page.tsx" highlight="9,18-20" 'use client'; import { useCompletion } from '@ai-sdk/react'; export default function Chat() { const { input, completion, stop, status, handleSubmit, handleInputChange } = useCompletion(); return ( <div> {(status === 'submitted' || status === 'streaming') && ( <button type="button" onClick={() => stop()}> Stop </button> )} {completion} <form onSubmit={handleSubmit}> <input value={input} onChange={handleInputChange} /> </form> </div> ); } ``` ## Handling stream abort cleanup When streams are aborted, you may need to perform cleanup operations such as persisting partial results or cleaning up resources. The `onAbort` callback provides a way to handle these scenarios on the server side. Unlike `onFinish`, which is called when a stream completes normally, `onAbort` is specifically called when a stream is aborted via `AbortSignal`. This distinction allows you to handle normal completion and aborted streams differently. <Note> For UI message streams (`toUIMessageStreamResponse`), the `onFinish` callback also receives an `isAborted` parameter that indicates whether the stream was aborted. This allows you to handle both completion and abort scenarios in a single callback. </Note> ```tsx highlight="8-12" import { streamText } from 'ai'; const result = streamText({ model: openai('gpt-4.1'), prompt: 'Write a long story...', abortSignal: controller.signal, onAbort: ({ steps }) => { // Called when stream is aborted - persist partial results await savePartialResults(steps); await logAbortEvent(steps.length); }, onFinish: ({ steps, totalUsage }) => { // Called when stream completes normally await saveFinalResults(steps, totalUsage); }, }); ``` The `onAbort` callback receives: - `steps`: Array of all completed steps before the abort occurred This is particularly useful for: - Persisting partial conversation history to database - Saving partial progress for later continuation - Cleaning up server-side resources or connections - Logging abort events for analytics You can also handle abort events directly in the stream using the `abort` stream part: ```tsx highlight="8-12" for await (const part of result.fullStream) { switch (part.type) { case 'text-delta': // Handle text delta content break; case 'abort': // Handle abort event directly in stream console.log('Stream was aborted'); break; // ... other cases } } ``` ## UI Message Streams When using `toUIMessageStreamResponse`, you need to handle stream abortion slightly differently. The `onFinish` callback receives an `isAborted` parameter, and you should pass the `consumeStream` function to ensure proper abort handling: ```tsx highlight="5,19,20-24,26" import { openai } from '@ai-sdk/openai'; import { consumeStream, 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), abortSignal: req.signal, }); return result.toUIMessageStreamResponse({ onFinish: async ({ isAborted }) => { if (isAborted) { console.log('Stream was aborted'); // Handle abort-specific cleanup } else { console.log('Stream completed normally'); // Handle normal completion } }, consumeSseStream: consumeStream, }); } ``` The `consumeStream` function is necessary for proper abort handling in UI message streams. It ensures that the stream is properly consumed even when aborted, preventing potential memory leaks or hanging connections. ## AI SDK RSC <Note type="warning"> The AI SDK RSC does not currently support stopping streams. </Note> --- File: /ai/content/docs/06-advanced/03-backpressure.mdx --- --- title: Backpressure description: How to handle backpressure and cancellation when working with the AI SDK --- # Stream Back-pressure and Cancellation This page focuses on understanding back-pressure and cancellation when working with streams. You do not need to know this information to use the AI SDK, but for those interested, it offers a deeper dive on why and how the SDK optimally streams responses. In the following sections, we'll explore back-pressure and cancellation in the context of a simple example program. We'll discuss the issues that can arise from an eager approach and demonstrate how a lazy approach can resolve them. ## Back-pressure and Cancellation with Streams Let's begin by setting up a simple example program: ```jsx // A generator that will yield positive integers async function* integers() { let i = 1; while (true) { console.log(`yielding ${i}`); yield i++; await sleep(100); } } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Wraps a generator into a ReadableStream function createStream(iterator) { return new ReadableStream({ async start(controller) { for await (const v of iterator) { controller.enqueue(v); } controller.close(); }, }); } // Collect data from stream async function run() { // Set up a stream of integers const stream = createStream(integers()); // Read values from our stream const reader = stream.getReader(); for (let i = 0; i < 10_000; i++) { // we know our stream is infinite, so there's no need to check `done`. const { value } = await reader.read(); console.log(`read ${value}`); await sleep(1_000); } } run(); ``` In this example, we create an async-generator that yields positive integers, a `ReadableStream` that wraps our integer generator, and a reader which will read values out of our stream. Notice, too, that our integer generator logs out `"yielding ${i}"`, and our reader logs out `"read ${value}"`. Both take an arbitrary amount of time to process data, represented with a 100ms sleep in our generator, and a 1sec sleep in our reader. ## Back-pressure If you were to run this program, you'd notice something funny. We'll see roughly 10 "yield" logs for every "read" log. This might seem obvious, the generator can push values 10x faster than the reader can pull them out. But it represents a problem, our `stream` has to maintain an ever expanding queue of items that have been pushed in but not pulled out. The problem stems from the way we wrap our generator into a stream. Notice the use of `for await (…)` inside our `start` handler. This is an **eager** for-loop, and it is constantly running to get the next value from our generator to be enqueued in our stream. This means our stream does not respect back-pressure, the signal from the consumer to the producer that more values aren't needed _yet_. We've essentially spawned a thread that will perpetually push more data into the stream, one that runs as fast as possible to push new data immediately. Worse, there's no way to signal to this thread to stop running when we don't need additional data. To fix this, `ReadableStream` allows a `pull` handler. `pull` is called every time the consumer attempts to read more data from our stream (if there's no data already queued internally). But it's not enough to just move the `for await(…)` into `pull`, we also need to convert from an eager enqueuing to a **lazy** one. By making these 2 changes, we'll be able to react to the consumer. If they need more data, we can easily produce it, and if they don't, then we don't need to spend any time doing unnecessary work. ```jsx function createStream(iterator) { return new ReadableStream({ async pull(controller) { const { value, done } = await iterator.next(); if (done) { controller.close(); } else { controller.enqueue(value); } }, }); } ``` Our `createStream` is a little more verbose now, but the new code is important. First, we need to manually call our `iterator.next()` method. This returns a `Promise` for an object with the type signature `{ done: boolean, value: T }`. If `done` is `true`, then we know that our iterator won't yield any more values and we must `close` the stream (this allows the consumer to know that the stream is also finished producing values). Else, we need to `enqueue` our newly produced value. When we run this program, we see that our "yield" and "read" logs are now paired. We're no longer yielding 10x integers for every read! And, our stream now only needs to maintain 1 item in its internal buffer. We've essentially given control to the consumer, so that it's responsible for producing new values as it needs it. Neato! ## Cancellation Let's go back to our initial eager example, with 1 small edit. Now instead of reading 10,000 integers, we're only going to read 3: ```jsx // A generator that will yield positive integers async function* integers() { let i = 1; while (true) { console.log(`yielding ${i}`); yield i++; await sleep(100); } } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Wraps a generator into a ReadableStream function createStream(iterator) { return new ReadableStream({ async start(controller) { for await (const v of iterator) { controller.enqueue(v); } controller.close(); }, }); } // Collect data from stream async function run() { // Set up a stream that of integers const stream = createStream(integers()); // Read values from our stream const reader = stream.getReader(); // We're only reading 3 items this time: for (let i = 0; i < 3; i++) { // we know our stream is infinite, so there's no need to check `done`. const { value } = await reader.read(); console.log(`read ${value}`); await sleep(1000); } } run(); ``` We're back to yielding 10x the number of values read. But notice now, after we've read 3 values, we're continuing to yield new values. We know that our reader will never read another value, but our stream doesn't! The eager `for await (…)` will continue forever, loudly enqueuing new values into our stream's buffer and increasing our memory usage until it consumes all available program memory. The fix to this is exactly the same: use `pull` and manual iteration. By producing values _**lazily**_, we tie the lifetime of our integer generator to the lifetime of the reader. Once the reads stop, the yields will stop too: ```jsx // Wraps a generator into a ReadableStream function createStream(iterator) { return new ReadableStream({ async pull(controller) { const { value, done } = await iterator.next(); if (done) { controller.close(); } else { controller.enqueue(value); } }, }); } ``` Since the solution is the same as implementing back-pressure, it shows that they're just 2 facets of the same problem: Pushing values into a stream should be done **lazily**, and doing it eagerly results in expected problems. ## Tying Stream Laziness to AI Responses Now let's imagine you're integrating AIBot service into your product. Users will be able to prompt "count from 1 to infinity", the browser will fetch your AI API endpoint, and your servers connect to AIBot to get a response. But "infinity" is, well, infinite. The response will never end! After a few seconds, the user gets bored and navigates away. Or maybe you're doing local development and a hot-module reload refreshes your page. The browser will have ended its connection to the API endpoint, but will your server end its connection with AIBot? If you used the eager `for await (...)` approach, then the connection is still running and your server is asking for more and more data from AIBot. Our server spawned a "thread" and there's no signal when we can end the eager pulls. Eventually, the server is going to run out of memory (remember, there's no active fetch connection to read the buffering responses and free them). {/* When we started writing the streaming code for the AI SDK, we confirm aborting a fetch will end a streamed response from Next.js */} With the lazy approach, this is taken care of for you. Because the stream will only request new data from AIBot when the consumer requests it, navigating away from the page naturally frees all resources. The fetch connection aborts and the server can clean up the response. The `ReadableStream` tied to that response can now be garbage collected. When that happens, the connection it holds to AIBot can then be freed. --- File: /ai/content/docs/06-advanced/04-caching.mdx --- --- title: Caching description: How to handle caching when working with the AI SDK --- # Caching Responses Depending on the type of application you're building, you may want to cache the responses you receive from your AI provider, at least temporarily. ## Using Language Model Middleware (Recommended) The recommended approach to caching responses is using [language model middleware](/docs/ai-sdk-core/middleware) and the [`simulateReadableStream`](/docs/reference/ai-sdk-core/simulate-readable-stream) function. Language model middleware is a way to enhance the behavior of language models by intercepting and modifying the calls to the language model. Let's see how you can use language model middleware to cache responses. ```ts filename="ai/middleware.ts" import { Redis } from '@upstash/redis'; import { type LanguageModelV2, type LanguageModelV2Middleware, type LanguageModelV2StreamPart, simulateReadableStream, } from 'ai'; const redis = new Redis({ url: process.env.KV_URL, token: process.env.KV_TOKEN, }); export const cacheMiddleware: LanguageModelV2Middleware = { wrapGenerate: async ({ doGenerate, params }) => { const cacheKey = JSON.stringify(params); const cached = (await redis.get(cacheKey)) as Awaited< ReturnType<LanguageModelV2['doGenerate']> > | null; if (cached !== null) { return { ...cached, response: { ...cached.response, timestamp: cached?.response?.timestamp ? new Date(cached?.response?.timestamp) : undefined, }, }; } const result = await doGenerate(); redis.set(cacheKey, result); return result; }, wrapStream: async ({ doStream, params }) => { const cacheKey = JSON.stringify(params); // Check if the result is in the cache const cached = await redis.get(cacheKey); // If cached, return a simulated ReadableStream that yields the cached result if (cached !== null) { // Format the timestamps in the cached response const formattedChunks = (cached as LanguageModelV2StreamPart[]).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, }), }; } // If not cached, proceed with streaming const { stream, ...rest } = await doStream(); const fullResponse: LanguageModelV2StreamPart[] = []; const transformStream = new TransformStream< LanguageModelV2StreamPart, LanguageModelV2StreamPart >({ transform(chunk, controller) { fullResponse.push(chunk); controller.enqueue(chunk); }, flush() { // Store the full response in the cache after streaming is complete redis.set(cacheKey, fullResponse); }, }); return { stream: stream.pipeThrough(transformStream), ...rest, }; }, }; ``` <Note> This example uses `@upstash/redis` to store and retrieve the assistant's responses but you can use any KV storage provider you would like. </Note> `LanguageModelMiddleware` has two methods: `wrapGenerate` and `wrapStream`. `wrapGenerate` is called when using [`generateText`](/docs/reference/ai-sdk-core/generate-text) and [`generateObject`](/docs/reference/ai-sdk-core/generate-object), while `wrapStream` is called when using [`streamText`](/docs/reference/ai-sdk-core/stream-text) and [`streamObject`](/docs/reference/ai-sdk-core/stream-object). For `wrapGenerate`, you can cache the response directly. Instead, for `wrapStream`, you cache an array of the stream parts, which can then be used with [`simulateReadableStream`](/docs/ai-sdk-core/testing#simulate-data-stream-protocol-responses) function to create a simulated `ReadableStream` that returns the cached response. In this way, the cached response is returned chunk-by-chunk as if it were being generated by the model. You can control the initial delay and delay between chunks by adjusting the `initialDelayInMs` and `chunkDelayInMs` parameters of `simulateReadableStream`. You can see a full example of caching with Redis in a Next.js application in our [Caching Middleware Recipe](/cookbook/next/caching-middleware). ## Using Lifecycle Callbacks Alternatively, each AI SDK Core function has special lifecycle callbacks you can use. The one of interest is likely `onFinish`, which is called when the generation is complete. This is where you can cache the full response. Here's an example of how you can implement caching using Vercel KV and Next.js to cache the OpenAI response for 1 hour: This example uses [Upstash Redis](https://upstash.com/docs/redis/overall/getstarted) and Next.js to cache the response for 1 hour. ```tsx filename="app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { formatDataStreamPart, streamText, UIMessage } from 'ai'; import { Redis } from '@upstash/redis'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; const redis = new Redis({ url: process.env.KV_URL, token: process.env.KV_TOKEN, }); export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); // come up with a key based on the request: const key = JSON.stringify(messages); // Check if we have a cached response const cached = await redis.get(key); if (cached != null) { return new Response(formatDataStreamPart('text', cached), { status: 200, headers: { 'Content-Type': 'text/plain' }, }); } // Call the language model: const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), async onFinish({ text }) { // Cache the response text: await redis.set(key, text); await redis.expire(key, 60 * 60); }, }); // Respond with the stream return result.toUIMessageStreamResponse(); } ``` --- File: /ai/content/docs/06-advanced/05-multiple-streamables.mdx --- --- title: Multiple Streamables description: Learn to handle multiple streamables in your application. --- # Multiple Streams ## Multiple Streamable UIs The AI SDK RSC APIs allow you to compose and return any number of streamable UIs, along with other data, in a single request. This can be useful when you want to decouple the UI into smaller components and stream them separately. ```tsx file='app/actions.tsx' 'use server'; import { createStreamableUI } from '@ai-sdk/rsc'; export async function getWeather() { const weatherUI = createStreamableUI(); const forecastUI = createStreamableUI(); weatherUI.update(<div>Loading weather...</div>); forecastUI.update(<div>Loading forecast...</div>); getWeatherData().then(weatherData => { weatherUI.done(<div>{weatherData}</div>); }); getForecastData().then(forecastData => { forecastUI.done(<div>{forecastData}</div>); }); // Return both streamable UIs and other data fields. return { requestedAt: Date.now(), weather: weatherUI.value, forecast: forecastUI.value, }; } ``` The client side code is similar to the previous example, but the [tool call](/docs/ai-sdk-core/tools-and-tool-calling) will return the new data structure with the weather and forecast UIs. Depending on the speed of getting weather and forecast data, these two components might be updated independently. ## Nested Streamable UIs You can stream UI components within other UI components. This allows you to create complex UIs that are built up from smaller, reusable components. In the example below, we pass a `historyChart` streamable as a prop to a `StockCard` component. The StockCard can render the `historyChart` streamable, and it will automatically update as the server responds with new data. ```tsx file='app/actions.tsx' async function getStockHistoryChart({ symbol: string }) { 'use server'; const ui = createStreamableUI(<Spinner />); // We need to wrap this in an async IIFE to avoid blocking. (async () => { const price = await getStockPrice({ symbol }); // Show a spinner as the history chart for now. const historyChart = createStreamableUI(<Spinner />); ui.done(<StockCard historyChart={historyChart.value} price={price} />); // Getting the history data and then update that part of the UI. const historyData = await fetch('https://my-stock-data-api.com'); historyChart.done(<HistoryChart data={historyData} />); })(); return ui; } ``` --- File: /ai/content/docs/06-advanced/06-rate-limiting.mdx --- --- title: Rate Limiting description: Learn how to rate limit your application. --- # Rate Limiting Rate limiting helps you protect your APIs from abuse. It involves setting a maximum threshold on the number of requests a client can make within a specified timeframe. This simple technique acts as a gatekeeper, preventing excessive usage that can degrade service performance and incur unnecessary costs. ## Rate Limiting with Vercel KV and Upstash Ratelimit In this example, you will protect an API endpoint using [Vercel KV](https://vercel.com/storage/kv) and [Upstash Ratelimit](https://github.com/upstash/ratelimit). ```tsx filename='app/api/generate/route.ts' import kv from '@vercel/kv'; import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import { Ratelimit } from '@upstash/ratelimit'; import { NextRequest } from 'next/server'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; // Create Rate limit const ratelimit = new Ratelimit({ redis: kv, limiter: Ratelimit.fixedWindow(5, '30s'), }); export async function POST(req: NextRequest) { // call ratelimit with request ip const ip = req.ip ?? 'ip'; const { success, remaining } = await ratelimit.limit(ip); // block the request if unsuccessfull if (!success) { return new Response('Ratelimited!', { status: 429 }); } const { messages } = await req.json(); const result = streamText({ model: openai('gpt-3.5-turbo'), messages, }); return result.toUIMessageStreamResponse(); } ``` ## Simplify API Protection With Vercel KV and Upstash Ratelimit, it is possible to protect your APIs from such attacks with ease. To learn more about how Ratelimit works and how it can be configured to your needs, see [Ratelimit Documentation](https://upstash.com/docs/oss/sdks/ts/ratelimit/overview). --- File: /ai/content/docs/06-advanced/07-rendering-ui-with-language-models.mdx --- --- title: Rendering UI with Language Models description: Rendering UI with Language Models --- # Rendering User Interfaces with Language Models Language models generate text, so at first it may seem like you would only need to render text in your application. ```tsx highlight="16" filename="app/actions.tsx" const text = generateText({ model: openai('gpt-3.5-turbo'), system: 'You are a friendly assistant', prompt: 'What is the weather in SF?', tools: { getWeather: { description: 'Get the weather for a location', parameters: z.object({ city: z.string().describe('The city to get the weather for'), unit: z .enum(['C', 'F']) .describe('The unit to display the temperature in'), }), execute: async ({ city, unit }) => { const weather = getWeather({ city, unit }); return `It is currently ${weather.value}°${unit} and ${weather.description} in ${city}!`; }, }, }, }); ``` Above, the language model is passed a [tool](/docs/ai-sdk-core/tools-and-tool-calling) called `getWeather` that returns the weather information as text. However, instead of returning text, if you return a JSON object that represents the weather information, you can use it to render a React component instead. ```tsx highlight="18-23" filename="app/action.ts" const text = generateText({ model: openai('gpt-3.5-turbo'), system: 'You are a friendly assistant', prompt: 'What is the weather in SF?', tools: { getWeather: { description: 'Get the weather for a location', parameters: z.object({ city: z.string().describe('The city to get the weather for'), unit: z .enum(['C', 'F']) .describe('The unit to display the temperature in'), }), execute: async ({ city, unit }) => { const weather = getWeather({ city, unit }); const { temperature, unit, description, forecast } = weather; return { temperature, unit, description, forecast, }; }, }, }, }); ``` Now you can use the object returned by the `getWeather` function to conditionally render a React component `<WeatherCard/>` that displays the weather information by passing the object as props. ```tsx filename="app/page.tsx" return ( <div> {messages.map(message => { if (message.role === 'function') { const { name, content } = message const { temperature, unit, description, forecast } = content; return ( <WeatherCard weather={{ temperature: 47, unit: 'F', description: 'sunny' forecast, }} /> ) } })} </div> ) ``` Here's a little preview of what that might look like. <div className="not-prose flex flex-col2"> <CardPlayer type="weather" title="Weather" description="An example of an assistant that renders the weather information in a streamed component." /> </div> Rendering interfaces as part of language model generations elevates the user experience of your application, allowing people to interact with language models beyond text. They also make it easier for you to interpret [sequential tool calls](/docs/ai-sdk-rsc/multistep-interfaces) that take place in multiple steps and help identify and debug where the model reasoned incorrectly. ## Rendering Multiple User Interfaces To recap, an application has to go through the following steps to render user interfaces as part of model generations: 1. The user prompts the language model. 2. The language model generates a response that includes a tool call. 3. The tool call returns a JSON object that represents the user interface. 4. The response is sent to the client. 5. The client receives the response and checks if the latest message was a tool call. 6. If it was a tool call, the client renders the user interface based on the JSON object returned by the tool call. Most applications have multiple tools that are called by the language model, and each tool can return a different user interface. For example, a tool that searches for courses can return a list of courses, while a tool that searches for people can return a list of people. As this list grows, the complexity of your application will grow as well and it can become increasingly difficult to manage these user interfaces. ```tsx filename='app/page.tsx' { message.role === 'tool' ? ( message.name === 'api-search-course' ? ( <Courses courses={message.content} /> ) : message.name === 'api-search-profile' ? ( <People people={message.content} /> ) : message.name === 'api-meetings' ? ( <Meetings meetings={message.content} /> ) : message.name === 'api-search-building' ? ( <Buildings buildings={message.content} /> ) : message.name === 'api-events' ? ( <Events events={message.content} /> ) : message.name === 'api-meals' ? ( <Meals meals={message.content} /> ) : null ) : ( <div>{message.content}</div> ); } ``` ## Rendering User Interfaces on the Server The **AI SDK RSC (`@ai-sdk/rsc`)** takes advantage of RSCs to solve the problem of managing all your React components on the client side, allowing you to render React components on the server and stream them to the client. Rather than conditionally rendering user interfaces on the client based on the data returned by the language model, you can directly stream them from the server during a model generation. ```tsx highlight="3,22-31,38" filename="app/action.ts" import { createStreamableUI } from '@ai-sdk/rsc' const uiStream = createStreamableUI(); const text = generateText({ model: openai('gpt-3.5-turbo'), system: 'you are a friendly assistant' prompt: 'what is the weather in SF?' tools: { getWeather: { description: 'Get the weather for a location', parameters: z.object({ city: z.string().describe('The city to get the weather for'), unit: z .enum(['C', 'F']) .describe('The unit to display the temperature in') }), execute: async ({ city, unit }) => { const weather = getWeather({ city, unit }) const { temperature, unit, description, forecast } = weather uiStream.done( <WeatherCard weather={{ temperature: 47, unit: 'F', description: 'sunny' forecast, }} /> ) } } } }) return { display: uiStream.value } ``` The [`createStreamableUI`](/docs/reference/ai-sdk-rsc/create-streamable-ui) function belongs to the `@ai-sdk/rsc` module and creates a stream that can send React components to the client. On the server, you render the `<WeatherCard/>` component with the props passed to it, and then stream it to the client. On the client side, you only need to render the UI that is streamed from the server. ```tsx filename="app/page.tsx" highlight="4" return ( <div> {messages.map(message => ( <div>{message.display}</div> ))} </div> ); ``` Now the steps involved are simplified: 1. The user prompts the language model. 2. The language model generates a response that includes a tool call. 3. The tool call renders a React component along with relevant props that represent the user interface. 4. The response is streamed to the client and rendered directly. > **Note:** You can also render text on the server and stream it to the client using React Server Components. This way, all operations from language model generation to UI rendering can be done on the server, while the client only needs to render the UI that is streamed from the server. Check out this [example](/examples/next-app/interface/stream-component-updates) for a full illustration of how to stream component updates with React Server Components in Next.js App Router. --- File: /ai/content/docs/06-advanced/08-model-as-router.mdx --- --- title: Language Models as Routers description: Generative User Interfaces and Language Models as Routers --- # Generative User Interfaces Since language models can render user interfaces as part of their generations, the resulting model generations are referred to as generative user interfaces. In this section we will learn more about generative user interfaces and their impact on the way AI applications are built. ## Deterministic Routes and Probabilistic Routing Generative user interfaces are not deterministic in nature because they depend on the model's generation output. Since these generations are probabilistic in nature, it is possible for every user query to result in a different user interface. Users expect their experience using your application to be predictable, so non-deterministic user interfaces can sound like a bad idea at first. However, language models can be set up to limit their generations to a particular set of outputs using their ability to call functions. When language models are provided with a set of function definitions and instructed to execute any of them based on user query, they do either one of the following things: - Execute a function that is most relevant to the user query. - Not execute any function if the user query is out of bounds of the set of functions available to them. ```tsx filename='app/actions.ts' const sendMessage = (prompt: string) => generateText({ model: 'gpt-3.5-turbo', system: 'you are a friendly weather assistant!', prompt, tools: { getWeather: { description: 'Get the weather in a location', parameters: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }: { location: string }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }, }, }); sendMessage('What is the weather in San Francisco?'); // getWeather is called sendMessage('What is the weather in New York?'); // getWeather is called sendMessage('What events are happening in London?'); // No function is called ``` This way, it is possible to ensure that the generations result in deterministic outputs, while the choice a model makes still remains to be probabilistic. This emergent ability exhibited by a language model to choose whether a function needs to be executed or not based on a user query is believed to be models emulating "reasoning". As a result, the combination of language models being able to reason which function to execute as well as render user interfaces at the same time gives you the ability to build applications where language models can be used as a router. ## Language Models as Routers Historically, developers had to write routing logic that connected different parts of an application to be navigable by a user and complete a specific task. In web applications today, most of the routing logic takes place in the form of routes: - `/login` would navigate you to a page with a login form. - `/user/john` would navigate you to a page with profile details about John. - `/api/events?limit=5` would display the five most recent events from an events database. While routes help you build web applications that connect different parts of an application into a seamless user experience, it can also be a burden to manage them as the complexity of applications grow. Next.js has helped reduce complexity in developing with routes by introducing: - File-based routing system - Dynamic routing - API routes - Middleware - App router, and so on... With language models becoming better at reasoning, we believe that there is a future where developers only write core application specific components while models take care of routing them based on the user's state in an application. With generative user interfaces, the language model decides which user interface to render based on the user's state in the application, giving users the flexibility to interact with your application in a conversational manner instead of navigating through a series of predefined routes. ### Routing by parameters For routes like: - `/profile/[username]` - `/search?q=[query]` - `/media/[id]` that have segments dependent on dynamic data, the language model can generate the correct parameters and render the user interface. For example, when you're in a search application, you can ask the language model to search for artworks from different artists. The language model will call the search function with the artist's name as a parameter and render the search results. <div className="not-prose"> <CardPlayer type="media-search" title="Media Search" description="Let your users see more than words can say by rendering components directly within your search experience." /> </div> ### Routing by sequence For actions that require a sequence of steps to be completed by navigating through different routes, the language model can generate the correct sequence of routes to complete in order to fulfill the user's request. For example, when you're in a calendar application, you can ask the language model to schedule a happy hour evening with your friends. The language model will then understand your request and will perform the right sequence of [tool calls](/docs/ai-sdk-core/tools-and-tool-calling) to: 1. Lookup your calendar 2. Lookup your friends' calendars 3. Determine the best time for everyone 4. Search for nearby happy hour spots 5. Create an event and send out invites to your friends <div className="not-prose"> <CardPlayer type="event-planning" title="Planning an Event" description="The model calls functions and generates interfaces based on user intent, acting like a router." /> </div> Just by defining functions to lookup contacts, pull events from a calendar, and search for nearby locations, the model is able to sequentially navigate the routes for you. To learn more, check out these [examples](/examples/next-app/interface) using the `streamUI` function to stream generative user interfaces to the client based on the response from the language model. --- File: /ai/content/docs/06-advanced/09-multistep-interfaces.mdx --- --- title: Multistep Interfaces description: Concepts behind building multistep interfaces --- # Multistep Interfaces Multistep interfaces refer to user interfaces that require multiple independent steps to be executed in order to complete a specific task. In order to understand multistep interfaces, it is important to understand two concepts: - Tool composition - Application context **Tool composition** is the process of combining multiple [tools](/docs/ai-sdk-core/tools-and-tool-calling) to create a new tool. This is a powerful concept that allows you to break down complex tasks into smaller, more manageable steps. **Application context** refers to the state of the application at any given point in time. This includes the user's input, the output of the language model, and any other relevant information. When designing multistep interfaces, you need to consider how the tools in your application can be composed together to form a coherent user experience as well as how the application context changes as the user progresses through the interface. ## Application Context The application context can be thought of as the conversation history between the user and the language model. The richer the context, the more information the model has to generate relevant responses. In the context of multistep interfaces, the application context becomes even more important. This is because **the user's input in one step may affect the output of the model in the next step**. For example, consider a meal logging application that helps users track their daily food intake. The language model is provided with the following tools: - `log_meal` takes in parameters like the name of the food, the quantity, and the time of consumption to log a meal. - `delete_meal` takes in the name of the meal to be deleted. When the user logs a meal, the model generates a response confirming the meal has been logged. ```txt highlight="2" User: Log a chicken shawarma for lunch. Tool: log_meal("chicken shawarma", "250g", "12:00 PM") Model: Chicken shawarma has been logged for lunch. ``` Now when the user decides to delete the meal, the model should be able to reference the previous step to identify the meal to be deleted. ```txt highlight="7" User: Log a chicken shawarma for lunch. Tool: log_meal("chicken shawarma", "250g", "12:00 PM") Model: Chicken shawarma has been logged for lunch. ... ... User: I skipped lunch today, can you update my log? Tool: delete_meal("chicken shawarma") Model: Chicken shawarma has been deleted from your log. ``` In this example, managing the application context is important for the model to generate the correct response. The model needs to have information about the previous actions in order for it to use generate the parameters for the `delete_meal` tool. ## Tool Composition Tool composition is the process of combining multiple tools to create a new tool. This involves defining the inputs and outputs of each tool, as well as how they interact with each other. The design of how these tools can be composed together to form a multistep interface is crucial to both the user experience of your application and the model's ability to generate the correct output. For example, consider a flight booking assistant that can help users book flights. The assistant can be designed to have the following tools: - `searchFlights`: Searches for flights based on the user's query. - `lookupFlight`: Looks up details of a specific flight based on the flight number. - `bookFlight`: Books a flight based on the user's selection. The `searchFlights` tool is called when the user wants to lookup flights for a specific route. This would typically mean the tool should be able to take in parameters like the origin and destination of the flight. The `lookupFlight` tool is called when the user wants to get more details about a specific flight. This would typically mean the tool should be able to take in parameters like the flight number and return information about seat availability. The `bookFlight` tool is called when the user decides to book a flight. In order to identify the flight to book, the tool should be able to take in parameters like the flight number, trip date, and passenger details. So the conversation between the user and the model could look like this: ```txt highlight="8" User: I want to book a flight from New York to London. Tool: searchFlights("New York", "London") Model: Here are the available flights from New York to London. User: I want to book flight number BA123 on 12th December for myself and my wife. Tool: lookupFlight("BA123") -> "4 seats available" Model: Sure, there are seats available! Can you provide the names of the passengers? User: John Doe and Jane Doe. Tool: bookFlight("BA123", "12th December", ["John Doe", "Jane Doe"]) Model: Your flight has been booked! ``` In the last tool call, the `bookFlight` tool does not include passenger details as it is not available in the application context. As a result, it requests the user to provide the passenger details before proceeding with the booking. Looking up passenger information could've been another tool that the model could've called before calling the `bookFlight` tool assuming that the user is logged into the application. This way, the model does not have to ask the user for the passenger details and can proceed with the booking. ```txt highlight="5,6" User: I want to book a flight from New York to London. Tool: searchFlights("New York", "London") Model: Here are the available flights from New York to London. User: I want to book flight number BA123 on 12th December for myself an my wife. Tool: lookupContacts() -> ["John Doe", "Jane Doe"] Tool: bookFlight("BA123", "12th December", ["John Doe", "Jane Doe"]) Model: Your flight has been booked! ``` The `lookupContacts` tool is called before the `bookFlight` tool to ensure that the passenger details are available in the application context when booking the flight. This way, the model can reduce the number of steps required from the user and use its ability to call tools that populate its context and use that information to complete the booking process. Now, let's introduce another tool called `lookupBooking` that can be used to show booking details by taking in the name of the passenger as parameter. This tool can be composed with the existing tools to provide a more complete user experience. ```txt highlight="2-4" User: What's the status of my wife's upcoming flight? Tool: lookupContacts() -> ["John Doe", "Jane Doe"] Tool: lookupBooking("Jane Doe") -> "BA123 confirmed" Tool: lookupFlight("BA123") -> "Flight BA123 is scheduled to depart on 12th December." Model: Your wife's flight BA123 is confirmed and scheduled to depart on 12th December. ``` In this example, the `lookupBooking` tool is used to provide the user with the status of their wife's upcoming flight. By composing this tool with the existing tools, the model is able to generate a response that includes the booking status and the departure date of the flight without requiring the user to provide additional information. As a result, the more tools you design that can be composed together, the more complex and powerful your application can become. --- File: /ai/content/docs/06-advanced/09-sequential-generations.mdx --- --- title: Sequential Generations description: Learn how to implement sequential generations ("chains") with the AI SDK --- # Sequential Generations When working with the AI SDK, you may want to create sequences of generations (often referred to as "chains" or "pipes"), where the output of one becomes the input for the next. This can be useful for creating more complex AI-powered workflows or for breaking down larger tasks into smaller, more manageable steps. ## Example In a sequential chain, the output of one generation is directly used as input for the next generation. This allows you to create a series of dependent generations, where each step builds upon the previous one. Here's an example of how you can implement sequential actions: ```typescript import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; async function sequentialActions() { // Generate blog post ideas const ideasGeneration = await generateText({ model: openai('gpt-4o'), prompt: 'Generate 10 ideas for a blog post about making spaghetti.', }); console.log('Generated Ideas:\n', ideasGeneration); // Pick the best idea const bestIdeaGeneration = await generateText({ model: openai('gpt-4o'), prompt: `Here are some blog post ideas about making spaghetti: ${ideasGeneration} Pick the best idea from the list above and explain why it's the best.`, }); console.log('\nBest Idea:\n', bestIdeaGeneration); // Generate an outline const outlineGeneration = await generateText({ model: openai('gpt-4o'), prompt: `We've chosen the following blog post idea about making spaghetti: ${bestIdeaGeneration} Create a detailed outline for a blog post based on this idea.`, }); console.log('\nBlog Post Outline:\n', outlineGeneration); } sequentialActions().catch(console.error); ``` In this example, we first generate ideas for a blog post, then pick the best idea, and finally create an outline based on that idea. Each step uses the output from the previous step as input for the next generation. --- File: /ai/content/docs/06-advanced/10-vercel-deployment-guide.mdx --- --- title: Vercel Deployment Guide description: Learn how to deploy an AI application to production on Vercel --- # Vercel Deployment Guide In this guide, you will deploy an AI application to [Vercel](https://vercel.com) using [Next.js](https://nextjs.org) (App Router). Vercel is a platform for developers that provides the tools, workflows, and infrastructure you need to build and deploy your web apps faster, without the need for additional configuration. Vercel allows for automatic deployments on every branch push and merges onto the production branch of your GitHub, GitLab, and Bitbucket projects. It is a great option for deploying your AI application. ## Before You Begin To follow along with this guide, you will need: - a Vercel account - an account with a Git provider (this tutorial will use [Github](https://github.com)) - an OpenAI API key This guide will teach you how to deploy the application you built in the Next.js (App Router) quickstart tutorial to Vercel. If you haven’t completed the quickstart guide, you can start with [this repo](https://github.com/vercel-labs/ai-sdk-deployment-guide). ## Commit Changes Vercel offers a powerful git-centered workflow that automatically deploys your application to production every time you push to your repository’s main branch. Before committing your local changes, make sure that you have a `.gitignore`. Within your `.gitignore`, ensure that you are excluding your environment variables (`.env`) and your node modules (`node_modules`). If you have any local changes, you can commit them by running the following commands: ```bash git add . git commit -m "init" ``` ## Create Git Repo You can create a GitHub repository from within your terminal, or on [github.com](https://github.com/). For this tutorial, you will use the GitHub CLI ([more info here](https://cli.github.com/)). To create your GitHub repository: 1. Navigate to [github.com](http://github.com/) 2. In the top right corner, click the "plus" icon and select "New repository" 3. Pick a name for your repository (this can be anything) 4. Click "Create repository" Once you have created your repository, GitHub will redirect you to your new repository. 1. Scroll down the page and copy the commands under the title "...or push an existing repository from the command line" 2. Go back to the terminal, paste and then run the commands Note: if you run into the error "error: remote origin already exists.", this is because your local repository is still linked to the repository you cloned. To "unlink", you can run the following command: ```bash rm -rf .git git init git add . git commit -m "init" ``` Rerun the code snippet from the previous step. ## Import Project in Vercel On the [New Project](https://vercel.com/new) page, under the **Import Git Repository** section, select the Git provider that you would like to import your project from. Follow the prompts to sign in to your GitHub account. Once you have signed in, you should see your newly created repository from the previous step in the "Import Git Repository" section. Click the "Import" button next to that project. ### Add Environment Variables Your application stores uses environment secrets to store your OpenAI API key using a `.env.local` file locally in development. To add this API key to your production deployment, expand the "Environment Variables" section and paste in your `.env.local` file. Vercel will automatically parse your variables and enter them in the appropriate `key:value` format. ### Deploy Press the **Deploy** button. Vercel will create the Project and deploy it based on the chosen configurations. ### Enjoy the confetti! To view your deployment, select the Project in the dashboard and then select the **Domain**. This page is now visible to anyone who has the URL. ## Considerations When deploying an AI application, there are infrastructure-related considerations to be aware of. ### Function Duration In most cases, you will call the large language model (LLM) on the server. By default, Vercel serverless functions have a maximum duration of 10 seconds on the Hobby Tier. Depending on your prompt, it can take an LLM more than this limit to complete a response. If the response is not resolved within this limit, the server will throw an error. You can specify the maximum duration of your Vercel function using [route segment config](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config). To update your maximum duration, add the following route segment config to the top of your route handler or the page which is calling your server action. ```ts export const maxDuration = 30; ``` You can increase the max duration to 60 seconds on the Hobby Tier. For other tiers, [see the documentation](https://vercel.com/docs/functions/runtimes#max-duration) for limits. ## Security Considerations Given the high cost of calling an LLM, it's important to have measures in place that can protect your application from abuse. ### Rate Limit Rate limiting is a method used to regulate network traffic by defining a maximum number of requests that a client can send to a server within a given time frame. Follow [this guide](https://vercel.com/guides/securing-ai-app-rate-limiting) to add rate limiting to your application. ### Firewall A firewall helps protect your applications and websites from DDoS attacks and unauthorized access. [Vercel Firewall](https://vercel.com/docs/security/vercel-firewall) is a set of tools and infrastructure, created specifically with security in mind. It automatically mitigates DDoS attacks and Enterprise teams can get further customization for their site, including dedicated support and custom rules for IP blocking. ## Troubleshooting - Streaming not working when [proxied](/docs/troubleshooting/streaming-not-working-when-proxied) - Experiencing [Timeouts](/docs/troubleshooting/timeout-on-vercel) --- File: /ai/content/docs/06-advanced/index.mdx --- --- title: Advanced description: Learn how to use advanced functionality within the AI SDK and RSC API. collapsed: true --- # Advanced This section covers advanced topics and concepts for the AI SDK and RSC API. Working with LLMs often requires a different mental model compared to traditional software development. After these concepts, you should have a better understanding of the paradigms behind the AI SDK and RSC API, and how to use them to build more AI applications. --- File: /ai/content/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx --- --- title: generateText description: API Reference for generateText. --- # `generateText()` Generates text and calls tools for a given prompt using a language model. It is ideal for non-interactive use cases such as automation tasks where you need to write text (e.g. drafting email or summarizing web pages) and for agents that use tools. ```ts import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; const { text } = await generateText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(text); ``` To see `generateText` in action, check out [these examples](#examples). ## Import <Snippet text={`import { generateText } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'model', type: 'LanguageModel', description: "The language model to use. Example: openai('gpt-4o')", }, { name: 'system', type: 'string', description: 'The system prompt to use that specifies the behavior of the model.', }, { name: 'prompt', type: 'string', description: 'The input prompt to generate the text from.', }, { name: 'messages', type: 'Array<SystemModelMessage | UserModelMessage | AssistantModelMessage | ToolModelMessage> | Array<UIMessage>', description: 'A list of messages that represent a conversation. Automatically converts UI messages from the useChat hook.', properties: [ { type: 'SystemModelMessage', parameters: [ { name: 'role', type: "'system'", description: 'The role for the system message.', }, { name: 'content', type: 'string', description: 'The content of the message.', }, ], }, { type: 'UserModelMessage', parameters: [ { name: 'role', type: "'user'", description: 'The role for the user message.', }, { name: 'content', type: 'string | Array<TextPart | ImagePart | FilePart>', description: 'The content of the message.', properties: [ { type: 'TextPart', parameters: [ { name: 'type', type: "'text'", description: 'The type of the message part.', }, { name: 'text', type: 'string', description: 'The text content of the message part.', }, ], }, { type: 'ImagePart', parameters: [ { name: 'type', type: "'image'", description: 'The type of the message part.', }, { name: 'image', type: 'string | Uint8Array | Buffer | ArrayBuffer | URL', description: 'The image content of the message part. String are either base64 encoded content, base64 data URLs, or http(s) URLs.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the image. Optional.', isOptional: true, }, ], }, { type: 'FilePart', parameters: [ { name: 'type', type: "'file'", description: 'The type of the message part.', }, { name: 'data', type: 'string | Uint8Array | Buffer | ArrayBuffer | URL', description: 'The file content of the message part. String are either base64 encoded content, base64 data URLs, or http(s) URLs.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the file.', }, ], }, ], }, ], }, { type: 'AssistantModelMessage', parameters: [ { name: 'role', type: "'assistant'", description: 'The role for the assistant message.', }, { name: 'content', type: 'string | Array<TextPart | FilePart | ReasoningPart | ToolCallPart>', description: 'The content of the message.', properties: [ { type: 'TextPart', parameters: [ { name: 'type', type: "'text'", description: 'The type of the message part.', }, { name: 'text', type: 'string', description: 'The text content of the message part.', }, ], }, { type: 'ReasoningPart', parameters: [ { name: 'type', type: "'reasoning'", description: 'The type of the message part.', }, { name: 'text', type: 'string', description: 'The reasoning text.', }, ], }, { type: 'FilePart', parameters: [ { name: 'type', type: "'file'", description: 'The type of the message part.', }, { name: 'data', type: 'string | Uint8Array | Buffer | ArrayBuffer | URL', description: 'The file content of the message part. String are either base64 encoded content, base64 data URLs, or http(s) URLs.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the file.', }, { name: 'filename', type: 'string', description: 'The name of the file.', isOptional: true, }, ], }, { type: 'ToolCallPart', parameters: [ { name: 'type', type: "'tool-call'", description: 'The type of the message part.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call.', }, { name: 'toolName', type: 'string', description: 'The name of the tool, which typically would be the name of the function.', }, { name: 'input', type: 'object based on zod schema', description: 'Input (parameters) generated by the model to be used by the tool.', }, ], }, ], }, ], }, { type: 'ToolModelMessage', parameters: [ { name: 'role', type: "'tool'", description: 'The role for the assistant message.', }, { name: 'content', type: 'Array<ToolResultPart>', description: 'The content of the message.', properties: [ { type: 'ToolResultPart', parameters: [ { name: 'type', type: "'tool-result'", description: 'The type of the message part.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call the result corresponds to.', }, { name: 'toolName', type: 'string', description: 'The name of the tool the result corresponds to.', }, { name: 'output', type: 'unknown', description: 'The result returned by the tool after execution.', }, { name: 'isError', type: 'boolean', isOptional: true, description: 'Whether the result is an error or an error message.', }, ], }, ], }, ], }, ], }, { name: 'tools', type: 'ToolSet', description: 'Tools that are accessible to and can be called by the model. The model needs to support calling tools.', properties: [ { type: 'Tool', parameters: [ { name: 'description', isOptional: true, type: 'string', description: 'Information about the purpose of the tool including details on how and when it can be used by the model.', }, { name: 'inputSchema', type: 'Zod Schema | JSON Schema', description: 'The schema of the input that the tool expects. The language model will use this to generate the input. It is also used to validate the output of the language model. Use descriptions to make the input understandable for the language model. You can either pass in a Zod schema or a JSON schema (using the `jsonSchema` function).', }, { name: 'execute', isOptional: true, type: 'async (parameters: T, options: ToolExecutionOptions) => RESULT', description: 'An async function that is called with the arguments from the tool call and produces a result. If not provided, the tool will not be executed automatically.', properties: [ { type: 'ToolExecutionOptions', parameters: [ { name: 'toolCallId', type: 'string', description: 'The ID of the tool call. You can use it e.g. when sending tool-call related information with stream data.', }, { name: 'messages', type: 'ModelMessage[]', description: 'Messages that were sent to the language model to initiate the response that contained the tool call. The messages do not include the system prompt nor the assistant response that contained the tool call.', }, { name: 'abortSignal', type: 'AbortSignal', description: 'An optional abort signal that indicates that the overall operation should be aborted.', }, ], }, ], }, ], }, ], }, { name: 'toolChoice', isOptional: true, type: '"auto" | "none" | "required" | { "type": "tool", "toolName": string }', description: 'The tool choice setting. It specifies how tools are selected for execution. The default is "auto". "none" disables tool execution. "required" requires tools to be executed. { "type": "tool", "toolName": string } specifies a specific tool to execute.', }, { name: 'maxOutputTokens', type: 'number', isOptional: true, description: 'Maximum number of tokens to generate.', }, { name: 'temperature', type: 'number', isOptional: true, description: 'Temperature setting. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both.', }, { name: 'topP', type: 'number', isOptional: true, description: 'Nucleus sampling. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both.', }, { name: 'topK', type: 'number', isOptional: true, description: 'Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature.', }, { name: 'presencePenalty', type: 'number', isOptional: true, description: 'Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The value is passed through to the provider. The range depends on the provider and model.', }, { name: 'frequencyPenalty', type: 'number', isOptional: true, description: 'Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The value is passed through to the provider. The range depends on the provider and model.', }, { name: 'stopSequences', type: 'string[]', isOptional: true, description: 'Sequences that will stop the generation of the text. If the model generates any of these sequences, it will stop generating further text.', }, { name: 'seed', type: 'number', isOptional: true, description: 'The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results.', }, { name: 'maxRetries', type: 'number', isOptional: true, description: 'Maximum number of retries. Set to 0 to disable retries. Default: 2.', }, { name: 'abortSignal', type: 'AbortSignal', isOptional: true, description: 'An optional abort signal that can be used to cancel the call.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.', }, { name: 'experimental_telemetry', type: 'TelemetrySettings', isOptional: true, description: 'Telemetry configuration. Experimental feature.', properties: [ { type: 'TelemetrySettings', parameters: [ { name: 'isEnabled', type: 'boolean', isOptional: true, description: 'Enable or disable telemetry. Disabled by default while experimental.', }, { name: 'recordInputs', type: 'boolean', isOptional: true, description: 'Enable or disable input recording. Enabled by default.', }, { name: 'recordOutputs', type: 'boolean', isOptional: true, description: 'Enable or disable output recording. Enabled by default.', }, { name: 'functionId', type: 'string', isOptional: true, description: 'Identifier for this function. Used to group telemetry data by function.', }, { name: 'metadata', isOptional: true, type: 'Record<string, string | number | boolean | Array<null | undefined | string> | Array<null | undefined | number> | Array<null | undefined | boolean>>', description: 'Additional information to include in the telemetry data.', }, ], }, ], }, { name: 'providerOptions', type: 'Record<string,Record<string,JSONValue>> | undefined', isOptional: true, description: 'Provider-specific options. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.', }, { name: 'activeTools', type: 'Array<TOOLNAME>', isOptional: true, description: 'Limits the tools that are available for the model to call without changing the tool call and result types in the result. All tools are active by default.', }, { name: 'stopWhen', type: 'StopCondition<TOOLS> | Array<StopCondition<TOOLS>>', isOptional: true, description: 'Condition for stopping the generation when there are tool results in the last step. When the condition is an array, any of the conditions can be met to stop the generation. Default: stepCountIs(1).', }, { name: 'prepareStep', type: '(options: PrepareStepOptions) => PrepareStepResult<TOOLS> | Promise<PrepareStepResult<TOOLS>>', isOptional: true, description: 'Optional function that you can use to provide different settings for a step. You can modify the model, tool choices, active tools, system prompt, and input messages for each step.', properties: [ { type: 'PrepareStepFunction<TOOLS>', parameters: [ { name: 'options', type: 'object', description: 'The options for the step.', properties: [ { type: 'PrepareStepOptions', parameters: [ { name: 'steps', type: 'Array<StepResult<TOOLS>>', description: 'The steps that have been executed so far.', }, { name: 'stepNumber', type: 'number', description: 'The number of the step that is being executed.', }, { name: 'model', type: 'LanguageModel', description: 'The model that is being used.', }, { name: 'messages', type: 'Array<ModelMessage>', description: 'The messages that will be sent to the model for the current step.', }, ], }, ], }, ], }, { type: 'PrepareStepResult<TOOLS>', description: 'Return value that can modify settings for the current step.', parameters: [ { name: 'model', type: 'LanguageModel', isOptional: true, description: 'Change the model for this step.', }, { name: 'toolChoice', type: 'ToolChoice<TOOLS>', isOptional: true, description: 'Change the tool choice strategy for this step.', }, { name: 'activeTools', type: 'Array<keyof TOOLS>', isOptional: true, description: 'Change which tools are active for this step.', }, { name: 'system', type: 'string', isOptional: true, description: 'Change the system prompt for this step.', }, { name: 'messages', type: 'Array<ModelMessage>', isOptional: true, description: 'Modify the input messages for this step.', }, ], }, ], }, { name: 'experimental_context', type: 'unknown', isOptional: true, description: 'Context that is passed into tool execution. Experimental (can break in patch releases).', }, { name: 'experimental_repairToolCall', type: '(options: ToolCallRepairOptions) => Promise<LanguageModelV2ToolCall | null>', isOptional: true, description: 'A function that attempts to repair a tool call that failed to parse. Return either a repaired tool call or null if the tool call cannot be repaired.', properties: [ { type: 'ToolCallRepairOptions', parameters: [ { name: 'system', type: 'string | undefined', description: 'The system prompt.', }, { name: 'messages', type: 'ModelMessage[]', description: 'The messages in the current generation step.', }, { name: 'toolCall', type: 'LanguageModelV2ToolCall', description: 'The tool call that failed to parse.', }, { name: 'tools', type: 'TOOLS', description: 'The tools that are available.', }, { name: 'parameterSchema', type: '(options: { toolName: string }) => JSONSchema7', description: 'A function that returns the JSON Schema for a tool.', }, { name: 'error', type: 'NoSuchToolError | InvalidToolArgumentsError', description: 'The error that occurred while parsing the tool call.', }, ], }, ], }, { name: 'experimental_output', type: 'Output', isOptional: true, description: 'Experimental setting for generating structured outputs.', properties: [ { type: 'Output', parameters: [ { name: 'Output.text()', type: 'Output', description: 'Forward text output.', }, { name: 'Output.object()', type: 'Output', description: 'Generate a JSON object of type OBJECT.', properties: [ { type: 'Options', parameters: [ { name: 'schema', type: 'Schema<OBJECT>', description: 'The schema of the JSON object to generate.', }, ], }, ], }, ], }, ], }, { name: 'onStepFinish', type: '(result: OnStepFinishResult) => Promise<void> | void', isOptional: true, description: 'Callback that is called when a step is finished.', properties: [ { type: 'OnStepFinishResult', parameters: [ { name: 'finishReason', type: '"stop" | "length" | "content-filter" | "tool-calls" | "error" | "other" | "unknown"', description: 'The reason the model finished generating the text for the step.', }, { name: 'usage', type: 'LanguageModelUsage', description: 'The token usage of the step.', properties: [ { type: 'LanguageModelUsage', parameters: [ { name: 'inputTokens', type: 'number | undefined', description: 'The number of input (prompt) tokens used.', }, { name: 'outputTokens', type: 'number | undefined', description: 'The number of output (completion) tokens used.', }, { name: 'totalTokens', type: 'number | undefined', description: 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', }, { name: 'reasoningTokens', type: 'number | undefined', isOptional: true, description: 'The number of reasoning tokens used.', }, { name: 'cachedInputTokens', type: 'number | undefined', isOptional: true, description: 'The number of cached input tokens.', }, ], }, ], }, { name: 'text', type: 'string', description: 'The full text that has been generated.', }, { name: 'toolCalls', type: 'ToolCall[]', description: 'The tool calls that have been executed.', }, { name: 'toolResults', type: 'ToolResult[]', description: 'The tool results that have been generated.', }, { name: 'warnings', type: 'Warning[] | undefined', description: 'Warnings from the model provider (e.g. unsupported settings).', }, { name: 'response', type: 'Response', isOptional: true, description: 'Response metadata.', properties: [ { type: 'Response', parameters: [ { name: 'id', type: 'string', description: 'The response identifier. The AI SDK uses the ID from the provider response when available, and generates an ID otherwise.', }, { name: 'modelId', type: 'string', description: 'The model that was used to generate the response. The AI SDK uses the response model from the provider response when available, and the model from the function call otherwise.', }, { name: 'timestamp', type: 'Date', description: 'The timestamp of the response. The AI SDK uses the response timestamp from the provider response when available, and creates a timestamp otherwise.', }, { name: 'headers', isOptional: true, type: 'Record<string, string>', description: 'Optional response headers.', }, { name: 'body', isOptional: true, type: 'unknown', description: 'Optional response body.', }, ], }, ], }, { name: 'isContinued', type: 'boolean', description: 'True when there will be a continuation step with a continuation text.', }, { name: 'providerMetadata', type: 'Record<string,Record<string,JSONValue>> | undefined', isOptional: true, description: 'Optional metadata from the provider. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.', }, ], }, ], }, ]} /> ### Returns <PropertiesTable content={[ { name: 'content', type: 'Array<ContentPart<TOOLS>>', description: 'The content that was generated in the last step.', }, { name: 'text', type: 'string', description: 'The generated text by the model.', }, { name: 'reasoning', type: 'Array<ReasoningPart>', description: 'The full reasoning that the model has generated in the last step.', properties: [ { type: 'ReasoningPart', parameters: [ { name: 'type', type: "'reasoning'", description: 'The type of the message part.', }, { name: 'text', type: 'string', description: 'The reasoning text.', }, ], }, ], }, { name: 'reasoningText', type: 'string | undefined', description: 'The reasoning text that the model has generated in the last step. Can be undefined if the model has only generated text.', }, { name: 'sources', type: 'Array<Source>', description: 'Sources that have been used as input to generate the response. For multi-step generation, the sources are accumulated from all steps.', properties: [ { type: 'Source', parameters: [ { name: 'sourceType', type: "'url'", description: 'A URL source. This is return by web search RAG models.', }, { name: 'id', type: 'string', description: 'The ID of the source.', }, { name: 'url', type: 'string', description: 'The URL of the source.', }, { name: 'title', type: 'string', isOptional: true, description: 'The title of the source.', }, { name: 'providerMetadata', type: 'SharedV2ProviderMetadata', isOptional: true, description: 'Additional provider metadata for the source.', }, ], }, ], }, { name: 'files', type: 'Array<GeneratedFile>', description: 'Files that were generated in the final step.', properties: [ { type: 'GeneratedFile', parameters: [ { name: 'base64', type: 'string', description: 'File as a base64 encoded string.', }, { name: 'uint8Array', type: 'Uint8Array', description: 'File as a Uint8Array.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the file.', }, ], }, ], }, { name: 'toolCalls', type: 'ToolCallArray<TOOLS>', description: 'The tool calls that were made in the last step.', }, { name: 'toolResults', type: 'ToolResultArray<TOOLS>', description: 'The results of the tool calls from the last step.', }, { name: 'finishReason', type: "'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error' | 'other' | 'unknown'", description: 'The reason the model finished generating the text.', }, { name: 'usage', type: 'LanguageModelUsage', description: 'The token usage of the last step.', properties: [ { type: 'LanguageModelUsage', parameters: [ { name: 'inputTokens', type: 'number | undefined', description: 'The number of input (prompt) tokens used.', }, { name: 'outputTokens', type: 'number | undefined', description: 'The number of output (completion) tokens used.', }, { name: 'totalTokens', type: 'number | undefined', description: 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', }, { name: 'reasoningTokens', type: 'number | undefined', isOptional: true, description: 'The number of reasoning tokens used.', }, { name: 'cachedInputTokens', type: 'number | undefined', isOptional: true, description: 'The number of cached input tokens.', }, ], }, ], }, { name: 'totalUsage', type: 'CompletionTokenUsage', description: 'The total token usage of all steps. When there are multiple steps, the usage is the sum of all step usages.', properties: [ { type: 'LanguageModelUsage', parameters: [ { name: 'inputTokens', type: 'number | undefined', description: 'The number of input (prompt) tokens used.', }, { name: 'outputTokens', type: 'number | undefined', description: 'The number of output (completion) tokens used.', }, { name: 'totalTokens', type: 'number | undefined', description: 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', }, { name: 'reasoningTokens', type: 'number | undefined', isOptional: true, description: 'The number of reasoning tokens used.', }, { name: 'cachedInputTokens', type: 'number | undefined', isOptional: true, description: 'The number of cached input tokens.', }, ], }, ], }, { name: 'request', type: 'LanguageModelRequestMetadata', isOptional: true, description: 'Request metadata.', properties: [ { type: 'LanguageModelRequestMetadata', parameters: [ { name: 'body', type: 'string', description: 'Raw request HTTP body that was sent to the provider API as a string (JSON should be stringified).', }, ], }, ], }, { name: 'response', type: 'LanguageModelResponseMetadata', isOptional: true, description: 'Response metadata.', properties: [ { type: 'LanguageModelResponseMetadata', parameters: [ { name: 'id', type: 'string', description: 'The response identifier. The AI SDK uses the ID from the provider response when available, and generates an ID otherwise.', }, { name: 'modelId', type: 'string', description: 'The model that was used to generate the response. The AI SDK uses the response model from the provider response when available, and the model from the function call otherwise.', }, { name: 'timestamp', type: 'Date', description: 'The timestamp of the response. The AI SDK uses the response timestamp from the provider response when available, and creates a timestamp otherwise.', }, { name: 'headers', isOptional: true, type: 'Record<string, string>', description: 'Optional response headers.', }, { name: 'body', isOptional: true, type: 'unknown', description: 'Optional response body.', }, { name: 'messages', type: 'Array<ResponseMessage>', description: 'The response messages that were generated during the call. It consists of an assistant message, potentially containing tool calls. When there are tool results, there is an additional tool message with the tool results that are available. If there are tools that do not have execute functions, they are not included in the tool results and need to be added separately.', }, ], }, ], }, { name: 'warnings', type: 'CallWarning[] | undefined', description: 'Warnings from the model provider (e.g. unsupported settings).', }, { name: 'providerMetadata', type: 'ProviderMetadata | undefined', description: 'Optional metadata from the provider. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.', }, { name: 'experimental_output', type: 'Output', isOptional: true, description: 'Experimental setting for generating structured outputs.', }, { name: 'steps', type: 'Array<StepResult<TOOLS>>', description: 'Response information for every step. You can use this to get information about intermediate steps, such as the tool calls or the response headers.', properties: [ { type: 'StepResult', parameters: [ { name: 'content', type: 'Array<ContentPart<TOOLS>>', description: 'The content that was generated in the last step.', }, { name: 'text', type: 'string', description: 'The generated text.', }, { name: 'reasoning', type: 'Array<ReasoningPart>', description: 'The reasoning that was generated during the generation.', properties: [ { type: 'ReasoningPart', parameters: [ { name: 'type', type: "'reasoning'", description: 'The type of the message part.', }, { name: 'text', type: 'string', description: 'The reasoning text.', }, ], }, ], }, { name: 'reasoningText', type: 'string | undefined', description: 'The reasoning text that was generated during the generation.', }, { name: 'files', type: 'Array<GeneratedFile>', description: 'The files that were generated during the generation.', properties: [ { type: 'GeneratedFile', parameters: [ { name: 'base64', type: 'string', description: 'File as a base64 encoded string.', }, { name: 'uint8Array', type: 'Uint8Array', description: 'File as a Uint8Array.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the file.', }, ], }, ], }, { name: 'sources', type: 'Array<Source>', description: 'The sources that were used to generate the text.', properties: [ { type: 'Source', parameters: [ { name: 'sourceType', type: "'url'", description: 'A URL source. This is return by web search RAG models.', }, { name: 'id', type: 'string', description: 'The ID of the source.', }, { name: 'url', type: 'string', description: 'The URL of the source.', }, { name: 'title', type: 'string', isOptional: true, description: 'The title of the source.', }, { name: 'providerMetadata', type: 'SharedV2ProviderMetadata', isOptional: true, description: 'Additional provider metadata for the source.', }, ], }, ], }, { name: 'toolCalls', type: 'ToolCallArray<TOOLS>', description: 'The tool calls that were made during the generation.', }, { name: 'toolResults', type: 'ToolResultArray<TOOLS>', description: 'The results of the tool calls.', }, { name: 'finishReason', type: "'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error' | 'other' | 'unknown'", description: 'The reason why the generation finished.', }, { name: 'usage', type: 'LanguageModelUsage', description: 'The token usage of the generated text.', properties: [ { type: 'LanguageModelUsage', parameters: [ { name: 'inputTokens', type: 'number | undefined', description: 'The number of input (prompt) tokens used.', }, { name: 'outputTokens', type: 'number | undefined', description: 'The number of output (completion) tokens used.', }, { name: 'totalTokens', type: 'number | undefined', description: 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', }, { name: 'reasoningTokens', type: 'number | undefined', isOptional: true, description: 'The number of reasoning tokens used.', }, { name: 'cachedInputTokens', type: 'number | undefined', isOptional: true, description: 'The number of cached input tokens.', }, ], }, ], }, { name: 'warnings', type: 'CallWarning[] | undefined', description: 'Warnings from the model provider (e.g. unsupported settings).', }, { name: 'request', type: 'LanguageModelRequestMetadata', description: 'Additional request information.', properties: [ { type: 'LanguageModelRequestMetadata', parameters: [ { name: 'body', type: 'string', description: 'Raw request HTTP body that was sent to the provider API as a string (JSON should be stringified).', }, ], }, ], }, { name: 'response', type: 'LanguageModelResponseMetadata', description: 'Additional response information.', properties: [ { type: 'LanguageModelResponseMetadata', parameters: [ { name: 'id', type: 'string', description: 'The response identifier. The AI SDK uses the ID from the provider response when available, and generates an ID otherwise.', }, { name: 'modelId', type: 'string', description: 'The model that was used to generate the response. The AI SDK uses the response model from the provider response when available, and the model from the function call otherwise.', }, { name: 'timestamp', type: 'Date', description: 'The timestamp of the response. The AI SDK uses the response timestamp from the provider response when available, and creates a timestamp otherwise.', }, { name: 'headers', isOptional: true, type: 'Record<string, string>', description: 'Optional response headers.', }, { name: 'body', isOptional: true, type: 'unknown', description: 'Response body (available only for providers that use HTTP requests).', }, { name: 'messages', type: 'Array<ResponseMessage>', description: 'The response messages that were generated during the call. Response messages can be either assistant messages or tool messages. They contain a generated id.', }, ], }, ], }, { name: 'providerMetadata', type: 'ProviderMetadata | undefined', description: 'Additional provider-specific metadata. They are passed through from the provider to the AI SDK and enable provider-specific results that can be fully encapsulated in the provider.', }, ], }, ], }, ]} /> ## Examples <ExampleLinks examples={[ { title: 'Learn to generate text using a language model in Next.js', link: '/examples/next-app/basics/generating-text', }, { title: 'Learn to generate a chat completion using a language model in Next.js', link: '/examples/next-app/basics/generating-text', }, { title: 'Learn to call tools using a language model in Next.js', link: '/examples/next-app/tools/call-tool', }, { title: 'Learn to render a React component as a tool call using a language model in Next.js', link: '/examples/next-app/tools/render-interface-during-tool-call', }, { title: 'Learn to generate text using a language model in Node.js', link: '/examples/node/generating-text/generate-text', }, { title: 'Learn to generate chat completions using a language model in Node.js', link: '/examples/node/generating-text/generate-text-with-chat-prompt', }, ]} /> --- File: /ai/content/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx --- --- title: streamText description: API Reference for streamText. --- # `streamText()` Streams text generations from a language model. You can use the streamText function for interactive use cases such as chat bots and other real-time applications. You can also generate UI components with tools. ```ts import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; const { textStream } = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); for await (const textPart of textStream) { process.stdout.write(textPart); } ``` To see `streamText` in action, check out [these examples](#examples). ## Import <Snippet text={`import { streamText } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'model', type: 'LanguageModel', description: "The language model to use. Example: openai('gpt-4.1')", }, { name: 'system', type: 'string', description: 'The system prompt to use that specifies the behavior of the model.', }, { name: 'prompt', type: 'string', description: 'The input prompt to generate the text from.', }, { name: 'messages', type: 'Array<SystemModelMessage | UserModelMessage | AssistantModelMessage | ToolModelMessage> | Array<UIMessage>', description: 'A list of messages that represent a conversation. Automatically converts UI messages from the useChat hook.', properties: [ { type: 'SystemModelMessage', parameters: [ { name: 'role', type: "'system'", description: 'The role for the system message.', }, { name: 'content', type: 'string', description: 'The content of the message.', }, ], }, { type: 'UserModelMessage', parameters: [ { name: 'role', type: "'user'", description: 'The role for the user message.', }, { name: 'content', type: 'string | Array<TextPart | ImagePart | FilePart>', description: 'The content of the message.', properties: [ { type: 'TextPart', parameters: [ { name: 'type', type: "'text'", description: 'The type of the message part.', }, { name: 'text', type: 'string', description: 'The text content of the message part.', }, ], }, { type: 'ImagePart', parameters: [ { name: 'type', type: "'image'", description: 'The type of the message part.', }, { name: 'image', type: 'string | Uint8Array | Buffer | ArrayBuffer | URL', description: 'The image content of the message part. String are either base64 encoded content, base64 data URLs, or http(s) URLs.', }, { name: 'mediaType', type: 'string', isOptional: true, description: 'The IANA media type of the image.', }, ], }, { type: 'FilePart', parameters: [ { name: 'type', type: "'file'", description: 'The type of the message part.', }, { name: 'data', type: 'string | Uint8Array | Buffer | ArrayBuffer | URL', description: 'The file content of the message part. String are either base64 encoded content, base64 data URLs, or http(s) URLs.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the file.', }, ], }, ], }, ], }, { type: 'AssistantModelMessage', parameters: [ { name: 'role', type: "'assistant'", description: 'The role for the assistant message.', }, { name: 'content', type: 'string | Array<TextPart | FilePart | ReasoningPart | ToolCallPart>', description: 'The content of the message.', properties: [ { type: 'TextPart', parameters: [ { name: 'type', type: "'text'", description: 'The type of the message part.', }, { name: 'text', type: 'string', description: 'The text content of the message part.', }, ], }, { type: 'ReasoningPart', parameters: [ { name: 'type', type: "'reasoning'", description: 'The type of the reasoning part.', }, { name: 'text', type: 'string', description: 'The reasoning text.', }, ], }, { type: 'FilePart', parameters: [ { name: 'type', type: "'file'", description: 'The type of the message part.', }, { name: 'data', type: 'string | Uint8Array | Buffer | ArrayBuffer | URL', description: 'The file content of the message part. String are either base64 encoded content, base64 data URLs, or http(s) URLs.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the file.', }, { name: 'filename', type: 'string', description: 'The name of the file.', isOptional: true, }, ], }, { type: 'ToolCallPart', parameters: [ { name: 'type', type: "'tool-call'", description: 'The type of the message part.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call.', }, { name: 'toolName', type: 'string', description: 'The name of the tool, which typically would be the name of the function.', }, { name: 'input', type: 'object based on zod schema', description: 'Parameters generated by the model to be used by the tool.', }, ], }, ], }, ], }, { type: 'ToolModelMessage', parameters: [ { name: 'role', type: "'tool'", description: 'The role for the assistant message.', }, { name: 'content', type: 'Array<ToolResultPart>', description: 'The content of the message.', properties: [ { type: 'ToolResultPart', parameters: [ { name: 'type', type: "'tool-result'", description: 'The type of the message part.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call the result corresponds to.', }, { name: 'toolName', type: 'string', description: 'The name of the tool the result corresponds to.', }, { name: 'result', type: 'unknown', description: 'The result returned by the tool after execution.', }, { name: 'isError', type: 'boolean', isOptional: true, description: 'Whether the result is an error or an error message.', }, ], }, ], }, ], }, ], }, { name: 'tools', type: 'ToolSet', description: 'Tools that are accessible to and can be called by the model. The model needs to support calling tools.', properties: [ { type: 'Tool', parameters: [ { name: 'description', isOptional: true, type: 'string', description: 'Information about the purpose of the tool including details on how and when it can be used by the model.', }, { name: 'inputSchema', type: 'Zod Schema | JSON Schema', description: 'The schema of the input that the tool expects. The language model will use this to generate the input. It is also used to validate the output of the language model. Use descriptions to make the input understandable for the language model. You can either pass in a Zod schema or a JSON schema (using the `jsonSchema` function).', }, { name: 'execute', isOptional: true, type: 'async (parameters: T, options: ToolExecutionOptions) => RESULT', description: 'An async function that is called with the arguments from the tool call and produces a result. If not provided, the tool will not be executed automatically.', properties: [ { type: 'ToolExecutionOptions', parameters: [ { name: 'toolCallId', type: 'string', description: 'The ID of the tool call. You can use it e.g. when sending tool-call related information with stream data.', }, { name: 'messages', type: 'ModelMessage[]', description: 'Messages that were sent to the language model to initiate the response that contained the tool call. The messages do not include the system prompt nor the assistant response that contained the tool call.', }, { name: 'abortSignal', type: 'AbortSignal', description: 'An optional abort signal that indicates that the overall operation should be aborted.', }, ], }, ], }, ], }, ], }, { name: 'toolChoice', isOptional: true, type: '"auto" | "none" | "required" | { "type": "tool", "toolName": string }', description: 'The tool choice setting. It specifies how tools are selected for execution. The default is "auto". "none" disables tool execution. "required" requires tools to be executed. { "type": "tool", "toolName": string } specifies a specific tool to execute.', }, { name: 'maxOutputTokens', type: 'number', isOptional: true, description: 'Maximum number of tokens to generate.', }, { name: 'temperature', type: 'number', isOptional: true, description: 'Temperature setting. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both.', }, { name: 'topP', type: 'number', isOptional: true, description: 'Nucleus sampling. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both.', }, { name: 'topK', type: 'number', isOptional: true, description: 'Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature.', }, { name: 'presencePenalty', type: 'number', isOptional: true, description: 'Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The value is passed through to the provider. The range depends on the provider and model.', }, { name: 'frequencyPenalty', type: 'number', isOptional: true, description: 'Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The value is passed through to the provider. The range depends on the provider and model.', }, { name: 'stopSequences', type: 'string[]', isOptional: true, description: 'Sequences that will stop the generation of the text. If the model generates any of these sequences, it will stop generating further text.', }, { name: 'seed', type: 'number', isOptional: true, description: 'The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results.', }, { name: 'maxRetries', type: 'number', isOptional: true, description: 'Maximum number of retries. Set to 0 to disable retries. Default: 2.', }, { name: 'abortSignal', type: 'AbortSignal', isOptional: true, description: 'An optional abort signal that can be used to cancel the call.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.', }, { name: 'experimental_generateMessageId', type: '() => string', isOptional: true, description: 'Function used to generate a unique ID for each message. This is an experimental feature.', }, { name: 'experimental_telemetry', type: 'TelemetrySettings', isOptional: true, description: 'Telemetry configuration. Experimental feature.', properties: [ { type: 'TelemetrySettings', parameters: [ { name: 'isEnabled', type: 'boolean', isOptional: true, description: 'Enable or disable telemetry. Disabled by default while experimental.', }, { name: 'recordInputs', type: 'boolean', isOptional: true, description: 'Enable or disable input recording. Enabled by default.', }, { name: 'recordOutputs', type: 'boolean', isOptional: true, description: 'Enable or disable output recording. Enabled by default.', }, { name: 'functionId', type: 'string', isOptional: true, description: 'Identifier for this function. Used to group telemetry data by function.', }, { name: 'metadata', isOptional: true, type: 'Record<string, string | number | boolean | Array<null | undefined | string> | Array<null | undefined | number> | Array<null | undefined | boolean>>', description: 'Additional information to include in the telemetry data.', }, ], }, ], }, { name: 'toolCallStreaming', type: 'boolean', isOptional: true, description: 'Enable streaming of tool call deltas as they are generated. Disabled by default.', }, { name: 'experimental_transform', type: 'StreamTextTransform | Array<StreamTextTransform>', isOptional: true, description: 'Optional stream transformations. They are applied in the order they are provided. The stream transformations must maintain the stream structure for streamText to work correctly.', properties: [ { type: 'StreamTextTransform', parameters: [ { name: 'transform', type: '(options: TransformOptions) => TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>', description: 'A transformation that is applied to the stream.', properties: [ { type: 'TransformOptions', parameters: [ { name: 'stopStream', type: '() => void', description: 'A function that stops the stream.', }, { name: 'tools', type: 'TOOLS', description: 'The tools that are available.', }, ], }, ], }, ], }, ], }, { name: 'includeRawChunks', type: 'boolean', isOptional: true, description: 'Whether to include raw chunks from the provider in the stream. When enabled, you will receive raw chunks with type "raw" that contain the unprocessed data from the provider. This allows access to cutting-edge provider features not yet wrapped by the AI SDK. Defaults to false.', }, { name: 'providerOptions', type: 'Record<string,Record<string,JSONValue>> | undefined', isOptional: true, description: 'Provider-specific options. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.', }, { name: 'activeTools', type: 'Array<TOOLNAME> | undefined', isOptional: true, description: 'The tools that are currently active. All tools are active by default.', }, { name: 'stopWhen', type: 'StopCondition<TOOLS> | Array<StopCondition<TOOLS>>', isOptional: true, description: 'Condition for stopping the generation when there are tool results in the last step. When the condition is an array, any of the conditions can be met to stop the generation. Default: stepCountIs(1).', }, { name: 'prepareStep', type: '(options: PrepareStepOptions) => PrepareStepResult<TOOLS> | Promise<PrepareStepResult<TOOLS>>', isOptional: true, description: 'Optional function that you can use to provide different settings for a step. You can modify the model, tool choices, active tools, system prompt, and input messages for each step.', properties: [ { type: 'PrepareStepFunction<TOOLS>', parameters: [ { name: 'options', type: 'object', description: 'The options for the step.', properties: [ { type: 'PrepareStepOptions', parameters: [ { name: 'steps', type: 'Array<StepResult<TOOLS>>', description: 'The steps that have been executed so far.', }, { name: 'stepNumber', type: 'number', description: 'The number of the step that is being executed.', }, { name: 'model', type: 'LanguageModel', description: 'The model that is being used.', }, { name: 'messages', type: 'Array<ModelMessage>', description: 'The messages that will be sent to the model for the current step.', }, ], }, ], }, ], }, { type: 'PrepareStepResult<TOOLS>', description: 'Return value that can modify settings for the current step.', parameters: [ { name: 'model', type: 'LanguageModel', isOptional: true, description: 'Change the model for this step.', }, { name: 'toolChoice', type: 'ToolChoice<TOOLS>', isOptional: true, description: 'Change the tool choice strategy for this step.', }, { name: 'activeTools', type: 'Array<keyof TOOLS>', isOptional: true, description: 'Change which tools are active for this step.', }, { name: 'system', type: 'string', isOptional: true, description: 'Change the system prompt for this step.', }, { name: 'messages', type: 'Array<ModelMessage>', isOptional: true, description: 'Modify the input messages for this step.', }, ], }, ], }, { name: 'experimental_context', type: 'unknown', isOptional: true, description: 'Context that is passed into tool execution. Experimental (can break in patch releases).', }, { name: 'experimental_repairToolCall', type: '(options: ToolCallRepairOptions) => Promise<LanguageModelV2ToolCall | null>', isOptional: true, description: 'A function that attempts to repair a tool call that failed to parse. Return either a repaired tool call or null if the tool call cannot be repaired.', properties: [ { type: 'ToolCallRepairOptions', parameters: [ { name: 'system', type: 'string | undefined', description: 'The system prompt.', }, { name: 'messages', type: 'ModelMessage[]', description: 'The messages in the current generation step.', }, { name: 'toolCall', type: 'LanguageModelV2ToolCall', description: 'The tool call that failed to parse.', }, { name: 'tools', type: 'TOOLS', description: 'The tools that are available.', }, { name: 'parameterSchema', type: '(options: { toolName: string }) => JSONSchema7', description: 'A function that returns the JSON Schema for a tool.', }, { name: 'error', type: 'NoSuchToolError | InvalidToolArgumentsError', description: 'The error that occurred while parsing the tool call.', }, ], }, ], }, { name: 'onChunk', type: '(event: OnChunkResult) => Promise<void> |void', isOptional: true, description: 'Callback that is called for each chunk of the stream. The stream processing will pause until the callback promise is resolved.', properties: [ { type: 'OnChunkResult', parameters: [ { name: 'chunk', type: 'TextStreamPart', description: 'The chunk of the stream.', properties: [ { type: 'TextStreamPart', parameters: [ { name: 'type', type: "'text'", description: 'The type to identify the object as text delta.', }, { name: 'text', type: 'string', description: 'The text delta.', }, ], }, { type: 'TextStreamPart', parameters: [ { name: 'type', type: "'reasoning'", description: 'The type to identify the object as reasoning.', }, { name: 'text', type: 'string', description: 'The reasoning text delta.', }, ], }, { type: 'TextStreamPart', parameters: [ { name: 'type', type: "'source'", description: 'The type to identify the object as source.', }, { name: 'source', type: 'Source', description: 'The source.', }, ], }, { type: 'TextStreamPart', parameters: [ { name: 'type', type: "'tool-call'", description: 'The type to identify the object as tool call.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call.', }, { name: 'toolName', type: 'string', description: 'The name of the tool, which typically would be the name of the function.', }, { name: 'input', type: 'object based on zod schema', description: 'Parameters generated by the model to be used by the tool.', }, ], }, { type: 'TextStreamPart', parameters: [ { name: 'type', type: "'tool-call-streaming-start'", description: 'Indicates the start of a tool call streaming. Only available when streaming tool calls.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call.', }, { name: 'toolName', type: 'string', description: 'The name of the tool, which typically would be the name of the function.', }, ], }, { type: 'TextStreamPart', parameters: [ { name: 'type', type: "'tool-call-delta'", description: 'The type to identify the object as tool call delta. Only available when streaming tool calls.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call.', }, { name: 'toolName', type: 'string', description: 'The name of the tool, which typically would be the name of the function.', }, { name: 'argsTextDelta', type: 'string', description: 'The text delta of the tool call arguments.', }, ], }, { type: 'TextStreamPart', description: 'The result of a tool call execution.', parameters: [ { name: 'type', type: "'tool-result'", description: 'The type to identify the object as tool result.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call.', }, { name: 'toolName', type: 'string', description: 'The name of the tool, which typically would be the name of the function.', }, { name: 'input', type: 'object based on zod schema', description: 'Parameters generated by the model to be used by the tool.', }, { name: 'output', type: 'any', description: 'The result returned by the tool after execution has completed.', }, ], }, ], }, ], }, ], }, { name: 'onError', type: '(event: OnErrorResult) => Promise<void> |void', isOptional: true, description: 'Callback that is called when an error occurs during streaming. You can use it to log errors.', properties: [ { type: 'OnErrorResult', parameters: [ { name: 'error', type: 'unknown', description: 'The error that occurred.', }, ], }, ], }, { name: 'experimental_output', type: 'Output', isOptional: true, description: 'Experimental setting for generating structured outputs.', properties: [ { type: 'Output', parameters: [ { name: 'Output.text()', type: 'Output', description: 'Forward text output.', }, { name: 'Output.object()', type: 'Output', description: 'Generate a JSON object of type OBJECT.', properties: [ { type: 'Options', parameters: [ { name: 'schema', type: 'Schema<OBJECT>', description: 'The schema of the JSON object to generate.', }, ], }, ], }, ], }, ], }, { name: 'onStepFinish', type: '(result: onStepFinishResult) => Promise<void> | void', isOptional: true, description: 'Callback that is called when a step is finished.', properties: [ { type: 'onStepFinishResult', parameters: [ { name: 'stepType', type: '"initial" | "continue" | "tool-result"', description: 'The type of step. The first step is always an "initial" step, and subsequent steps are either "continue" steps or "tool-result" steps.', }, { name: 'finishReason', type: '"stop" | "length" | "content-filter" | "tool-calls" | "error" | "other" | "unknown"', description: 'The reason the model finished generating the text for the step.', }, { name: 'usage', type: 'TokenUsage', description: 'The token usage of the step.', properties: [ { type: 'TokenUsage', parameters: [ { name: 'inputTokens', type: 'number', description: 'The total number of tokens in the prompt.', }, { name: 'outputTokens', type: 'number', description: 'The total number of tokens in the completion.', }, { name: 'totalTokens', type: 'number', description: 'The total number of tokens generated.', }, ], }, ], }, { name: 'text', type: 'string', description: 'The full text that has been generated.', }, { name: 'reasoning', type: 'string | undefined', description: 'The reasoning text of the model (only available for some models).', }, { name: 'sources', type: 'Array<Source>', description: 'Sources that have been used as input to generate the response. For multi-step generation, the sources are accumulated from all steps.', properties: [ { type: 'Source', parameters: [ { name: 'sourceType', type: "'url'", description: 'A URL source. This is return by web search RAG models.', }, { name: 'id', type: 'string', description: 'The ID of the source.', }, { name: 'url', type: 'string', description: 'The URL of the source.', }, { name: 'title', type: 'string', isOptional: true, description: 'The title of the source.', }, { name: 'providerMetadata', type: 'SharedV2ProviderMetadata', isOptional: true, description: 'Additional provider metadata for the source.', }, ], }, ], }, { name: 'files', type: 'Array<GeneratedFile>', description: 'All files that were generated in this step.', properties: [ { type: 'GeneratedFile', parameters: [ { name: 'base64', type: 'string', description: 'File as a base64 encoded string.', }, { name: 'uint8Array', type: 'Uint8Array', description: 'File as a Uint8Array.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the file.', }, ], }, ], }, { name: 'toolCalls', type: 'ToolCall[]', description: 'The tool calls that have been executed.', }, { name: 'toolResults', type: 'ToolResult[]', description: 'The tool results that have been generated.', }, { name: 'warnings', type: 'Warning[] | undefined', description: 'Warnings from the model provider (e.g. unsupported settings).', }, { name: 'response', type: 'Response', isOptional: true, description: 'Response metadata.', properties: [ { type: 'Response', parameters: [ { name: 'id', type: 'string', description: 'The response identifier. The AI SDK uses the ID from the provider response when available, and generates an ID otherwise.', }, { name: 'model', type: 'string', description: 'The model that was used to generate the response. The AI SDK uses the response model from the provider response when available, and the model from the function call otherwise.', }, { name: 'timestamp', type: 'Date', description: 'The timestamp of the response. The AI SDK uses the response timestamp from the provider response when available, and creates a timestamp otherwise.', }, { name: 'headers', isOptional: true, type: 'Record<string, string>', description: 'Optional response headers.', }, ], }, ], }, { name: 'isContinued', type: 'boolean', description: 'True when there will be a continuation step with a continuation text.', }, { name: 'providerMetadata', type: 'Record<string,Record<string,JSONValue>> | undefined', isOptional: true, description: 'Optional metadata from the provider. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.', }, ], }, ], }, { name: 'onFinish', type: '(result: OnFinishResult) => Promise<void> | void', isOptional: true, description: 'Callback that is called when the LLM response and all request tool executions (for tools that have an `execute` function) are finished.', properties: [ { type: 'OnFinishResult', parameters: [ { name: 'finishReason', type: '"stop" | "length" | "content-filter" | "tool-calls" | "error" | "other" | "unknown"', description: 'The reason the model finished generating the text.', }, { name: 'usage', type: 'TokenUsage', description: 'The token usage of the generated text.', properties: [ { type: 'TokenUsage', parameters: [ { name: 'inputTokens', type: 'number', description: 'The total number of tokens in the prompt.', }, { name: 'outputTokens', type: 'number', description: 'The total number of tokens in the completion.', }, { name: 'totalTokens', type: 'number', description: 'The total number of tokens generated.', }, ], }, ], }, { name: 'providerMetadata', type: 'Record<string,Record<string,JSONValue>> | undefined', description: 'Optional metadata from the provider. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.', }, { name: 'text', type: 'string', description: 'The full text that has been generated.', }, { name: 'reasoning', type: 'string | undefined', description: 'The reasoning text of the model (only available for some models).', }, { name: 'reasoningDetails', type: 'Array<ReasoningDetail>', description: 'The reasoning details of the model (only available for some models).', properties: [ { type: 'ReasoningDetail', parameters: [ { name: 'type', type: "'text'", description: 'The type of the reasoning detail.', }, { name: 'text', type: 'string', description: 'The text content (only for type "text").', }, { name: 'signature', type: 'string', isOptional: true, description: 'Optional signature (only for type "text").', }, ], }, { type: 'ReasoningDetail', parameters: [ { name: 'type', type: "'redacted'", description: 'The type of the reasoning detail.', }, { name: 'data', type: 'string', description: 'The redacted data content (only for type "redacted").', }, ], }, ], }, { name: 'sources', type: 'Array<Source>', description: 'Sources that have been used as input to generate the response. For multi-step generation, the sources are accumulated from all steps.', properties: [ { type: 'Source', parameters: [ { name: 'sourceType', type: "'url'", description: 'A URL source. This is return by web search RAG models.', }, { name: 'id', type: 'string', description: 'The ID of the source.', }, { name: 'url', type: 'string', description: 'The URL of the source.', }, { name: 'title', type: 'string', isOptional: true, description: 'The title of the source.', }, { name: 'providerMetadata', type: 'SharedV2ProviderMetadata', isOptional: true, description: 'Additional provider metadata for the source.', }, ], }, ], }, { name: 'files', type: 'Array<GeneratedFile>', description: 'Files that were generated in the final step.', properties: [ { type: 'GeneratedFile', parameters: [ { name: 'base64', type: 'string', description: 'File as a base64 encoded string.', }, { name: 'uint8Array', type: 'Uint8Array', description: 'File as a Uint8Array.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the file.', }, ], }, ], }, { name: 'toolCalls', type: 'ToolCall[]', description: 'The tool calls that have been executed.', }, { name: 'toolResults', type: 'ToolResult[]', description: 'The tool results that have been generated.', }, { name: 'warnings', type: 'Warning[] | undefined', description: 'Warnings from the model provider (e.g. unsupported settings).', }, { name: 'response', type: 'Response', isOptional: true, description: 'Response metadata.', properties: [ { type: 'Response', parameters: [ { name: 'id', type: 'string', description: 'The response identifier. The AI SDK uses the ID from the provider response when available, and generates an ID otherwise.', }, { name: 'model', type: 'string', description: 'The model that was used to generate the response. The AI SDK uses the response model from the provider response when available, and the model from the function call otherwise.', }, { name: 'timestamp', type: 'Date', description: 'The timestamp of the response. The AI SDK uses the response timestamp from the provider response when available, and creates a timestamp otherwise.', }, { name: 'headers', isOptional: true, type: 'Record<string, string>', description: 'Optional response headers.', }, { name: 'messages', type: 'Array<ResponseMessage>', description: 'The response messages that were generated during the call. It consists of an assistant message, potentially containing tool calls. When there are tool results, there is an additional tool message with the tool results that are available. If there are tools that do not have execute functions, they are not included in the tool results and need to be added separately.', }, ], }, ], }, { name: 'steps', type: 'Array<StepResult>', description: 'Response information for every step. You can use this to get information about intermediate steps, such as the tool calls or the response headers.', }, ], }, ], }, { name: 'onAbort', type: '(event: OnAbortResult) => Promise<void> | void', isOptional: true, description: 'Callback that is called when a stream is aborted via AbortSignal. You can use it to perform cleanup operations.', properties: [ { type: 'OnAbortResult', parameters: [ { name: 'steps', type: 'Array<StepResult>', description: 'Details for all previously finished steps.', }, ], }, ], }, ]} /> ### Returns <PropertiesTable content={[ { name: 'content', type: 'Promise<Array<ContentPart<TOOLS>>>', description: 'The content that was generated in the last step. Resolved when the response is finished.', }, { name: 'finishReason', type: "Promise<'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error' | 'other' | 'unknown'>", description: 'The reason why the generation finished. Resolved when the response is finished.', }, { name: 'usage', type: 'Promise<LanguageModelUsage>', description: 'The token usage of the last step. Resolved when the response is finished.', properties: [ { type: 'LanguageModelUsage', parameters: [ { name: 'promptTokens', type: 'number', description: 'The total number of tokens in the prompt.', }, { name: 'completionTokens', type: 'number', description: 'The total number of tokens in the completion.', }, { name: 'totalTokens', type: 'number', description: 'The total number of tokens generated.', }, ], }, ], }, { name: 'totalUsage', type: 'Promise<LanguageModelUsage>', description: 'The total token usage of the generated response. When there are multiple steps, the usage is the sum of all step usages. Resolved when the response is finished.', properties: [ { type: 'LanguageModelUsage', parameters: [ { name: 'promptTokens', type: 'number', description: 'The total number of tokens in the prompt.', }, { name: 'completionTokens', type: 'number', description: 'The total number of tokens in the completion.', }, { name: 'totalTokens', type: 'number', description: 'The total number of tokens generated.', }, ], }, ], }, { name: 'providerMetadata', type: 'Promise<ProviderMetadata | undefined>', description: 'Additional provider-specific metadata from the last step. Metadata is passed through from the provider to the AI SDK and enables provider-specific results that can be fully encapsulated in the provider.', }, { name: 'text', type: 'Promise<string>', description: 'The full text that has been generated. Resolved when the response is finished.', }, { name: 'reasoning', type: 'Promise<Array<ReasoningPart>>', description: 'The full reasoning that the model has generated in the last step. Resolved when the response is finished.', properties: [ { type: 'ReasoningPart', parameters: [ { name: 'type', type: "'reasoning'", description: 'The type of the reasoning part.', }, { name: 'text', type: 'string', description: 'The reasoning text.', }, ], }, ], }, { name: 'reasoningText', type: 'Promise<string | undefined>', description: 'The reasoning text that the model has generated in the last step. Can be undefined if the model has only generated text. Resolved when the response is finished.', }, { name: 'sources', type: 'Promise<Array<Source>>', description: 'Sources that have been used as input to generate the response. For multi-step generation, the sources are accumulated from all steps. Resolved when the response is finished.', properties: [ { type: 'Source', parameters: [ { name: 'sourceType', type: "'url'", description: 'A URL source. This is return by web search RAG models.', }, { name: 'id', type: 'string', description: 'The ID of the source.', }, { name: 'url', type: 'string', description: 'The URL of the source.', }, { name: 'title', type: 'string', isOptional: true, description: 'The title of the source.', }, { name: 'providerMetadata', type: 'SharedV2ProviderMetadata', isOptional: true, description: 'Additional provider metadata for the source.', }, ], }, ], }, { name: 'files', type: 'Promise<Array<GeneratedFile>>', description: 'Files that were generated in the final step. Resolved when the response is finished.', properties: [ { type: 'GeneratedFile', parameters: [ { name: 'base64', type: 'string', description: 'File as a base64 encoded string.', }, { name: 'uint8Array', type: 'Uint8Array', description: 'File as a Uint8Array.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the file.', }, ], }, ], }, { name: 'toolCalls', type: 'Promise<TypedToolCall<TOOLS>[]>', description: 'The tool calls that have been executed. Resolved when the response is finished.', }, { name: 'toolResults', type: 'Promise<TypedToolResult<TOOLS>[]>', description: 'The tool results that have been generated. Resolved when the all tool executions are finished.', }, { name: 'request', type: 'Promise<LanguageModelRequestMetadata>', description: 'Additional request information from the last step.', properties: [ { type: 'LanguageModelRequestMetadata', parameters: [ { name: 'body', type: 'string', description: 'Raw request HTTP body that was sent to the provider API as a string (JSON should be stringified).', }, ], }, ], }, { name: 'response', type: 'Promise<LanguageModelResponseMetadata & { messages: Array<ResponseMessage>; }>', description: 'Additional response information from the last step.', properties: [ { type: 'LanguageModelResponseMetadata', parameters: [ { name: 'id', type: 'string', description: 'The response identifier. The AI SDK uses the ID from the provider response when available, and generates an ID otherwise.', }, { name: 'model', type: 'string', description: 'The model that was used to generate the response. The AI SDK uses the response model from the provider response when available, and the model from the function call otherwise.', }, { name: 'timestamp', type: 'Date', description: 'The timestamp of the response. The AI SDK uses the response timestamp from the provider response when available, and creates a timestamp otherwise.', }, { name: 'headers', isOptional: true, type: 'Record<string, string>', description: 'Optional response headers.', }, { name: 'messages', type: 'Array<ResponseMessage>', description: 'The response messages that were generated during the call. It consists of an assistant message, potentially containing tool calls. When there are tool results, there is an additional tool message with the tool results that are available. If there are tools that do not have execute functions, they are not included in the tool results and need to be added separately.', }, ], }, ], }, { name: 'warnings', type: 'Promise<CallWarning[] | undefined>', description: 'Warnings from the model provider (e.g. unsupported settings) for the first step.', }, { name: 'steps', type: 'Promise<Array<StepResult>>', description: 'Response information for every step. You can use this to get information about intermediate steps, such as the tool calls or the response headers.', properties: [ { type: 'StepResult', parameters: [ { name: 'stepType', type: '"initial" | "continue" | "tool-result"', description: 'The type of step. The first step is always an "initial" step, and subsequent steps are either "continue" steps or "tool-result" steps.', }, { name: 'text', type: 'string', description: 'The generated text by the model.', }, { name: 'reasoning', type: 'string | undefined', description: 'The reasoning text of the model (only available for some models).', }, { name: 'sources', type: 'Array<Source>', description: 'Sources that have been used as input.', properties: [ { type: 'Source', parameters: [ { name: 'sourceType', type: "'url'", description: 'A URL source. This is return by web search RAG models.', }, { name: 'id', type: 'string', description: 'The ID of the source.', }, { name: 'url', type: 'string', description: 'The URL of the source.', }, { name: 'title', type: 'string', isOptional: true, description: 'The title of the source.', }, { name: 'providerMetadata', type: 'SharedV2ProviderMetadata', isOptional: true, description: 'Additional provider metadata for the source.', }, ], }, ], }, { name: 'files', type: 'Array<GeneratedFile>', description: 'Files that were generated in this step.', properties: [ { type: 'GeneratedFile', parameters: [ { name: 'base64', type: 'string', description: 'File as a base64 encoded string.', }, { name: 'uint8Array', type: 'Uint8Array', description: 'File as a Uint8Array.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the file.', }, ], }, ], }, { name: 'toolCalls', type: 'array', description: 'A list of tool calls made by the model.', }, { name: 'toolResults', type: 'array', description: 'A list of tool results returned as responses to earlier tool calls.', }, { name: 'finishReason', type: "'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error' | 'other' | 'unknown'", description: 'The reason the model finished generating the text.', }, { name: 'usage', type: 'LanguageModelUsage', description: 'The token usage of the generated text.', properties: [ { type: 'LanguageModelUsage', parameters: [ { name: 'inputTokens', type: 'number', description: 'The total number of tokens in the prompt.', }, { name: 'outputTokens', type: 'number', description: 'The total number of tokens in the completion.', }, { name: 'totalTokens', type: 'number', description: 'The total number of tokens generated.', }, ], }, ], }, { name: 'request', type: 'RequestMetadata', isOptional: true, description: 'Request metadata.', properties: [ { type: 'RequestMetadata', parameters: [ { name: 'body', type: 'string', description: 'Raw request HTTP body that was sent to the provider API as a string (JSON should be stringified).', }, ], }, ], }, { name: 'response', type: 'ResponseMetadata', isOptional: true, description: 'Response metadata.', properties: [ { type: 'ResponseMetadata', parameters: [ { name: 'id', type: 'string', description: 'The response identifier. The AI SDK uses the ID from the provider response when available, and generates an ID otherwise.', }, { name: 'model', type: 'string', description: 'The model that was used to generate the response. The AI SDK uses the response model from the provider response when available, and the model from the function call otherwise.', }, { name: 'timestamp', type: 'Date', description: 'The timestamp of the response. The AI SDK uses the response timestamp from the provider response when available, and creates a timestamp otherwise.', }, { name: 'headers', isOptional: true, type: 'Record<string, string>', description: 'Optional response headers.', }, { name: 'messages', type: 'Array<ResponseMessage>', description: 'The response messages that were generated during the call. It consists of an assistant message, potentially containing tool calls. When there are tool results, there is an additional tool message with the tool results that are available. If there are tools that do not have execute functions, they are not included in the tool results and need to be added separately.', }, ], }, ], }, { name: 'warnings', type: 'Warning[] | undefined', description: 'Warnings from the model provider (e.g. unsupported settings).', }, { name: 'isContinued', type: 'boolean', description: 'True when there will be a continuation step with a continuation text.', }, { name: 'providerMetadata', type: 'Record<string,Record<string,JSONValue>> | undefined', isOptional: true, description: 'Optional metadata from the provider. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.', }, ], }, ], }, { name: 'textStream', type: 'AsyncIterableStream<string>', description: 'A text stream that returns only the generated text deltas. You can use it as either an AsyncIterable or a ReadableStream. When an error occurs, the stream will throw the error.', }, { name: 'fullStream', type: 'AsyncIterable<TextStreamPart<TOOLS>> & ReadableStream<TextStreamPart<TOOLS>>', description: 'A stream with all events, including text deltas, tool calls, tool results, and errors. You can use it as either an AsyncIterable or a ReadableStream. Only errors that stop the stream, such as network errors, are thrown.', properties: [ { type: 'TextStreamPart', description: 'Text content part from ContentPart<TOOLS>', parameters: [ { name: 'type', type: "'text'", description: 'The type to identify the object as text.', }, { name: 'text', type: 'string', description: 'The text content.', }, ], }, { type: 'TextStreamPart', description: 'Reasoning content part from ContentPart<TOOLS>', parameters: [ { name: 'type', type: "'reasoning'", description: 'The type to identify the object as reasoning.', }, { name: 'text', type: 'string', description: 'The reasoning text.', }, { name: 'providerMetadata', type: 'ProviderMetadata', isOptional: true, description: 'Optional provider metadata for the reasoning.', }, ], }, { type: 'TextStreamPart', description: 'Source content part from ContentPart<TOOLS>', parameters: [ { name: 'type', type: "'source'", description: 'The type to identify the object as source.', }, { name: 'sourceType', type: "'url'", description: 'A URL source. This is returned by web search RAG models.', }, { name: 'id', type: 'string', description: 'The ID of the source.', }, { name: 'url', type: 'string', description: 'The URL of the source.', }, { name: 'title', type: 'string', isOptional: true, description: 'The title of the source.', }, { name: 'providerMetadata', type: 'ProviderMetadata', isOptional: true, description: 'Additional provider metadata for the source.', }, ], }, { type: 'TextStreamPart', description: 'File content part from ContentPart<TOOLS>', parameters: [ { name: 'type', type: "'file'", description: 'The type to identify the object as file.', }, { name: 'file', type: 'GeneratedFile', description: 'The file.', properties: [ { type: 'GeneratedFile', parameters: [ { name: 'base64', type: 'string', description: 'File as a base64 encoded string.', }, { name: 'uint8Array', type: 'Uint8Array', description: 'File as a Uint8Array.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the file.', }, ], }, ], }, ], }, { type: 'TextStreamPart', description: 'Tool call from ContentPart<TOOLS>', parameters: [ { name: 'type', type: "'tool-call'", description: 'The type to identify the object as tool call.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call.', }, { name: 'toolName', type: 'string', description: 'The name of the tool, which typically would be the name of the function.', }, { name: 'input', type: 'object based on tool parameters', description: 'Parameters generated by the model to be used by the tool. The type is inferred from the tool definition.', }, ], }, { type: 'TextStreamPart', parameters: [ { name: 'type', type: "'tool-call-streaming-start'", description: 'Indicates the start of a tool call streaming. Only available when streaming tool calls.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call.', }, { name: 'toolName', type: 'string', description: 'The name of the tool, which typically would be the name of the function.', }, ], }, { type: 'TextStreamPart', parameters: [ { name: 'type', type: "'tool-call-delta'", description: 'The type to identify the object as tool call delta. Only available when streaming tool calls.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call.', }, { name: 'toolName', type: 'string', description: 'The name of the tool, which typically would be the name of the function.', }, { name: 'argsTextDelta', type: 'string', description: 'The text delta of the tool call arguments.', }, ], }, { type: 'TextStreamPart', description: 'Tool result from ContentPart<TOOLS>', parameters: [ { name: 'type', type: "'tool-result'", description: 'The type to identify the object as tool result.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call.', }, { name: 'toolName', type: 'string', description: 'The name of the tool, which typically would be the name of the function.', }, { name: 'input', type: 'object based on tool parameters', description: 'Parameters that were passed to the tool. The type is inferred from the tool definition.', }, { name: 'output', type: 'tool execution return type', description: 'The result returned by the tool after execution has completed. The type is inferred from the tool execute function return type.', }, ], }, { type: 'TextStreamPart', parameters: [ { name: 'type', type: "'start-step'", description: 'Indicates the start of a new step in the stream.', }, { name: 'request', type: 'LanguageModelRequestMetadata', description: 'Information about the request that was sent to the language model provider.', properties: [ { type: 'LanguageModelRequestMetadata', parameters: [ { name: 'body', type: 'string', description: 'Raw request HTTP body that was sent to the provider API as a string.', }, ], }, ], }, { name: 'warnings', type: 'CallWarning[]', description: 'Warnings from the model provider (e.g. unsupported settings).', }, ], }, { type: 'TextStreamPart', parameters: [ { name: 'type', type: "'finish-step'", description: 'Indicates the end of the current step in the stream.', }, { name: 'response', type: 'LanguageModelResponseMetadata', description: 'Response metadata from the language model provider.', properties: [ { type: 'LanguageModelResponseMetadata', parameters: [ { name: 'id', type: 'string', description: 'The response identifier. The AI SDK uses the ID from the provider response when available, and generates an ID otherwise.', }, { name: 'model', type: 'string', description: 'The model that was used to generate the response. The AI SDK uses the response model from the provider response when available, and the model from the function call otherwise.', }, { name: 'timestamp', type: 'Date', description: 'The timestamp of the response. The AI SDK uses the response timestamp from the provider response when available, and creates a timestamp otherwise.', }, { name: 'headers', type: 'Record<string, string>', description: 'The response headers.', }, ], }, ], }, { name: 'usage', type: 'LanguageModelUsage', description: 'The token usage of the generated text.', properties: [ { type: 'LanguageModelUsage', parameters: [ { name: 'inputTokens', type: 'number', description: 'The total number of tokens in the prompt.', }, { name: 'outputTokens', type: 'number', description: 'The total number of tokens in the completion.', }, { name: 'totalTokens', type: 'number', description: 'The total number of tokens generated.', }, ], }, ], }, { name: 'finishReason', type: "'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error' | 'other' | 'unknown'", description: 'The reason the model finished generating the text.', }, { name: 'providerMetadata', type: 'ProviderMetadata | undefined', isOptional: true, description: 'Optional metadata from the provider. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.', }, ], }, { type: 'TextStreamPart', parameters: [ { name: 'type', type: "'start'", description: 'Indicates the start of the stream.', }, ], }, { type: 'TextStreamPart', parameters: [ { name: 'type', type: "'finish'", description: 'The type to identify the object as finish.', }, { name: 'finishReason', type: "'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error' | 'other' | 'unknown'", description: 'The reason the model finished generating the text.', }, { name: 'totalUsage', type: 'LanguageModelUsage', description: 'The total token usage of the generated text.', properties: [ { type: 'LanguageModelUsage', parameters: [ { name: 'inputTokens', type: 'number', description: 'The total number of tokens in the prompt.', }, { name: 'outputTokens', type: 'number', description: 'The total number of tokens in the completion.', }, { name: 'totalTokens', type: 'number', description: 'The total number of tokens generated.', }, ], }, ], }, ], }, { type: 'TextStreamPart', parameters: [ { name: 'type', type: "'reasoning-part-finish'", description: 'Indicates the end of a reasoning part.', }, ], }, { type: 'TextStreamPart', parameters: [ { name: 'type', type: "'error'", description: 'The type to identify the object as error.', }, { name: 'error', type: 'unknown', description: 'Describes the error that may have occurred during execution.', }, ], }, { type: 'TextStreamPart', parameters: [ { name: 'type', type: "'abort'", description: 'The type to identify the object as abort.', }, ], }, ], }, { name: 'experimental_partialOutputStream', type: 'AsyncIterableStream<PARTIAL_OUTPUT>', description: 'A stream of partial outputs. It uses the `experimental_output` specification. AsyncIterableStream is defined as AsyncIterable<T> & ReadableStream<T>.', }, { name: 'consumeStream', type: '(options?: ConsumeStreamOptions) => Promise<void>', description: 'Consumes the stream without processing the parts. This is useful to force the stream to finish. If an error occurs, it is passed to the optional `onError` callback.', properties: [ { type: 'ConsumeStreamOptions', parameters: [ { name: 'onError', type: '(error: unknown) => void', isOptional: true, description: 'The error callback.', }, ], }, ], }, { name: 'toUIMessageStream', type: '(options?: UIMessageStreamOptions) => AsyncIterableStream<UIMessageChunk>', description: 'Converts the result to a UI message stream. Returns an AsyncIterableStream that can be used as both an AsyncIterable and a ReadableStream.', properties: [ { type: 'UIMessageStreamOptions', parameters: [ { name: 'originalMessages', type: 'UIMessage[]', isOptional: true, description: 'The original messages.', }, { name: 'onFinish', type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; }) => void', isOptional: true, description: 'Callback function called when the stream finishes. Provides the updated list of UI messages, whether the response is a continuation, the response message, and whether the stream was aborted.', }, { name: 'messageMetadata', type: '(options: { part: TextStreamPart<TOOLS> & { type: "start" | "finish" | "start-step" | "finish-step"; }; }) => unknown', isOptional: true, description: 'Extracts message metadata that will be sent to the client. Called on start and finish events.', }, { name: 'sendReasoning', type: 'boolean', isOptional: true, description: 'Send reasoning parts to the client. Defaults to false.', }, { name: 'sendSources', type: 'boolean', isOptional: true, description: 'Send source parts to the client. Defaults to false.', }, { name: 'sendFinish', type: 'boolean', isOptional: true, description: 'Send the finish event to the client. Defaults to true.', }, { name: 'sendStart', type: 'boolean', isOptional: true, description: 'Send the message start event to the client. Set to false if you are using additional streamText calls and the message start event has already been sent. Defaults to true.', }, { name: 'onError', type: '(error: unknown) => string', isOptional: true, description: 'Process an error, e.g. to log it. Returns error message to include in the data stream. Defaults to () => "An error occurred."', }, { name: 'consumeSseStream', type: '(stream: ReadableStream) => Promise<void>', isOptional: true, description: 'Function to consume the SSE stream. Required for proper abort handling in UI message streams. Use the `consumeStream` function from the AI SDK.', }, ], }, ], }, { name: 'pipeUIMessageStreamToResponse', type: '(response: ServerResponse, options?: ResponseInit & UIMessageStreamOptions) => void', description: 'Writes UI message stream output to a Node.js response-like object.', properties: [ { type: 'ResponseInit & UIMessageStreamOptions', parameters: [ { name: 'status', type: 'number', isOptional: true, description: 'The response status code.', }, { name: 'statusText', type: 'string', isOptional: true, description: 'The response status text.', }, { name: 'headers', type: 'HeadersInit', isOptional: true, description: 'The response headers.', }, ], }, ], }, { name: 'pipeTextStreamToResponse', type: '(response: ServerResponse, init?: ResponseInit) => void', description: 'Writes text delta output to a Node.js response-like object. It sets a `Content-Type` header to `text/plain; charset=utf-8` and writes each text delta as a separate chunk.', properties: [ { type: 'ResponseInit', parameters: [ { name: 'status', type: 'number', isOptional: true, description: 'The response status code.', }, { name: 'statusText', type: 'string', isOptional: true, description: 'The response status text.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'The response headers.', }, ], }, ], }, { name: 'toUIMessageStreamResponse', type: '(options?: ResponseInit & UIMessageStreamOptions) => Response', description: 'Converts the result to a streamed response object with a UI message stream.', properties: [ { type: 'ResponseInit & UIMessageStreamOptions', parameters: [ { name: 'status', type: 'number', isOptional: true, description: 'The response status code.', }, { name: 'statusText', type: 'string', isOptional: true, description: 'The response status text.', }, { name: 'headers', type: 'HeadersInit', isOptional: true, description: 'The response headers.', }, ], }, ], }, { name: 'toTextStreamResponse', type: '(init?: ResponseInit) => Response', description: 'Creates a simple text stream response. Each text delta is encoded as UTF-8 and sent as a separate chunk. Non-text-delta events are ignored.', properties: [ { type: 'ResponseInit', parameters: [ { name: 'status', type: 'number', isOptional: true, description: 'The response status code.', }, { name: 'statusText', type: 'string', isOptional: true, description: 'The response status text.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'The response headers.', }, ], }, ], }, ]} /> ## Examples <ExampleLinks examples={[ { title: 'Learn to stream text generated by a language model in Next.js', link: '/examples/next-app/basics/streaming-text-generation', }, { title: 'Learn to stream chat completions generated by a language model in Next.js', link: '/examples/next-app/chat/stream-chat-completion', }, { title: 'Learn to stream text generated by a language model in Node.js', link: '/examples/node/generating-text/stream-text', }, { title: 'Learn to stream chat completions generated by a language model in Node.js', link: '/examples/node/generating-text/stream-text-with-chat-prompt', }, ]} /> --- File: /ai/content/docs/07-reference/01-ai-sdk-core/03-generate-object.mdx --- --- title: generateObject description: API Reference for generateObject. --- # `generateObject()` Generates a typed, structured object for a given prompt and schema using a language model. It can be used to force the language model to return structured data, e.g. for information extraction, synthetic data generation, or classification tasks. #### Example: generate an object using a schema ```ts import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import { z } from 'zod'; const { object } = await generateObject({ model: openai('gpt-4.1'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.string()), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(object, null, 2)); ``` #### Example: generate an array using a schema For arrays, you specify the schema of the array items. ```ts highlight="7" import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import { z } from 'zod'; const { object } = await generateObject({ model: openai('gpt-4.1'), output: 'array', schema: z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), prompt: 'Generate 3 hero descriptions for a fantasy role playing game.', }); ``` #### Example: generate an enum When you want to generate a specific enum value, you can set the output strategy to `enum` and provide the list of possible values in the `enum` parameter. ```ts highlight="5-6" import { generateObject } from 'ai'; const { object } = await generateObject({ model: 'openai/gpt-4.1', output: 'enum', enum: ['action', 'comedy', 'drama', 'horror', 'sci-fi'], prompt: 'Classify the genre of this movie plot: ' + '"A group of astronauts travel through a wormhole in search of a ' + 'new habitable planet for humanity."', }); ``` #### Example: generate JSON without a schema ```ts highlight="6" import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; const { object } = await generateObject({ model: openai('gpt-4.1'), output: 'no-schema', prompt: 'Generate a lasagna recipe.', }); ``` To see `generateObject` in action, check out the [additional examples](#more-examples). ## Import <Snippet text={`import { generateObject } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'model', type: 'LanguageModel', description: "The language model to use. Example: openai('gpt-4.1')", }, { name: 'output', type: "'object' | 'array' | 'enum' | 'no-schema' | undefined", description: "The type of output to generate. Defaults to 'object'.", }, { name: 'mode', type: "'auto' | 'json' | 'tool'", description: "The mode to use for object generation. Not every model supports all modes. \ Defaults to 'auto' for 'object' output and to 'json' for 'no-schema' output. \ Must be 'json' for 'no-schema' output.", }, { name: 'schema', type: 'Zod Schema | JSON Schema', description: "The schema that describes the shape of the object to generate. \ It is sent to the model to generate the object and used to validate the output. \ You can either pass in a Zod schema or a JSON schema (using the `jsonSchema` function). \ In 'array' mode, the schema is used to describe an array element. \ Not available with 'no-schema' or 'enum' output.", }, { name: 'schemaName', type: 'string | undefined', description: "Optional name of the output that should be generated. \ Used by some providers for additional LLM guidance, e.g. via tool or schema name. \ Not available with 'no-schema' or 'enum' output.", }, { name: 'schemaDescription', type: 'string | undefined', description: "Optional description of the output that should be generated. \ Used by some providers for additional LLM guidance, e.g. via tool or schema name. \ Not available with 'no-schema' or 'enum' output.", }, { name: 'enum', type: 'string[]', description: "List of possible values to generate. \ Only available with 'enum' output.", }, { name: 'system', type: 'string', description: 'The system prompt to use that specifies the behavior of the model.', }, { name: 'prompt', type: 'string', description: 'The input prompt to generate the text from.', }, { name: 'messages', type: 'Array<SystemModelMessage | UserModelMessage | AssistantModelMessage | ToolModelMessage> | Array<UIMessage>', description: 'A list of messages that represent a conversation. Automatically converts UI messages from the useChat hook.', properties: [ { type: 'SystemModelMessage', parameters: [ { name: 'role', type: "'system'", description: 'The role for the system message.', }, { name: 'content', type: 'string', description: 'The content of the message.', }, ], }, { type: 'UserModelMessage', parameters: [ { name: 'role', type: "'user'", description: 'The role for the user message.', }, { name: 'content', type: 'string | Array<TextPart | ImagePart | FilePart>', description: 'The content of the message.', properties: [ { type: 'TextPart', parameters: [ { name: 'type', type: "'text'", description: 'The type of the message part.', }, { name: 'text', type: 'string', description: 'The text content of the message part.', }, ], }, { type: 'ImagePart', parameters: [ { name: 'type', type: "'image'", description: 'The type of the message part.', }, { name: 'image', type: 'string | Uint8Array | Buffer | ArrayBuffer | URL', description: 'The image content of the message part. String are either base64 encoded content, base64 data URLs, or http(s) URLs.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the image. Optional.', isOptional: true, }, ], }, { type: 'FilePart', parameters: [ { name: 'type', type: "'file'", description: 'The type of the message part.', }, { name: 'data', type: 'string | Uint8Array | Buffer | ArrayBuffer | URL', description: 'The file content of the message part. String are either base64 encoded content, base64 data URLs, or http(s) URLs.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the file.', }, ], }, ], }, ], }, { type: 'AssistantModelMessage', parameters: [ { name: 'role', type: "'assistant'", description: 'The role for the assistant message.', }, { name: 'content', type: 'string | Array<TextPart | FilePart | ReasoningPart | ToolCallPart>', description: 'The content of the message.', properties: [ { type: 'TextPart', parameters: [ { name: 'type', type: "'text'", description: 'The type of the message part.', }, { name: 'text', type: 'string', description: 'The text content of the message part.', }, ], }, { type: 'ReasoningPart', parameters: [ { name: 'type', type: "'reasoning'", description: 'The type of the message part.', }, { name: 'text', type: 'string', description: 'The reasoning text.', }, ], }, { type: 'FilePart', parameters: [ { name: 'type', type: "'file'", description: 'The type of the message part.', }, { name: 'data', type: 'string | Uint8Array | Buffer | ArrayBuffer | URL', description: 'The file content of the message part. String are either base64 encoded content, base64 data URLs, or http(s) URLs.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the file.', }, { name: 'filename', type: 'string', description: 'The name of the file.', isOptional: true, }, ], }, { type: 'ToolCallPart', parameters: [ { name: 'type', type: "'tool-call'", description: 'The type of the message part.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call.', }, { name: 'toolName', type: 'string', description: 'The name of the tool, which typically would be the name of the function.', }, { name: 'args', type: 'object based on zod schema', description: 'Parameters generated by the model to be used by the tool.', }, ], }, ], }, ], }, { type: 'ToolModelMessage', parameters: [ { name: 'role', type: "'tool'", description: 'The role for the assistant message.', }, { name: 'content', type: 'Array<ToolResultPart>', description: 'The content of the message.', properties: [ { type: 'ToolResultPart', parameters: [ { name: 'type', type: "'tool-result'", description: 'The type of the message part.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call the result corresponds to.', }, { name: 'toolName', type: 'string', description: 'The name of the tool the result corresponds to.', }, { name: 'result', type: 'unknown', description: 'The result returned by the tool after execution.', }, { name: 'isError', type: 'boolean', isOptional: true, description: 'Whether the result is an error or an error message.', }, ], }, ], }, ], }, ], }, { name: 'maxOutputTokens', type: 'number', isOptional: true, description: 'Maximum number of tokens to generate.', }, { name: 'temperature', type: 'number', isOptional: true, description: 'Temperature setting. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both.', }, { name: 'topP', type: 'number', isOptional: true, description: 'Nucleus sampling. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both.', }, { name: 'topK', type: 'number', isOptional: true, description: 'Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature.', }, { name: 'presencePenalty', type: 'number', isOptional: true, description: 'Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The value is passed through to the provider. The range depends on the provider and model.', }, { name: 'frequencyPenalty', type: 'number', isOptional: true, description: 'Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The value is passed through to the provider. The range depends on the provider and model.', }, { name: 'seed', type: 'number', isOptional: true, description: 'The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results.', }, { name: 'maxRetries', type: 'number', isOptional: true, description: 'Maximum number of retries. Set to 0 to disable retries. Default: 2.', }, { name: 'abortSignal', type: 'AbortSignal', isOptional: true, description: 'An optional abort signal that can be used to cancel the call.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.', }, { name: 'experimental_repairText', type: '(options: RepairTextOptions) => Promise<string>', isOptional: true, description: 'A function that attempts to repair the raw output of the model to enable JSON parsing. Should return the repaired text or null if the text cannot be repaired.', properties: [ { type: 'RepairTextOptions', parameters: [ { name: 'text', type: 'string', description: 'The text that was generated by the model.', }, { name: 'error', type: 'JSONParseError | TypeValidationError', description: 'The error that occurred while parsing the text.', }, ], }, ], }, { name: 'experimental_telemetry', type: 'TelemetrySettings', isOptional: true, description: 'Telemetry configuration. Experimental feature.', properties: [ { type: 'TelemetrySettings', parameters: [ { name: 'isEnabled', type: 'boolean', isOptional: true, description: 'Enable or disable telemetry. Disabled by default while experimental.', }, { name: 'recordInputs', type: 'boolean', isOptional: true, description: 'Enable or disable input recording. Enabled by default.', }, { name: 'recordOutputs', type: 'boolean', isOptional: true, description: 'Enable or disable output recording. Enabled by default.', }, { name: 'functionId', type: 'string', isOptional: true, description: 'Identifier for this function. Used to group telemetry data by function.', }, { name: 'metadata', isOptional: true, type: 'Record<string, string | number | boolean | Array<null | undefined | string> | Array<null | undefined | number> | Array<null | undefined | boolean>>', description: 'Additional information to include in the telemetry data.', }, ], }, ], }, { name: 'providerOptions', type: 'Record<string,Record<string,JSONValue>> | undefined', isOptional: true, description: 'Provider-specific options. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.', }, ]} /> ### Returns <PropertiesTable content={[ { name: 'object', type: 'based on the schema', description: 'The generated object, validated by the schema (if it supports validation).', }, { name: 'finishReason', type: "'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error' | 'other' | 'unknown'", description: 'The reason the model finished generating the text.', }, { name: 'usage', type: 'LanguageModelUsage', description: 'The token usage of the generated text.', properties: [ { type: 'LanguageModelUsage', parameters: [ { name: 'inputTokens', type: 'number | undefined', description: 'The number of input (prompt) tokens used.', }, { name: 'outputTokens', type: 'number | undefined', description: 'The number of output (completion) tokens used.', }, { name: 'totalTokens', type: 'number | undefined', description: 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', }, { name: 'reasoningTokens', type: 'number | undefined', isOptional: true, description: 'The number of reasoning tokens used.', }, { name: 'cachedInputTokens', type: 'number | undefined', isOptional: true, description: 'The number of cached input tokens.', }, ], }, ], }, { name: 'request', type: 'LanguageModelRequestMetadata', isOptional: true, description: 'Request metadata.', properties: [ { type: 'LanguageModelRequestMetadata', parameters: [ { name: 'body', type: 'string', description: 'Raw request HTTP body that was sent to the provider API as a string (JSON should be stringified).', }, ], }, ], }, { name: 'response', type: 'LanguageModelResponseMetadata', isOptional: true, description: 'Response metadata.', properties: [ { type: 'LanguageModelResponseMetadata', parameters: [ { name: 'id', type: 'string', description: 'The response identifier. The AI SDK uses the ID from the provider response when available, and generates an ID otherwise.', }, { name: 'modelId', type: 'string', description: 'The model that was used to generate the response. The AI SDK uses the response model from the provider response when available, and the model from the function call otherwise.', }, { name: 'timestamp', type: 'Date', description: 'The timestamp of the response. The AI SDK uses the response timestamp from the provider response when available, and creates a timestamp otherwise.', }, { name: 'headers', isOptional: true, type: 'Record<string, string>', description: 'Optional response headers.', }, { name: 'body', isOptional: true, type: 'unknown', description: 'Optional response body.', }, ], }, ], }, { name: 'warnings', type: 'CallWarning[] | undefined', description: 'Warnings from the model provider (e.g. unsupported settings).', }, { name: 'providerMetadata', type: 'ProviderMetadata | undefined', description: 'Optional metadata from the provider. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.', }, { name: 'toJsonResponse', type: '(init?: ResponseInit) => Response', description: 'Converts the object to a JSON response. The response will have a status code of 200 and a content type of `application/json; charset=utf-8`.', }, ]} /> ## More Examples <ExampleLinks examples={[ { title: 'Learn to generate structured data using a language model in Next.js', link: '/examples/next-app/basics/generating-object', }, { title: 'Learn to generate structured data using a language model in Node.js', link: '/examples/node/generating-structured-data/generate-object', }, ]} /> --- File: /ai/content/docs/07-reference/01-ai-sdk-core/04-stream-object.mdx --- --- title: streamObject description: API Reference for streamObject --- # `streamObject()` Streams a typed, structured object for a given prompt and schema using a language model. It can be used to force the language model to return structured data, e.g. for information extraction, synthetic data generation, or classification tasks. #### Example: stream an object using a schema ```ts import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import { z } from 'zod'; const { partialObjectStream } = streamObject({ model: openai('gpt-4.1'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.string()), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); for await (const partialObject of partialObjectStream) { console.clear(); console.log(partialObject); } ``` #### Example: stream an array using a schema For arrays, you specify the schema of the array items. You can use `elementStream` to get the stream of complete array elements. ```ts highlight="7,18" import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import { z } from 'zod'; const { elementStream } = streamObject({ model: openai('gpt-4.1'), output: 'array', schema: z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), prompt: 'Generate 3 hero descriptions for a fantasy role playing game.', }); for await (const hero of elementStream) { console.log(hero); } ``` #### Example: generate JSON without a schema ```ts import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; const { partialObjectStream } = streamObject({ model: openai('gpt-4.1'), output: 'no-schema', prompt: 'Generate a lasagna recipe.', }); for await (const partialObject of partialObjectStream) { console.clear(); console.log(partialObject); } ``` #### Example: generate an enum When you want to generate a specific enum value, you can set the output strategy to `enum` and provide the list of possible values in the `enum` parameter. ```ts highlight="5-6" import { streamObject } from 'ai'; const { partialObjectStream } = streamObject({ model: 'openai/gpt-4.1', output: 'enum', enum: ['action', 'comedy', 'drama', 'horror', 'sci-fi'], prompt: 'Classify the genre of this movie plot: ' + '"A group of astronauts travel through a wormhole in search of a ' + 'new habitable planet for humanity."', }); ``` To see `streamObject` in action, check out the [additional examples](#more-examples). ## Import <Snippet text={`import { streamObject } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'model', type: 'LanguageModel', description: "The language model to use. Example: openai('gpt-4.1')", }, { name: 'output', type: "'object' | 'array' | 'enum' | 'no-schema' | undefined", description: "The type of output to generate. Defaults to 'object'.", }, { name: 'mode', type: "'auto' | 'json' | 'tool'", description: "The mode to use for object generation. Not every model supports all modes. \ Defaults to 'auto' for 'object' output and to 'json' for 'no-schema' output. \ Must be 'json' for 'no-schema' output.", }, { name: 'schema', type: 'Zod Schema | JSON Schema', description: "The schema that describes the shape of the object to generate. \ It is sent to the model to generate the object and used to validate the output. \ You can either pass in a Zod schema or a JSON schema (using the `jsonSchema` function). \ In 'array' mode, the schema is used to describe an array element. \ Not available with 'no-schema' or 'enum' output.", }, { name: 'schemaName', type: 'string | undefined', description: "Optional name of the output that should be generated. \ Used by some providers for additional LLM guidance, e.g. via tool or schema name. \ Not available with 'no-schema' or 'enum' output.", }, { name: 'schemaDescription', type: 'string | undefined', description: "Optional description of the output that should be generated. \ Used by some providers for additional LLM guidance, e.g. via tool or schema name. \ Not available with 'no-schema' or 'enum' output.", }, { name: 'system', type: 'string', description: 'The system prompt to use that specifies the behavior of the model.', }, { name: 'prompt', type: 'string', description: 'The input prompt to generate the text from.', }, { name: 'messages', type: 'Array<SystemModelMessage | UserModelMessage | AssistantModelMessage | ToolModelMessage> | Array<UIMessage>', description: 'A list of messages that represent a conversation. Automatically converts UI messages from the useChat hook.', properties: [ { type: 'SystemModelMessage', parameters: [ { name: 'role', type: "'system'", description: 'The role for the system message.', }, { name: 'content', type: 'string', description: 'The content of the message.', }, ], }, { type: 'UserModelMessage', parameters: [ { name: 'role', type: "'user'", description: 'The role for the user message.', }, { name: 'content', type: 'string | Array<TextPart | ImagePart | FilePart>', description: 'The content of the message.', properties: [ { type: 'TextPart', parameters: [ { name: 'type', type: "'text'", description: 'The type of the message part.', }, { name: 'text', type: 'string', description: 'The text content of the message part.', }, ], }, { type: 'ImagePart', parameters: [ { name: 'type', type: "'image'", description: 'The type of the message part.', }, { name: 'image', type: 'string | Uint8Array | Buffer | ArrayBuffer | URL', description: 'The image content of the message part. String are either base64 encoded content, base64 data URLs, or http(s) URLs.', }, { name: 'mediaType', type: 'string', isOptional: true, description: 'The IANA media type of the image. Optional.', }, ], }, { type: 'FilePart', parameters: [ { name: 'type', type: "'file'", description: 'The type of the message part.', }, { name: 'data', type: 'string | Uint8Array | Buffer | ArrayBuffer | URL', description: 'The file content of the message part. String are either base64 encoded content, base64 data URLs, or http(s) URLs.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the file.', }, ], }, ], }, ], }, { type: 'AssistantModelMessage', parameters: [ { name: 'role', type: "'assistant'", description: 'The role for the assistant message.', }, { name: 'content', type: 'string | Array<TextPart | FilePart | ReasoningPart | ToolCallPart>', description: 'The content of the message.', properties: [ { type: 'TextPart', parameters: [ { name: 'type', type: "'text'", description: 'The type of the message part.', }, { name: 'text', type: 'string', description: 'The text content of the message part.', }, ], }, { type: 'ReasoningPart', parameters: [ { name: 'type', type: "'reasoning'", description: 'The type of the message part.', }, { name: 'text', type: 'string', description: 'The reasoning text.', }, ], }, { type: 'FilePart', parameters: [ { name: 'type', type: "'file'", description: 'The type of the message part.', }, { name: 'data', type: 'string | Uint8Array | Buffer | ArrayBuffer | URL', description: 'The file content of the message part. String are either base64 encoded content, base64 data URLs, or http(s) URLs.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the file.', }, { name: 'filename', type: 'string', description: 'The name of the file.', isOptional: true, }, ], }, { type: 'ToolCallPart', parameters: [ { name: 'type', type: "'tool-call'", description: 'The type of the message part.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call.', }, { name: 'toolName', type: 'string', description: 'The name of the tool, which typically would be the name of the function.', }, { name: 'args', type: 'object based on zod schema', description: 'Parameters generated by the model to be used by the tool.', }, ], }, ], }, ], }, { type: 'ToolModelMessage', parameters: [ { name: 'role', type: "'tool'", description: 'The role for the assistant message.', }, { name: 'content', type: 'Array<ToolResultPart>', description: 'The content of the message.', properties: [ { type: 'ToolResultPart', parameters: [ { name: 'type', type: "'tool-result'", description: 'The type of the message part.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call the result corresponds to.', }, { name: 'toolName', type: 'string', description: 'The name of the tool the result corresponds to.', }, { name: 'result', type: 'unknown', description: 'The result returned by the tool after execution.', }, { name: 'isError', type: 'boolean', isOptional: true, description: 'Whether the result is an error or an error message.', }, ], }, ], }, ], }, ], }, { name: 'maxOutputTokens', type: 'number', isOptional: true, description: 'Maximum number of tokens to generate.', }, { name: 'temperature', type: 'number', isOptional: true, description: 'Temperature setting. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both.', }, { name: 'topP', type: 'number', isOptional: true, description: 'Nucleus sampling. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both.', }, { name: 'topK', type: 'number', isOptional: true, description: 'Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature.', }, { name: 'presencePenalty', type: 'number', isOptional: true, description: 'Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The value is passed through to the provider. The range depends on the provider and model.', }, { name: 'frequencyPenalty', type: 'number', isOptional: true, description: 'Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The value is passed through to the provider. The range depends on the provider and model.', }, { name: 'seed', type: 'number', isOptional: true, description: 'The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results.', }, { name: 'maxRetries', type: 'number', isOptional: true, description: 'Maximum number of retries. Set to 0 to disable retries. Default: 2.', }, { name: 'abortSignal', type: 'AbortSignal', isOptional: true, description: 'An optional abort signal that can be used to cancel the call.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.', }, { name: 'experimental_repairText', type: '(options: RepairTextOptions) => Promise<string>', isOptional: true, description: 'A function that attempts to repair the raw output of the model to enable JSON parsing. Should return the repaired text or null if the text cannot be repaired.', properties: [ { type: 'RepairTextOptions', parameters: [ { name: 'text', type: 'string', description: 'The text that was generated by the model.', }, { name: 'error', type: 'JSONParseError | TypeValidationError', description: 'The error that occurred while parsing the text.', }, ], }, ], }, { name: 'experimental_telemetry', type: 'TelemetrySettings', isOptional: true, description: 'Telemetry configuration. Experimental feature.', properties: [ { type: 'TelemetrySettings', parameters: [ { name: 'isEnabled', type: 'boolean', isOptional: true, description: 'Enable or disable telemetry. Disabled by default while experimental.', }, { name: 'recordInputs', type: 'boolean', isOptional: true, description: 'Enable or disable input recording. Enabled by default.', }, { name: 'recordOutputs', type: 'boolean', isOptional: true, description: 'Enable or disable output recording. Enabled by default.', }, { name: 'functionId', type: 'string', isOptional: true, description: 'Identifier for this function. Used to group telemetry data by function.', }, { name: 'metadata', isOptional: true, type: 'Record<string, string | number | boolean | Array<null | undefined | string> | Array<null | undefined | number> | Array<null | undefined | boolean>>', description: 'Additional information to include in the telemetry data.', }, ], }, ], }, { name: 'providerOptions', type: 'Record<string,Record<string,JSONValue>> | undefined', isOptional: true, description: 'Provider-specific options. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.', }, { name: 'onError', type: '(event: OnErrorResult) => Promise<void> |void', isOptional: true, description: 'Callback that is called when an error occurs during streaming. You can use it to log errors.', properties: [ { type: 'OnErrorResult', parameters: [ { name: 'error', type: 'unknown', description: 'The error that occurred.', }, ], }, ], }, { name: 'onFinish', type: '(result: OnFinishResult) => void', isOptional: true, description: 'Callback that is called when the LLM response has finished.', properties: [ { type: 'OnFinishResult', parameters: [ { name: 'usage', type: 'LanguageModelUsage', description: 'The token usage of the generated text.', properties: [ { type: 'LanguageModelUsage', parameters: [ { name: 'inputTokens', type: 'number | undefined', description: 'The number of input (prompt) tokens used.', }, { name: 'outputTokens', type: 'number | undefined', description: 'The number of output (completion) tokens used.', }, { name: 'totalTokens', type: 'number | undefined', description: 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', }, { name: 'reasoningTokens', type: 'number | undefined', isOptional: true, description: 'The number of reasoning tokens used.', }, { name: 'cachedInputTokens', type: 'number | undefined', isOptional: true, description: 'The number of cached input tokens.', }, ], }, ], }, { name: 'providerMetadata', type: 'ProviderMetadata | undefined', description: 'Optional metadata from the provider. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.', }, { name: 'object', type: 'T | undefined', description: 'The generated object (typed according to the schema). Can be undefined if the final object does not match the schema.', }, { name: 'error', type: 'unknown | undefined', description: 'Optional error object. This is e.g. a TypeValidationError when the final object does not match the schema.', }, { name: 'warnings', type: 'CallWarning[] | undefined', description: 'Warnings from the model provider (e.g. unsupported settings).', }, { name: 'response', type: 'Response', isOptional: true, description: 'Response metadata.', properties: [ { type: 'Response', parameters: [ { name: 'id', type: 'string', description: 'The response identifier. The AI SDK uses the ID from the provider response when available, and generates an ID otherwise.', }, { name: 'model', type: 'string', description: 'The model that was used to generate the response. The AI SDK uses the response model from the provider response when available, and the model from the function call otherwise.', }, { name: 'timestamp', type: 'Date', description: 'The timestamp of the response. The AI SDK uses the response timestamp from the provider response when available, and creates a timestamp otherwise.', }, { name: 'headers', isOptional: true, type: 'Record<string, string>', description: 'Optional response headers.', }, ], }, ], }, ], }, ], }, ]} /> ### Returns <PropertiesTable content={[ { name: 'usage', type: 'Promise<LanguageModelUsage>', description: 'The token usage of the generated text. Resolved when the response is finished.', properties: [ { type: 'LanguageModelUsage', parameters: [ { name: 'inputTokens', type: 'number | undefined', description: 'The number of input (prompt) tokens used.', }, { name: 'outputTokens', type: 'number | undefined', description: 'The number of output (completion) tokens used.', }, { name: 'totalTokens', type: 'number | undefined', description: 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', }, { name: 'reasoningTokens', type: 'number | undefined', isOptional: true, description: 'The number of reasoning tokens used.', }, { name: 'cachedInputTokens', type: 'number | undefined', isOptional: true, description: 'The number of cached input tokens.', }, ], }, ], }, { name: 'providerMetadata', type: 'Promise<Record<string,Record<string,JSONValue>> | undefined>', description: 'Optional metadata from the provider. Resolved whe the response is finished. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.', }, { name: 'object', type: 'Promise<T>', description: 'The generated object (typed according to the schema). Resolved when the response is finished.', }, { name: 'partialObjectStream', type: 'AsyncIterableStream<DeepPartial<T>>', description: 'Stream of partial objects. It gets more complete as the stream progresses. Note that the partial object is not validated. If you want to be certain that the actual content matches your schema, you need to implement your own validation for partial results.', }, { name: 'elementStream', type: 'AsyncIterableStream<ELEMENT>', description: 'Stream of array elements. Only available in "array" mode.', }, { name: 'textStream', type: 'AsyncIterableStream<string>', description: 'Text stream of the JSON representation of the generated object. It contains text chunks. When the stream is finished, the object is valid JSON that can be parsed.', }, { name: 'fullStream', type: 'AsyncIterableStream<ObjectStreamPart<T>>', description: 'Stream of different types of events, including partial objects, errors, and finish events. Only errors that stop the stream, such as network errors, are thrown.', properties: [ { type: 'ObjectPart', parameters: [ { name: 'type', type: "'object'", }, { name: 'object', type: 'DeepPartial<T>', description: 'The partial object that was generated.', }, ], }, { type: 'TextDeltaPart', parameters: [ { name: 'type', type: "'text-delta'", }, { name: 'textDelta', type: 'string', description: 'The text delta for the underlying raw JSON text.', }, ], }, { type: 'ErrorPart', parameters: [ { name: 'type', type: "'error'", }, { name: 'error', type: 'unknown', description: 'The error that occurred.', }, ], }, { type: 'FinishPart', parameters: [ { name: 'type', type: "'finish'", }, { name: 'finishReason', type: 'FinishReason', }, { name: 'logprobs', type: 'Logprobs', isOptional: true, }, { name: 'usage', type: 'Usage', description: 'Token usage.', }, { name: 'response', type: 'Response', isOptional: true, description: 'Response metadata.', properties: [ { type: 'Response', parameters: [ { name: 'id', type: 'string', description: 'The response identifier. The AI SDK uses the ID from the provider response when available, and generates an ID otherwise.', }, { name: 'model', type: 'string', description: 'The model that was used to generate the response. The AI SDK uses the response model from the provider response when available, and the model from the function call otherwise.', }, { name: 'timestamp', type: 'Date', description: 'The timestamp of the response. The AI SDK uses the response timestamp from the provider response when available, and creates a timestamp otherwise.', }, ], }, ], }, ], }, ], }, { name: 'request', type: 'Promise<LanguageModelRequestMetadata>', description: 'Request metadata.', properties: [ { type: 'LanguageModelRequestMetadata', parameters: [ { name: 'body', type: 'string', description: 'Raw request HTTP body that was sent to the provider API as a string (JSON should be stringified).', }, ], }, ], }, { name: 'response', type: 'Promise<LanguageModelResponseMetadata>', description: 'Response metadata. Resolved when the response is finished.', properties: [ { type: 'LanguageModelResponseMetadata', parameters: [ { name: 'id', type: 'string', description: 'The response identifier. The AI SDK uses the ID from the provider response when available, and generates an ID otherwise.', }, { name: 'model', type: 'string', description: 'The model that was used to generate the response. The AI SDK uses the response model from the provider response when available, and the model from the function call otherwise.', }, { name: 'timestamp', type: 'Date', description: 'The timestamp of the response. The AI SDK uses the response timestamp from the provider response when available, and creates a timestamp otherwise.', }, { name: 'headers', isOptional: true, type: 'Record<string, string>', description: 'Optional response headers.', }, ], }, ], }, { name: 'warnings', type: 'CallWarning[] | undefined', description: 'Warnings from the model provider (e.g. unsupported settings).', }, { name: 'pipeTextStreamToResponse', type: '(response: ServerResponse, init?: ResponseInit => void', description: 'Writes text delta output to a Node.js response-like object. It sets a `Content-Type` header to `text/plain; charset=utf-8` and writes each text delta as a separate chunk.', properties: [ { type: 'ResponseInit', parameters: [ { name: 'status', type: 'number', isOptional: true, description: 'The response status code.', }, { name: 'statusText', type: 'string', isOptional: true, description: 'The response status text.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'The response headers.', }, ], }, ], }, { name: 'toTextStreamResponse', type: '(init?: ResponseInit) => Response', description: 'Creates a simple text stream response. Each text delta is encoded as UTF-8 and sent as a separate chunk. Non-text-delta events are ignored.', properties: [ { type: 'ResponseInit', parameters: [ { name: 'status', type: 'number', isOptional: true, description: 'The response status code.', }, { name: 'statusText', type: 'string', isOptional: true, description: 'The response status text.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'The response headers.', }, ], }, ], }, ]} /> ## More Examples <ExampleLinks examples={[ { title: 'Streaming Object Generation with RSC', link: '/examples/next-app/basics/streaming-object-generation', }, { title: 'Streaming Object Generation with useObject', link: '/examples/next-pages/basics/streaming-object-generation', }, { title: 'Streaming Partial Objects', link: '/examples/node/streaming-structured-data/stream-object', }, { title: 'Recording Token Usage', link: '/examples/node/streaming-structured-data/token-usage', }, { title: 'Recording Final Object', link: '/examples/node/streaming-structured-data/object', }, ]} /> --- File: /ai/content/docs/07-reference/01-ai-sdk-core/05-embed.mdx --- --- title: embed description: API Reference for embed. --- # `embed()` Generate an embedding for a single value using an embedding model. This is ideal for use cases where you need to embed a single value to e.g. retrieve similar items or to use the embedding in a downstream task. ```ts import { openai } from '@ai-sdk/openai'; import { embed } from 'ai'; const { embedding } = await embed({ model: openai.textEmbeddingModel('text-embedding-3-small'), value: 'sunny day at the beach', }); ``` ## Import <Snippet text={`import { embed } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'model', type: 'EmbeddingModel', description: "The embedding model to use. Example: openai.textEmbeddingModel('text-embedding-3-small')", }, { name: 'value', type: 'VALUE', description: 'The value to embed. The type depends on the model.', }, { name: 'maxRetries', type: 'number', isOptional: true, description: 'Maximum number of retries. Set to 0 to disable retries. Default: 2.', }, { name: 'abortSignal', type: 'AbortSignal', isOptional: true, description: 'An optional abort signal that can be used to cancel the call.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.', }, { name: 'experimental_telemetry', type: 'TelemetrySettings', isOptional: true, description: 'Telemetry configuration. Experimental feature.', properties: [ { type: 'TelemetrySettings', parameters: [ { name: 'isEnabled', type: 'boolean', isOptional: true, description: 'Enable or disable telemetry. Disabled by default while experimental.', }, { name: 'recordInputs', type: 'boolean', isOptional: true, description: 'Enable or disable input recording. Enabled by default.', }, { name: 'recordOutputs', type: 'boolean', isOptional: true, description: 'Enable or disable output recording. Enabled by default.', }, { name: 'functionId', type: 'string', isOptional: true, description: 'Identifier for this function. Used to group telemetry data by function.', }, { name: 'metadata', isOptional: true, type: 'Record<string, string | number | boolean | Array<null | undefined | string> | Array<null | undefined | number> | Array<null | undefined | boolean>>', description: 'Additional information to include in the telemetry data.', }, { name: 'tracer', type: 'Tracer', isOptional: true, description: 'A custom tracer to use for the telemetry data.', }, ], }, ], }, ]} /> ### Returns <PropertiesTable content={[ { name: 'value', type: 'VALUE', description: 'The value that was embedded.', }, { name: 'embedding', type: 'number[]', description: 'The embedding of the value.', }, { name: 'usage', type: 'EmbeddingModelUsage', description: 'The token usage for generating the embeddings.', properties: [ { type: 'EmbeddingModelUsage', parameters: [ { name: 'tokens', type: 'number', description: 'The number of tokens used in the embedding.', }, ], }, ], }, { name: 'response', type: 'Response', isOptional: true, description: 'Optional response data.', properties: [ { type: 'Response', parameters: [ { name: 'headers', isOptional: true, type: 'Record<string, string>', description: 'Response headers.', }, { name: 'body', type: 'unknown', isOptional: true, description: 'The response body.', }, ], }, ], }, { name: 'providerMetadata', type: 'ProviderMetadata | undefined', isOptional: true, description: 'Optional metadata from the provider. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.', }, ]} /> --- File: /ai/content/docs/07-reference/01-ai-sdk-core/06-embed-many.mdx --- --- title: embedMany description: API Reference for embedMany. --- # `embedMany()` Embed several values using an embedding model. The type of the value is defined by the embedding model. `embedMany` automatically splits large requests into smaller chunks if the model has a limit on how many embeddings can be generated in a single call. ```ts import { openai } from '@ai-sdk/openai'; import { embedMany } from 'ai'; const { embeddings } = await embedMany({ model: openai.textEmbeddingModel('text-embedding-3-small'), values: [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ], }); ``` ## Import <Snippet text={`import { embedMany } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'model', type: 'EmbeddingModel', description: "The embedding model to use. Example: openai.textEmbeddingModel('text-embedding-3-small')", }, { name: 'values', type: 'Array<VALUE>', description: 'The values to embed. The type depends on the model.', }, { name: 'maxRetries', type: 'number', isOptional: true, description: 'Maximum number of retries. Set to 0 to disable retries. Default: 2.', }, { name: 'abortSignal', type: 'AbortSignal', isOptional: true, description: 'An optional abort signal that can be used to cancel the call.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.', }, { name: 'experimental_telemetry', type: 'TelemetrySettings', isOptional: true, description: 'Telemetry configuration. Experimental feature.', properties: [ { type: 'TelemetrySettings', parameters: [ { name: 'isEnabled', type: 'boolean', isOptional: true, description: 'Enable or disable telemetry. Disabled by default while experimental.', }, { name: 'recordInputs', type: 'boolean', isOptional: true, description: 'Enable or disable input recording. Enabled by default.', }, { name: 'recordOutputs', type: 'boolean', isOptional: true, description: 'Enable or disable output recording. Enabled by default.', }, { name: 'functionId', type: 'string', isOptional: true, description: 'Identifier for this function. Used to group telemetry data by function.', }, { name: 'metadata', isOptional: true, type: 'Record<string, string | number | boolean | Array<null | undefined | string> | Array<null | undefined | number> | Array<null | undefined | boolean>>', description: 'Additional information to include in the telemetry data.', }, { name: 'tracer', type: 'Tracer', isOptional: true, description: 'A custom tracer to use for the telemetry data.', }, ], }, ], }, ]} /> ### Returns <PropertiesTable content={[ { name: 'values', type: 'Array<VALUE>', description: 'The values that were embedded.', }, { name: 'embeddings', type: 'number[][]', description: 'The embeddings. They are in the same order as the values.', }, { name: 'usage', type: 'EmbeddingModelUsage', description: 'The token usage for generating the embeddings.', properties: [ { type: 'EmbeddingModelUsage', parameters: [ { name: 'tokens', type: 'number', description: 'The total number of input tokens.', }, { name: 'body', type: 'unknown', isOptional: true, description: 'The response body.', }, ], }, ], }, { name: 'providerMetadata', type: 'ProviderMetadata | undefined', isOptional: true, description: 'Optional metadata from the provider. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.', }, ]} /> --- File: /ai/content/docs/07-reference/01-ai-sdk-core/10-generate-image.mdx --- --- title: generateImage description: API Reference for generateImage. --- # `generateImage()` <Note type="warning">`generateImage` is an experimental feature.</Note> Generates images based on a given prompt using an image model. It is ideal for use cases where you need to generate images programmatically, such as creating visual content or generating images for data augmentation. ```ts import { experimental_generateImage as generateImage } from 'ai'; const { images } = await generateImage({ model: openai.image('dall-e-3'), prompt: 'A futuristic cityscape at sunset', n: 3, size: '1024x1024', }); console.log(images); ``` ## Import <Snippet text={`import { experimental_generateImage as generateImage } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'model', type: 'ImageModelV2', description: 'The image model to use.', }, { name: 'prompt', type: 'string', description: 'The input prompt to generate the image from.', }, { name: 'n', type: 'number', isOptional: true, description: 'Number of images to generate.', }, { name: 'size', type: 'string', isOptional: true, description: 'Size of the images to generate. Format: `{width}x{height}`.', }, { name: 'aspectRatio', type: 'string', isOptional: true, description: 'Aspect ratio of the images to generate. Format: `{width}:{height}`.', }, { name: 'seed', type: 'number', isOptional: true, description: 'Seed for the image generation.', }, { name: 'providerOptions', type: 'ProviderOptions', isOptional: true, description: 'Additional provider-specific options.', }, { name: 'maxRetries', type: 'number', isOptional: true, description: 'Maximum number of retries. Default: 2.', }, { name: 'abortSignal', type: 'AbortSignal', isOptional: true, description: 'An optional abort signal to cancel the call.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'Additional HTTP headers for the request.', }, ]} /> ### Returns <PropertiesTable content={[ { name: 'image', type: 'GeneratedFile', description: 'The first image that was generated.', properties: [ { type: 'GeneratedFile', parameters: [ { name: 'base64', type: 'string', description: 'Image as a base64 encoded string.', }, { name: 'uint8Array', type: 'Uint8Array', description: 'Image as a Uint8Array.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the image.', }, ], }, ], }, { name: 'images', type: 'Array<GeneratedFile>', description: 'All images that were generated.', properties: [ { type: 'GeneratedFile', parameters: [ { name: 'base64', type: 'string', description: 'Image as a base64 encoded string.', }, { name: 'uint8Array', type: 'Uint8Array', description: 'Image as a Uint8Array.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the image.', }, ], }, ], }, { name: 'warnings', type: 'ImageGenerationWarning[]', description: 'Warnings from the model provider (e.g. unsupported settings).', }, { name: 'providerMetadata', type: 'ImageModelProviderMetadata', isOptional: true, description: 'Optional metadata from the provider. The outer key is the provider name. The inner values are the metadata. An `images` key is always present in the metadata and is an array with the same length as the top level `images` key. Details depend on the provider.', }, { name: 'responses', type: 'Array<ImageModelResponseMetadata>', description: 'Response metadata from the provider. There may be multiple responses if we made multiple calls to the model.', properties: [ { type: 'ImageModelResponseMetadata', parameters: [ { name: 'timestamp', type: 'Date', description: 'Timestamp for the start of the generated response.', }, { name: 'modelId', type: 'string', description: 'The ID of the response model that was used to generate the response.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'Response headers.', }, ], }, ], }, ]} /> --- File: /ai/content/docs/07-reference/01-ai-sdk-core/11-transcribe.mdx --- --- title: transcribe description: API Reference for transcribe. --- # `transcribe()` <Note type="warning">`transcribe` is an experimental feature.</Note> Generates a transcript from an audio file. ```ts import { experimental_transcribe as transcribe } from 'ai'; import { openai } from '@ai-sdk/openai'; import { readFile } from 'fs/promises'; const { text: transcript } = await transcribe({ model: openai.transcription('whisper-1'), audio: await readFile('audio.mp3'), }); console.log(transcript); ``` ## Import <Snippet text={`import { experimental_transcribe as transcribe } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'model', type: 'TranscriptionModelV2', description: 'The transcription model to use.', }, { name: 'audio', type: 'DataContent (string | Uint8Array | ArrayBuffer | Buffer) | URL', description: 'The audio file to generate the transcript from.', }, { name: 'providerOptions', type: 'Record<string, Record<string, JSONValue>>', isOptional: true, description: 'Additional provider-specific options.', }, { name: 'maxRetries', type: 'number', isOptional: true, description: 'Maximum number of retries. Default: 2.', }, { name: 'abortSignal', type: 'AbortSignal', isOptional: true, description: 'An optional abort signal to cancel the call.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'Additional HTTP headers for the request.', }, ]} /> ### Returns <PropertiesTable content={[ { name: 'text', type: 'string', description: 'The complete transcribed text from the audio input.', }, { name: 'segments', type: 'Array<{ text: string; startSecond: number; endSecond: number }>', description: 'An array of transcript segments, each containing a portion of the transcribed text along with its start and end times in seconds.', }, { name: 'language', type: 'string | undefined', description: 'The language of the transcript in ISO-639-1 format e.g. "en" for English.', }, { name: 'durationInSeconds', type: 'number | undefined', description: 'The duration of the transcript in seconds.', }, { name: 'warnings', type: 'TranscriptionWarning[]', description: 'Warnings from the model provider (e.g. unsupported settings).', }, { name: 'responses', type: 'Array<TranscriptionModelResponseMetadata>', description: 'Response metadata from the provider. There may be multiple responses if we made multiple calls to the model.', properties: [ { type: 'TranscriptionModelResponseMetadata', parameters: [ { name: 'timestamp', type: 'Date', description: 'Timestamp for the start of the generated response.', }, { name: 'modelId', type: 'string', description: 'The ID of the response model that was used to generate the response.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'Response headers.', }, ], }, ], }, ]} /> --- File: /ai/content/docs/07-reference/01-ai-sdk-core/12-generate-speech.mdx --- --- title: generateSpeech description: API Reference for generateSpeech. --- # `generateSpeech()` <Note type="warning">`generateSpeech` is an experimental feature.</Note> Generates speech audio from text. ```ts import { experimental_generateSpeech as generateSpeech } from 'ai'; import { openai } from '@ai-sdk/openai'; import { readFile } from 'fs/promises'; const { audio } = await generateSpeech({ model: openai.speech('tts-1'), text: 'Hello from the AI SDK!', }); console.log(audio); ``` ## Import <Snippet text={`import { experimental_generateSpeech as generateSpeech } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'model', type: 'SpeechModelV2', description: 'The speech model to use.', }, { name: 'text', type: 'string', description: 'The text to generate the speech from.', }, { name: 'voice', type: 'string', isOptional: true, description: 'The voice to use for the speech.', }, { name: 'outputFormat', type: 'string', isOptional: true, description: 'The output format to use for the speech e.g. "mp3", "wav", etc.', }, { name: 'instructions', type: 'string', isOptional: true, description: 'Instructions for the speech generation.', }, { name: 'speed', type: 'number', isOptional: true, description: 'The speed of the speech generation.', }, { name: 'language', type: 'string', isOptional: true, description: 'The language for speech generation. This should be an ISO 639-1 language code (e.g. "en", "es", "fr") or "auto" for automatic language detection. Provider support varies.', }, { name: 'providerOptions', type: 'Record<string, Record<string, JSONValue>>', isOptional: true, description: 'Additional provider-specific options.', }, { name: 'maxRetries', type: 'number', isOptional: true, description: 'Maximum number of retries. Default: 2.', }, { name: 'abortSignal', type: 'AbortSignal', isOptional: true, description: 'An optional abort signal to cancel the call.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'Additional HTTP headers for the request.', }, ]} /> ### Returns <PropertiesTable content={[ { name: 'audio', type: 'GeneratedAudioFile', description: 'The generated audio.', properties: [ { type: 'GeneratedAudioFile', parameters: [ { name: 'base64', type: 'string', description: 'Audio as a base64 encoded string.', }, { name: 'uint8Array', type: 'Uint8Array', description: 'Audio as a Uint8Array.', }, { name: 'mimeType', type: 'string', description: 'MIME type of the audio (e.g. "audio/mpeg").', }, { name: 'format', type: 'string', description: 'Format of the audio (e.g. "mp3").', }, ], }, ], }, { name: 'warnings', type: 'SpeechWarning[]', description: 'Warnings from the model provider (e.g. unsupported settings).', }, { name: 'responses', type: 'Array<SpeechModelResponseMetadata>', description: 'Response metadata from the provider. There may be multiple responses if we made multiple calls to the model.', properties: [ { type: 'SpeechModelResponseMetadata', parameters: [ { name: 'timestamp', type: 'Date', description: 'Timestamp for the start of the generated response.', }, { name: 'modelId', type: 'string', description: 'The ID of the response model that was used to generate the response.', }, { name: 'body', isOptional: true, type: 'unknown', description: 'Optional response body.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'Response headers.', }, ], }, ], }, ]} /> --- File: /ai/content/docs/07-reference/01-ai-sdk-core/20-tool.mdx --- --- title: tool description: Helper function for tool type inference --- # `tool()` Tool is a helper function that infers the tool input for its `execute` method. It does not have any runtime behavior, but it helps TypeScript infer the types of the input for the `execute` method. Without this helper function, TypeScript is unable to connect the `inputSchema` property to the `execute` method, and the argument types of `execute` cannot be inferred. ```ts highlight={"1,4,9,10"} import { tool } from 'ai'; import { z } from 'zod'; export const weatherTool = tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), // location below is inferred to be a string: execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }); ``` ## Import <Snippet text={`import { tool } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'tool', type: 'Tool', description: 'The tool definition.', properties: [ { type: 'Tool', parameters: [ { name: 'description', isOptional: true, type: 'string', description: 'Information about the purpose of the tool including details on how and when it can be used by the model.', }, { name: 'inputSchema', type: 'Zod Schema | JSON Schema', description: 'The schema of the input that the tool expects. The language model will use this to generate the input. It is also used to validate the output of the language model. Use descriptions to make the input understandable for the language model. You can either pass in a Zod schema or a JSON schema (using the `jsonSchema` function).', }, { name: 'execute', isOptional: true, type: 'async (input: T, options: ToolCallOptions) => RESULT', description: 'An async function that is called with the arguments from the tool call and produces a result. If not provided, the tool will not be executed automatically.', properties: [ { type: 'ToolCallOptions', parameters: [ { name: 'toolCallId', type: 'string', description: 'The ID of the tool call. You can use it e.g. when sending tool-call related information with stream data.', }, { name: 'messages', type: 'ModelMessage[]', description: 'Messages that were sent to the language model to initiate the response that contained the tool call. The messages do not include the system prompt nor the assistant response that contained the tool call.', }, { name: 'abortSignal', type: 'AbortSignal', isOptional: true, description: 'An optional abort signal that indicates that the overall operation should be aborted.', }, { name: 'experimental_context', type: 'unknown', isOptional: true, description: 'Context that is passed into tool execution. Experimental (can break in patch releases).', }, ], }, ], }, { name: 'outputSchema', isOptional: true, type: 'Zod Schema | JSON Schema', description: 'The schema of the output that the tool produces. Used for validation and type inference.', }, { name: 'toModelOutput', isOptional: true, type: "(output: RESULT) => LanguageModelV2ToolResultPart['output']", description: 'Optional conversion function that maps the tool result to an output that can be used by the language model. If not provided, the tool result will be sent as a JSON object.', }, { name: 'onInputStart', isOptional: true, type: '(options: ToolCallOptions) => void | PromiseLike<void>', description: 'Optional function that is called when the argument streaming starts. Only called when the tool is used in a streaming context.', }, { name: 'onInputDelta', isOptional: true, type: '(options: { inputTextDelta: string } & ToolCallOptions) => void | PromiseLike<void>', description: 'Optional function that is called when an argument streaming delta is available. Only called when the tool is used in a streaming context.', }, { name: 'onInputAvailable', isOptional: true, type: '(options: { input: INPUT } & ToolCallOptions) => void | PromiseLike<void>', description: 'Optional function that is called when a tool call can be started, even if the execute function is not provided.', }, { name: 'providerOptions', isOptional: true, type: 'ProviderOptions', description: 'Additional provider-specific metadata. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider.', }, { name: 'type', isOptional: true, type: "'function' | 'provider-defined'", description: 'The type of the tool. Defaults to "function" for regular tools. Use "provider-defined" for provider-specific tools.', }, { name: 'id', isOptional: true, type: 'string', description: 'The ID of the tool for provider-defined tools. Should follow the format `<provider-name>.<unique-tool-name>`. Required when type is "provider-defined".', }, { name: 'name', isOptional: true, type: 'string', description: 'The name of the tool that the user must use in the tool set. Required when type is "provider-defined".', }, { name: 'args', isOptional: true, type: 'Record<string, unknown>', description: 'The arguments for configuring the tool. Must match the expected arguments defined by the provider for this tool. Required when type is "provider-defined".', }, ], }, ], }, ]} /> ### Returns The tool that was passed in. --- File: /ai/content/docs/07-reference/01-ai-sdk-core/22-dynamic-tool.mdx --- --- title: dynamicTool description: Helper function for creating dynamic tools with unknown types --- # `dynamicTool()` The `dynamicTool` function creates tools where the input and output types are not known at compile time. This is useful for scenarios such as: - MCP (Model Context Protocol) tools without schemas - User-defined functions loaded at runtime - Tools loaded from external sources or databases - Dynamic tool generation based on user input Unlike the regular `tool` function, `dynamicTool` accepts and returns `unknown` types, allowing you to work with tools that have runtime-determined schemas. ```ts highlight={"1,4,9,10,11"} import { dynamicTool } from 'ai'; import { z } from 'zod'; export const customTool = dynamicTool({ description: 'Execute a custom user-defined function', inputSchema: z.object({}), // input is typed as 'unknown' execute: async input => { const { action, parameters } = input as any; // Execute your dynamic logic return { result: `Executed ${action} with ${JSON.stringify(parameters)}`, }; }, }); ``` ## Import <Snippet text={`import { dynamicTool } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'tool', type: 'Object', description: 'The dynamic tool definition.', properties: [ { type: 'Object', parameters: [ { name: 'description', isOptional: true, type: 'string', description: 'Information about the purpose of the tool including details on how and when it can be used by the model.' }, { name: 'inputSchema', type: 'FlexibleSchema<unknown>', description: 'The schema of the input that the tool expects. While the type is unknown, a schema is still required for validation. You can use Zod schemas with z.unknown() or z.any() for fully dynamic inputs.' }, { name: 'execute', type: 'ToolExecuteFunction<unknown, unknown>', description: 'An async function that is called with the arguments from the tool call. The input is typed as unknown and must be validated/cast at runtime.', properties: [ { type: "ToolCallOptions", parameters: [ { name: 'toolCallId', type: 'string', description: 'The ID of the tool call.', }, { name: "messages", type: "ModelMessage[]", description: "Messages that were sent to the language model." }, { name: "abortSignal", type: "AbortSignal", isOptional: true, description: "An optional abort signal." } ] } ] }, { name: 'toModelOutput', isOptional: true, type: '(output: unknown) => LanguageModelV2ToolResultPart[\'output\']', description: 'Optional conversion function that maps the tool result to an output that can be used by the language model.' }, { name: 'providerOptions', isOptional: true, type: 'ProviderOptions', description: 'Additional provider-specific metadata.' } ] } ] } ]} /> ### Returns A `Tool<unknown, unknown>` with `type: 'dynamic'` that can be used with `generateText`, `streamText`, and other AI SDK functions. ## Type-Safe Usage When using dynamic tools alongside static tools, you need to check the `dynamic` flag for proper type narrowing: ```ts const result = await generateText({ model: openai('gpt-4'), tools: { // Static tool with known types weather: weatherTool, // Dynamic tool with unknown types custom: dynamicTool({ /* ... */ }), }, onStepFinish: ({ toolCalls, toolResults }) => { for (const toolCall of toolCalls) { if (toolCall.dynamic) { // Dynamic tool: input/output are 'unknown' console.log('Dynamic tool:', toolCall.toolName); console.log('Input:', toolCall.input); continue; } // Static tools have full type inference switch (toolCall.toolName) { case 'weather': // TypeScript knows the exact types console.log(toolCall.input.location); // string break; } } }, }); ``` ## Usage with `useChat` When used with useChat (`UIMessage` format), dynamic tools appear as `dynamic-tool` parts: ```tsx { message.parts.map(part => { switch (part.type) { case 'dynamic-tool': return ( <div> <h4>Tool: {part.toolName}</h4> <pre>{JSON.stringify(part.input, null, 2)}</pre> </div> ); // ... handle other part types } }); } ``` --- File: /ai/content/docs/07-reference/01-ai-sdk-core/23-create-mcp-client.mdx --- --- title: experimental_createMCPClient description: Create a client for connecting to MCP servers --- # `experimental_createMCPClient()` Creates a lightweight Model Context Protocol (MCP) client that connects to an MCP server. The client's primary purpose is tool conversion between MCP tools and AI SDK tools. It currently does not support accepting notifications from an MCP server, and custom configuration of the client. This feature is experimental and may change or be removed in the future. ## Import <Snippet text={`import { experimental_createMCPClient } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'config', type: 'MCPClientConfig', description: 'Configuration for the MCP client.', properties: [ { type: 'MCPClientConfig', parameters: [ { name: 'transport', type: 'TransportConfig = MCPTransport | McpSSEServerConfig', description: 'Configuration for the message transport layer.', properties: [ { type: 'MCPTransport', description: 'A client transport instance, used explicitly for stdio or custom transports', parameters: [ { name: 'start', type: '() => Promise<void>', description: 'A method that starts the transport', }, { name: 'send', type: '(message: JSONRPCMessage) => Promise<void>', description: 'A method that sends a message through the transport', }, { name: 'close', type: '() => Promise<void>', description: 'A method that closes the transport', }, { name: 'onclose', type: '() => void', description: 'A method that is called when the transport is closed', }, { name: 'onerror', type: '(error: Error) => void', description: 'A method that is called when the transport encounters an error', }, { name: 'onmessage', type: '(message: JSONRPCMessage) => void', description: 'A method that is called when the transport receives a message', }, ], }, { type: 'McpSSEServerConfig', parameters: [ { name: 'type', type: "'sse'", description: 'Use Server-Sent Events for communication', }, { name: 'url', type: 'string', description: 'URL of the MCP server', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'Additional HTTP headers to be sent with requests.', }, ], }, ], }, { name: 'name', type: 'string', isOptional: true, description: 'Client name. Defaults to "ai-sdk-mcp-client"', }, { name: 'onUncaughtError', type: '(error: unknown) => void', isOptional: true, description: 'Handler for uncaught errors', }, ], }, ], }, ]} /> ### Returns Returns a Promise that resolves to an `MCPClient` with the following methods: <PropertiesTable content={[ { name: 'tools', type: `async (options?: { schemas?: TOOL_SCHEMAS }) => Promise<McpToolSet<TOOL_SCHEMAS>>`, description: 'Gets the tools available from the MCP server.', properties: [ { type: 'options', parameters: [ { name: 'schemas', type: 'TOOL_SCHEMAS', isOptional: true, description: 'Schema definitions for compile-time type checking. When not provided, schemas are inferred from the server.', }, ], }, ], }, { name: 'close', type: 'async () => void', description: 'Closes the connection to the MCP server and cleans up resources.', }, ]} /> ## Example ```typescript import { experimental_createMCPClient, generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; try { const client = await experimental_createMCPClient({ transport: { type: 'stdio', command: 'node server.js', }, }); const tools = await client.tools(); const response = await generateText({ model: openai('gpt-4o-mini'), tools, messages: [{ role: 'user', content: 'Query the data' }], }); console.log(response); } finally { await client.close(); } ``` ## Error Handling The client throws `MCPClientError` for: - Client initialization failures - Protocol version mismatches - Missing server capabilities - Connection failures For tool execution, errors are propagated as `CallToolError` errors. For unknown errors, the client exposes an `onUncaughtError` callback that can be used to manually log or handle errors that are not covered by known error types. --- File: /ai/content/docs/07-reference/01-ai-sdk-core/24-mcp-stdio-transport.mdx --- --- title: Experimental_StdioMCPTransport description: Create a transport for Model Context Protocol (MCP) clients to communicate with MCP servers using standard input and output streams --- # `Experimental_StdioMCPTransport` Creates a transport for Model Context Protocol (MCP) clients to communicate with MCP servers using standard input and output streams. This transport is only supported in Node.js environments. This feature is experimental and may change or be removed in the future. ## Import <Snippet text={`import { Experimental_StdioMCPTransport } from "ai/mcp-stdio"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'config', type: 'StdioConfig', description: 'Configuration for the MCP client.', properties: [ { type: 'StdioConfig', parameters: [ { name: 'command', type: 'string', description: 'The command to run the MCP server.', }, { name: 'args', type: 'string[]', isOptional: true, description: 'The arguments to pass to the MCP server.', }, { name: 'env', type: 'Record<string, string>', isOptional: true, description: 'The environment variables to set for the MCP server.', }, { name: 'stderr', type: 'IOType | Stream | number', isOptional: true, description: "The stream to write the MCP server's stderr to.", }, { name: 'cwd', type: 'string', isOptional: true, description: 'The current working directory for the MCP server.', }, ], }, ], }, ]} /> --- File: /ai/content/docs/07-reference/01-ai-sdk-core/25-json-schema.mdx --- --- title: jsonSchema description: Helper function for creating JSON schemas --- # `jsonSchema()` `jsonSchema` is a helper function that creates a JSON schema object that is compatible with the AI SDK. It takes the JSON schema and an optional validation function as inputs, and can be typed. You can use it to [generate structured data](/docs/ai-sdk-core/generating-structured-data) and in [tools](/docs/ai-sdk-core/tools-and-tool-calling). `jsonSchema` is an alternative to using Zod schemas that provides you with flexibility in dynamic situations (e.g. when using OpenAPI definitions) or for using other validation libraries. ```ts import { jsonSchema } from 'ai'; const mySchema = jsonSchema<{ recipe: { name: string; ingredients: { name: string; amount: string }[]; steps: string[]; }; }>({ type: 'object', properties: { recipe: { type: 'object', properties: { name: { type: 'string' }, ingredients: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, amount: { type: 'string' }, }, required: ['name', 'amount'], }, }, steps: { type: 'array', items: { type: 'string' }, }, }, required: ['name', 'ingredients', 'steps'], }, }, required: ['recipe'], }); ``` ## Import <Snippet text={`import { jsonSchema } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'schema', type: 'JSONSchema7', description: 'The JSON schema definition.', }, { name: 'options', type: 'SchemaOptions', description: 'Additional options for the JSON schema.', properties: [ { type: 'SchemaOptions', parameters: [ { name: 'validate', isOptional: true, type: '(value: unknown) => { success: true; value: OBJECT } | { success: false; error: Error };', description: 'A function that validates the value against the JSON schema. If the value is valid, the function should return an object with a `success` property set to `true` and a `value` property set to the validated value. If the value is invalid, the function should return an object with a `success` property set to `false` and an `error` property set to the error.', }, ], }, ], }, ]} /> ### Returns A JSON schema object that is compatible with the AI SDK. --- File: /ai/content/docs/07-reference/01-ai-sdk-core/26-zod-schema.mdx --- --- title: zodSchema description: Helper function for creating Zod schemas --- # `zodSchema()` `zodSchema` is a helper function that converts a Zod schema into a JSON schema object that is compatible with the AI SDK. It takes a Zod schema and optional configuration as inputs, and returns a typed schema. You can use it to [generate structured data](/docs/ai-sdk-core/generating-structured-data) and in [tools](/docs/ai-sdk-core/tools-and-tool-calling). <Note> You can also pass Zod objects directly to the AI SDK functions. Internally, the AI SDK will convert the Zod schema to a JSON schema using `zodSchema()`. However, if you want to specify options such as `useReferences`, you can pass the `zodSchema()` helper function instead. </Note> ## Example with recursive schemas ```ts import { zodSchema } from 'ai'; import { z } from 'zod'; // Define a base category schema const baseCategorySchema = z.object({ name: z.string(), }); // Define the recursive Category type type Category = z.infer<typeof baseCategorySchema> & { subcategories: Category[]; }; // Create the recursive schema using z.lazy const categorySchema: z.ZodType<Category> = baseCategorySchema.extend({ subcategories: z.lazy(() => categorySchema.array()), }); // Create the final schema with useReferences enabled for recursive support const mySchema = zodSchema( z.object({ category: categorySchema, }), { useReferences: true }, ); ``` ## Import <Snippet text={`import { zodSchema } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'zodSchema', type: 'z.Schema', description: 'The Zod schema definition.', }, { name: 'options', type: 'object', description: 'Additional options for the schema conversion.', properties: [ { type: 'object', parameters: [ { name: 'useReferences', isOptional: true, type: 'boolean', description: 'Enables support for references in the schema. This is required for recursive schemas, e.g. with `z.lazy`. However, not all language models and providers support such references. Defaults to `false`.', }, ], }, ], }, ]} /> ### Returns A Schema object that is compatible with the AI SDK, containing both the JSON schema representation and validation functionality. --- File: /ai/content/docs/07-reference/01-ai-sdk-core/27-valibot-schema.mdx --- --- title: valibotSchema description: Helper function for creating Valibot schemas --- # `valibotSchema()` <Note type="warning">`valibotSchema` is currently experimental.</Note> `valibotSchema` is a helper function that converts a Valibot schema into a JSON schema object that is compatible with the AI SDK. It takes a Valibot schema as input, and returns a typed schema. You can use it to [generate structured data](/docs/ai-sdk-core/generating-structured-data) and in [tools](/docs/ai-sdk-core/tools-and-tool-calling). ## Example ```ts import { valibotSchema } from '@ai-sdk/valibot'; import { object, string, array } from 'valibot'; const recipeSchema = valibotSchema( object({ name: string(), ingredients: array( object({ name: string(), amount: string(), }), ), steps: array(string()), }), ); ``` ## Import <Snippet text={`import { valibotSchema } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'valibotSchema', type: 'GenericSchema<unknown, T>', description: 'The Valibot schema definition.', }, ]} /> ### Returns A Schema object that is compatible with the AI SDK, containing both the JSON schema representation and validation functionality. --- File: /ai/content/docs/07-reference/01-ai-sdk-core/30-model-message.mdx --- --- title: ModelMessage description: Message types for AI SDK Core (API Reference) --- # `ModelMessage` `ModelMessage` represents the fundamental message structure used with AI SDK Core functions. It encompasses various message types that can be used in the `messages` field of any AI SDK Core functions. You can access the Zod schema for `ModelMessage` with the `modelMessageSchema` export. ## `ModelMessage` Types ### `SystemModelMessage` A system message that can contain system information. ```typescript type SystemModelMessage = { role: 'system'; content: string; }; ``` You can access the Zod schema for `SystemModelMessage` with the `systemModelMessageSchema` export. <Note> Using the "system" property instead of a system message is recommended to enhance resilience against prompt injection attacks. </Note> ### `UserModelMessage` A user message that can contain text or a combination of text, images, and files. ```typescript type UserModelMessage = { role: 'user'; content: UserContent; }; type UserContent = string | Array<TextPart | ImagePart | FilePart>; ``` You can access the Zod schema for `UserModelMessage` with the `userModelMessageSchema` export. ### `AssistantModelMessage` An assistant message that can contain text, tool calls, or a combination of both. ```typescript type AssistantModelMessage = { role: 'assistant'; content: AssistantContent; }; type AssistantContent = string | Array<TextPart | ToolCallPart>; ``` You can access the Zod schema for `AssistantModelMessage` with the `assistantModelMessageSchema` export. ### `ToolModelMessage` A tool message that contains the result of one or more tool calls. ```typescript type ToolModelMessage = { role: 'tool'; content: ToolContent; }; type ToolContent = Array<ToolResultPart>; ``` You can access the Zod schema for `ToolModelMessage` with the `toolModelMessageSchema` export. ## `ModelMessage` Parts ### `TextPart` Represents a text content part of a prompt. It contains a string of text. ```typescript export interface TextPart { type: 'text'; /** * The text content. */ text: string; } ``` ### `ImagePart` Represents an image part in a user message. ```typescript export interface ImagePart { type: 'image'; /** * Image data. Can either be: * - data: a base64-encoded string, a Uint8Array, an ArrayBuffer, or a Buffer * - URL: a URL that points to the image */ image: DataContent | URL; /** * Optional IANA media type of the image. * We recommend leaving this out as it will be detected automatically. */ mediaType?: string; } ``` ### `FilePart` Represents an file part in a user message. ```typescript export interface FilePart { type: 'file'; /** * File data. Can either be: * - data: a base64-encoded string, a Uint8Array, an ArrayBuffer, or a Buffer * - URL: a URL that points to the file */ data: DataContent | URL; /** * Optional filename of the file. */ filename?: string; /** * IANA media type of the file. */ mediaType: string; } ``` ### `ToolCallPart` Represents a tool call content part of a prompt, typically generated by the AI model. ```typescript export interface ToolCallPart { type: 'tool-call'; /** * ID of the tool call. This ID is used to match the tool call with the tool result. */ toolCallId: string; /** * Name of the tool that is being called. */ toolName: string; /** * Arguments of the tool call. This is a JSON-serializable object that matches the tool's input schema. */ args: unknown; } ``` ### `ToolResultPart` Represents the result of a tool call in a tool message. ```typescript export interface ToolResultPart { type: 'tool-result'; /** * ID of the tool call that this result is associated with. */ toolCallId: string; /** * Name of the tool that generated this result. */ toolName: string; /** * Result of the tool call. This is a JSON-serializable object. */ result: unknown; /** * Multi-part content of the tool result. Only for tools that support multipart results. */ experimental_content?: ToolResultContent; /** * Optional flag if the result is an error or an error message. */ isError?: boolean; } ``` ### `ToolResultContent` ```ts export type ToolResultContent = Array< | { type: 'text'; text: string; } | { type: 'image'; data: string; // base64 encoded png image, e.g. screenshot mediaType?: string; // e.g. 'image/png'; } >; ``` --- File: /ai/content/docs/07-reference/01-ai-sdk-core/31-ui-message.mdx --- --- title: UIMessage description: API Reference for UIMessage --- # `UIMessage` `UIMessage` serves as the source of truth for your application's state, representing the complete message history including metadata, data parts, and all contextual information. In contrast to `ModelMessage`, which represents the state or context passed to the model, `UIMessage` contains the full application state needed for UI rendering and client-side functionality. ## Type Safety `UIMessage` is designed to be type-safe and accepts three generic parameters to ensure proper typing throughout your application: 1. **`METADATA`** - Custom metadata type for additional message information 2. **`DATA_PARTS`** - Custom data part types for structured data components 3. **`TOOLS`** - Tool definitions for type-safe tool interactions ## Creating Your Own UIMessage Type Here's an example of how to create a custom typed UIMessage for your application: ```typescript import { InferUITools, ToolSet, UIMessage, tool } from 'ai'; import z from 'zod'; const metadataSchema = z.object({ someMetadata: z.string().datetime(), }); type MyMetadata = z.infer<typeof metadataSchema>; const dataPartSchema = z.object({ someDataPart: z.object({}), anotherDataPart: z.object({}), }); type MyDataPart = z.infer<typeof dataPartSchema>; const tools: ToolSet = { someTool: tool({}), }; type MyTools = InferUITools<typeof tools>; export type MyUIMessage = UIMessage<MyMetadata, MyDataPart, MyTools>; ``` ## `UIMessage` Interface ```typescript interface UIMessage< METADATA = unknown, DATA_PARTS extends UIDataTypes = UIDataTypes, TOOLS extends UITools = UITools, > { /** * A unique identifier for the message. */ id: string; /** * The role of the message. */ role: 'system' | 'user' | 'assistant'; /** * The metadata of the message. */ metadata?: METADATA; /** * The parts of the message. Use this for rendering the message in the UI. */ parts: Array<UIMessagePart<DATA_PARTS, TOOLS>>; } ``` ## `UIMessagePart` Types ### `TextUIPart` A text part of a message. ```typescript type TextUIPart = { type: 'text'; /** * The text content. */ text: string; /** * The state of the text part. */ state?: 'streaming' | 'done'; }; ``` ### `ReasoningUIPart` A reasoning part of a message. ```typescript type ReasoningUIPart = { type: 'reasoning'; /** * The reasoning text. */ text: string; /** * The state of the reasoning part. */ state?: 'streaming' | 'done'; /** * The provider metadata. */ providerMetadata?: Record<string, any>; }; ``` ### `ToolUIPart` A tool part of a message that represents tool invocations and their results. <Note> The type is based on the name of the tool (e.g., `tool-someTool` for a tool named `someTool`). </Note> ```typescript type ToolUIPart<TOOLS extends UITools = UITools> = ValueOf<{ [NAME in keyof TOOLS & string]: { type: `tool-${NAME}`; toolCallId: string; } & ( | { state: 'input-streaming'; input: DeepPartial<TOOLS[NAME]['input']> | undefined; providerExecuted?: boolean; output?: never; errorText?: never; } | { state: 'input-available'; input: TOOLS[NAME]['input']; providerExecuted?: boolean; output?: never; errorText?: never; } | { state: 'output-available'; input: TOOLS[NAME]['input']; output: TOOLS[NAME]['output']; errorText?: never; providerExecuted?: boolean; } | { state: 'output-error'; input: TOOLS[NAME]['input']; output?: never; errorText: string; providerExecuted?: boolean; } ); }>; ``` ### `SourceUrlUIPart` A source URL part of a message. ```typescript type SourceUrlUIPart = { type: 'source-url'; sourceId: string; url: string; title?: string; providerMetadata?: Record<string, any>; }; ``` ### `SourceDocumentUIPart` A document source part of a message. ```typescript type SourceDocumentUIPart = { type: 'source-document'; sourceId: string; mediaType: string; title: string; filename?: string; providerMetadata?: Record<string, any>; }; ``` ### `FileUIPart` A file part of a message. ```typescript type FileUIPart = { type: 'file'; /** * IANA media type of the file. */ mediaType: string; /** * Optional filename of the file. */ filename?: string; /** * The URL of the file. * It can either be a URL to a hosted file or a Data URL. */ url: string; }; ``` ### `DataUIPart` A data part of a message for custom data types. <Note> The type is based on the name of the data part (e.g., `data-someDataPart` for a data part named `someDataPart`). </Note> ```typescript type DataUIPart<DATA_TYPES extends UIDataTypes> = ValueOf<{ [NAME in keyof DATA_TYPES & string]: { type: `data-${NAME}`; id?: string; data: DATA_TYPES[NAME]; }; }>; ``` ### `StepStartUIPart` A step boundary part of a message. ```typescript type StepStartUIPart = { type: 'step-start'; }; ``` --- File: /ai/content/docs/07-reference/01-ai-sdk-core/40-provider-registry.mdx --- --- title: createProviderRegistry description: Registry for managing multiple providers and models (API Reference) --- # `createProviderRegistry()` When you work with multiple providers and models, it is often desirable to manage them in a central place and access the models through simple string ids. `createProviderRegistry` lets you create a registry with multiple providers that you can access by their ids in the format `providerId:modelId`. ### Setup You can create a registry with multiple providers and models using `createProviderRegistry`. ```ts import { anthropic } from '@ai-sdk/anthropic'; import { createOpenAI } from '@ai-sdk/openai'; import { createProviderRegistry } from 'ai'; export const registry = createProviderRegistry({ // register provider with prefix and default setup: anthropic, // register provider with prefix and custom setup: openai: createOpenAI({ apiKey: process.env.OPENAI_API_KEY, }), }); ``` ### Custom Separator By default, the registry uses `:` as the separator between provider and model IDs. You can customize this separator by passing a `separator` option: ```ts const registry = createProviderRegistry( { anthropic, openai, }, { separator: ' > ' }, ); // Now you can use the custom separator const model = registry.languageModel('anthropic > claude-3-opus-20240229'); ``` ### Language models You can access language models by using the `languageModel` method on the registry. The provider id will become the prefix of the model id: `providerId:modelId`. ```ts highlight={"5"} import { generateText } from 'ai'; import { registry } from './registry'; const { text } = await generateText({ model: registry.languageModel('openai:gpt-4.1'), prompt: 'Invent a new holiday and describe its traditions.', }); ``` ### Text embedding models You can access text embedding models by using the `textEmbeddingModel` method on the registry. The provider id will become the prefix of the model id: `providerId:modelId`. ```ts highlight={"5"} import { embed } from 'ai'; import { registry } from './registry'; const { embedding } = await embed({ model: registry.textEmbeddingModel('openai:text-embedding-3-small'), value: 'sunny day at the beach', }); ``` ### Image models You can access image models by using the `imageModel` method on the registry. The provider id will become the prefix of the model id: `providerId:modelId`. ```ts highlight={"5"} import { generateImage } from 'ai'; import { registry } from './registry'; const { image } = await generateImage({ model: registry.imageModel('openai:dall-e-3'), prompt: 'A beautiful sunset over a calm ocean', }); ``` ## Import <Snippet text={`import { createProviderRegistry } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'providers', type: 'Record<string, Provider>', description: 'The unique identifier for the provider. It should be unique within the registry.', properties: [ { type: 'Provider', parameters: [ { name: 'languageModel', type: '(id: string) => LanguageModel', description: 'A function that returns a language model by its id.', }, { name: 'textEmbeddingModel', type: '(id: string) => EmbeddingModel<string>', description: 'A function that returns a text embedding model by its id.', }, { name: 'imageModel', type: '(id: string) => ImageModel', description: 'A function that returns an image model by its id.', }, ], }, ], }, { name: 'options', type: 'object', description: 'Optional configuration for the registry.', properties: [ { type: 'Options', parameters: [ { name: 'separator', type: 'string', description: 'Custom separator between provider and model IDs. Defaults to ":".', }, ], }, ], }, ]} /> ### Returns The `createProviderRegistry` function returns a `Provider` instance. It has the following methods: <PropertiesTable content={[ { name: 'languageModel', type: '(id: string) => LanguageModel', description: 'A function that returns a language model by its id (format: providerId:modelId)', }, { name: 'textEmbeddingModel', type: '(id: string) => EmbeddingModel<string>', description: 'A function that returns a text embedding model by its id (format: providerId:modelId)', }, { name: 'imageModel', type: '(id: string) => ImageModel', description: 'A function that returns an image model by its id (format: providerId:modelId)', }, ]} /> --- File: /ai/content/docs/07-reference/01-ai-sdk-core/42-custom-provider.mdx --- --- title: customProvider description: Custom provider that uses models from a different provider (API Reference) --- # `customProvider()` With a custom provider, you can map ids to any model. This allows you to set up custom model configurations, alias names, and more. The custom provider also supports a fallback provider, which is useful for wrapping existing providers and adding additional functionality. ### Example: custom model settings You can create a custom provider using `customProvider`. ```ts import { openai } from '@ai-sdk/openai'; import { customProvider } from 'ai'; // custom provider with different model settings: export const myOpenAI = customProvider({ languageModels: { // replacement model with custom settings: 'gpt-4': wrapLanguageModel({ model: openai('gpt-4'), middleware: defaultSettingsMiddleware({ settings: { providerOptions: { openai: { reasoningEffort: 'high', }, }, }, }), }), // alias model with custom settings: 'gpt-4o-reasoning-high': wrapLanguageModel({ model: openai('gpt-4o'), middleware: defaultSettingsMiddleware({ settings: { providerOptions: { openai: { reasoningEffort: 'high', }, }, }, }), }), }, fallbackProvider: openai, }); ``` ## Import <Snippet text={`import { customProvider } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'languageModels', type: 'Record<string, LanguageModel>', isOptional: true, description: 'A record of language models, where keys are model IDs and values are LanguageModel instances.', }, { name: 'textEmbeddingModels', type: 'Record<string, EmbeddingModel<string>>', isOptional: true, description: 'A record of text embedding models, where keys are model IDs and values are EmbeddingModel<string> instances.', }, { name: 'imageModels', type: 'Record<string, ImageModel>', isOptional: true, description: 'A record of image models, where keys are model IDs and values are ImageModelV2 instances.', }, { name: 'fallbackProvider', type: 'Provider', isOptional: true, description: 'An optional fallback provider to use when a requested model is not found in the custom provider.', }, ]} /> ### Returns The `customProvider` function returns a `Provider` instance. It has the following methods: <PropertiesTable content={[ { name: 'languageModel', type: '(id: string) => LanguageModel', description: 'A function that returns a language model by its id (format: providerId:modelId)', }, { name: 'textEmbeddingModel', type: '(id: string) => EmbeddingModel<string>', description: 'A function that returns a text embedding model by its id (format: providerId:modelId)', }, { name: 'imageModel', type: '(id: string) => ImageModel', description: 'A function that returns an image model by its id (format: providerId:modelId)', }, ]} /> --- File: /ai/content/docs/07-reference/01-ai-sdk-core/50-cosine-similarity.mdx --- --- title: cosineSimilarity description: Calculate the cosine similarity between two vectors (API Reference) --- # `cosineSimilarity()` When you want to compare the similarity of embeddings, standard vector similarity metrics like cosine similarity are often used. `cosineSimilarity` calculates the cosine similarity between two vectors. A high value (close to 1) indicates that the vectors are very similar, while a low value (close to -1) indicates that they are different. ```ts import { openai } from '@ai-sdk/openai'; import { cosineSimilarity, embedMany } from 'ai'; const { embeddings } = await embedMany({ model: openai.textEmbeddingModel('text-embedding-3-small'), values: ['sunny day at the beach', 'rainy afternoon in the city'], }); console.log( `cosine similarity: ${cosineSimilarity(embeddings[0], embeddings[1])}`, ); ``` ## Import <Snippet text={`import { cosineSimilarity } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'vector1', type: 'number[]', description: 'The first vector to compare', }, { name: 'vector2', type: 'number[]', description: 'The second vector to compare', }, ]} /> ### Returns A number between -1 and 1 representing the cosine similarity between the two vectors. --- File: /ai/content/docs/07-reference/01-ai-sdk-core/60-wrap-language-model.mdx --- --- title: wrapLanguageModel description: Function for wrapping a language model with middleware (API Reference) --- # `wrapLanguageModel()` The `wrapLanguageModel` function provides a way to enhance the behavior of language models by wrapping them with middleware. See [Language Model Middleware](/docs/ai-sdk-core/middleware) for more information on middleware. ```ts import { wrapLanguageModel } from 'ai'; const wrappedLanguageModel = wrapLanguageModel({ model: 'openai/gpt-4.1', middleware: yourLanguageModelMiddleware, }); ``` ## Import <Snippet text={`import { wrapLanguageModel } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'model', type: 'LanguageModelV2', description: 'The original LanguageModelV2 instance to be wrapped.', }, { name: 'middleware', type: 'LanguageModelV2Middleware | LanguageModelV2Middleware[]', description: 'The middleware to be applied to the language model. When multiple middlewares are provided, the first middleware will transform the input first, and the last middleware will be wrapped directly around the model.', }, { name: 'modelId', type: 'string', description: "Optional custom model ID to override the original model's ID.", }, { name: 'providerId', type: 'string', description: "Optional custom provider ID to override the original model's provider.", }, ]} /> ### Returns A new `LanguageModelV2` instance with middleware applied. --- File: /ai/content/docs/07-reference/01-ai-sdk-core/65-language-model-v2-middleware.mdx --- --- title: LanguageModelV2Middleware description: Middleware for enhancing language model behavior (API Reference) --- # `LanguageModelV2Middleware` <Note type="warning"> Language model middleware is an experimental feature. </Note> Language model middleware provides a way to enhance the behavior of language models by intercepting and modifying the calls to the language model. It can be used to add features like guardrails, RAG, caching, and logging in a language model agnostic way. See [Language Model Middleware](/docs/ai-sdk-core/middleware) for more information. ## Import <Snippet text={`import { LanguageModelV2Middleware } from "ai"`} prompt={false} /> ## API Signature <PropertiesTable content={[ { name: 'transformParams', type: '({ type: "generate" | "stream", params: LanguageModelV2CallOptions }) => Promise<LanguageModelV2CallOptions>', description: 'Transforms the parameters before they are passed to the language model.', }, { name: 'wrapGenerate', type: '({ doGenerate: DoGenerateFunction, params: LanguageModelV2CallOptions, model: LanguageModelV2 }) => Promise<DoGenerateResult>', description: 'Wraps the generate operation of the language model.', }, { name: 'wrapStream', type: '({ doStream: DoStreamFunction, params: LanguageModelV2CallOptions, model: LanguageModelV2 }) => Promise<DoStreamResult>', description: 'Wraps the stream operation of the language model.', }, ]} /> --- File: /ai/content/docs/07-reference/01-ai-sdk-core/66-extract-reasoning-middleware.mdx --- --- title: extractReasoningMiddleware description: Middleware that extracts XML-tagged reasoning sections from generated text --- # `extractReasoningMiddleware()` `extractReasoningMiddleware` is a middleware function that extracts XML-tagged reasoning sections from generated text and exposes them separately from the main text content. This is particularly useful when you want to separate an AI model's reasoning process from its final output. ```ts import { extractReasoningMiddleware } from 'ai'; const middleware = extractReasoningMiddleware({ tagName: 'reasoning', separator: '\n', }); ``` ## Import <Snippet text={`import { extractReasoningMiddleware } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'tagName', type: 'string', isOptional: false, description: 'The name of the XML tag to extract reasoning from (without angle brackets)', }, { name: 'separator', type: 'string', isOptional: true, description: 'The separator to use between reasoning and text sections. Defaults to "\\n"', }, { name: 'startWithReasoning', type: 'boolean', isOptional: true, description: 'Starts with reasoning tokens. Set to true when the response always starts with reasoning and the initial tag is omitted. Defaults to false.', }, ]} /> ### Returns Returns a middleware object that: - Processes both streaming and non-streaming responses - Extracts content between specified XML tags as reasoning - Removes the XML tags and reasoning from the main text - Adds a `reasoning` property to the result containing the extracted content - Maintains proper separation between text sections using the specified separator ### Type Parameters The middleware works with the `LanguageModelV2StreamPart` type for streaming responses. --- File: /ai/content/docs/07-reference/01-ai-sdk-core/67-simulate-streaming-middleware.mdx --- --- title: simulateStreamingMiddleware description: Middleware that simulates streaming for non-streaming language models --- # `simulateStreamingMiddleware()` `simulateStreamingMiddleware` is a middleware function that simulates streaming behavior with responses from non-streaming language models. This is useful when you want to maintain a consistent streaming interface even when using models that only provide complete responses. ```ts import { simulateStreamingMiddleware } from 'ai'; const middleware = simulateStreamingMiddleware(); ``` ## Import <Snippet text={`import { simulateStreamingMiddleware } from "ai"`} prompt={false} /> ## API Signature ### Parameters This middleware doesn't accept any parameters. ### Returns Returns a middleware object that: - Takes a complete response from a language model - Converts it into a simulated stream of chunks - Properly handles various response components including: - Text content - Reasoning (as string or array of objects) - Tool calls - Metadata and usage information - Warnings ### Usage Example ```ts import { streamText } from 'ai'; import { wrapLanguageModel } from 'ai'; import { simulateStreamingMiddleware } from 'ai'; // Example with a non-streaming model const result = streamText({ model: wrapLanguageModel({ model: nonStreamingModel, middleware: simulateStreamingMiddleware(), }), prompt: 'Your prompt here', }); // Now you can use the streaming interface for await (const chunk of result.fullStream) { // Process streaming chunks } ``` ## How It Works The middleware: 1. Awaits the complete response from the language model 2. Creates a `ReadableStream` that emits chunks in the correct sequence 3. Simulates streaming by breaking down the response into appropriate chunk types 4. Preserves all metadata, reasoning, tool calls, and other response properties --- File: /ai/content/docs/07-reference/01-ai-sdk-core/68-default-settings-middleware.mdx --- --- title: defaultSettingsMiddleware description: Middleware that applies default settings for language models --- # `defaultSettingsMiddleware()` `defaultSettingsMiddleware` is a middleware function that applies default settings to language model calls. This is useful when you want to establish consistent default parameters across multiple model invocations. ```ts import { defaultSettingsMiddleware } from 'ai'; const middleware = defaultSettingsMiddleware({ settings: { temperature: 0.7, maxOutputTokens: 1000, // other settings... }, }); ``` ## Import <Snippet text={`import { defaultSettingsMiddleware } from "ai"`} prompt={false} /> ## API Signature ### Parameters The middleware accepts a configuration object with the following properties: - `settings`: An object containing default parameter values to apply to language model calls. These can include any valid `LanguageModelV2CallOptions` properties and optional provider metadata. ### Returns Returns a middleware object that: - Merges the default settings with the parameters provided in each model call - Ensures that explicitly provided parameters take precedence over defaults - Merges provider metadata objects ### Usage Example ```ts import { streamText } from 'ai'; import { wrapLanguageModel } from 'ai'; import { defaultSettingsMiddleware } from 'ai'; import { openai } from 'ai'; // Create a model with default settings const modelWithDefaults = wrapLanguageModel({ model: openai.ChatTextGenerator({ model: 'gpt-4' }), middleware: defaultSettingsMiddleware({ settings: { temperature: 0.5, maxOutputTokens: 800, providerMetadata: { openai: { tags: ['production'], }, }, }, }), }); // Use the model - default settings will be applied const result = await streamText({ model: modelWithDefaults, prompt: 'Your prompt here', // These parameters will override the defaults temperature: 0.8, }); ``` ## How It Works The middleware: 1. Takes a set of default settings as configuration 2. Merges these defaults with the parameters provided in each model call 3. Ensures that explicitly provided parameters take precedence over defaults 4. Merges provider metadata objects from both sources --- File: /ai/content/docs/07-reference/01-ai-sdk-core/75-simulate-readable-stream.mdx --- --- title: simulateReadableStream description: Create a ReadableStream that emits values with configurable delays --- # `simulateReadableStream()` `simulateReadableStream` is a utility function that creates a ReadableStream which emits provided values sequentially with configurable delays. This is particularly useful for testing streaming functionality or simulating time-delayed data streams. ```ts import { simulateReadableStream } from 'ai'; const stream = simulateReadableStream({ chunks: ['Hello', ' ', 'World'], initialDelayInMs: 100, chunkDelayInMs: 50, }); ``` ## Import <Snippet text={`import { simulateReadableStream } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'chunks', type: 'T[]', isOptional: false, description: 'Array of values to be emitted by the stream', }, { name: 'initialDelayInMs', type: 'number | null', isOptional: true, description: 'Initial delay in milliseconds before emitting the first value. Defaults to 0. Set to null to skip the initial delay entirely.', }, { name: 'chunkDelayInMs', type: 'number | null', isOptional: true, description: 'Delay in milliseconds between emitting each value. Defaults to 0. Set to null to skip delays between chunks.', }, ]} /> ### Returns Returns a `ReadableStream<T>` that: - Emits each value from the provided `chunks` array sequentially - Waits for `initialDelayInMs` before emitting the first value (if not `null`) - Waits for `chunkDelayInMs` between emitting subsequent values (if not `null`) - Closes automatically after all chunks have been emitted ### Type Parameters - `T`: The type of values contained in the chunks array and emitted by the stream ## Examples ### Basic Usage ```ts const stream = simulateReadableStream({ chunks: ['Hello', ' ', 'World'], }); ``` ### With Delays ```ts const stream = simulateReadableStream({ chunks: ['Hello', ' ', 'World'], initialDelayInMs: 1000, // Wait 1 second before first chunk chunkDelayInMs: 500, // Wait 0.5 seconds between chunks }); ``` ### Without Delays ```ts const stream = simulateReadableStream({ chunks: ['Hello', ' ', 'World'], initialDelayInMs: null, // No initial delay chunkDelayInMs: null, // No delay between chunks }); ``` --- File: /ai/content/docs/07-reference/01-ai-sdk-core/80-smooth-stream.mdx --- --- title: smoothStream description: Stream transformer for smoothing text output --- # `smoothStream()` `smoothStream` is a utility function that creates a TransformStream for the `streamText` `transform` option to smooth out text streaming by buffering and releasing complete words with configurable delays. This creates a more natural reading experience when streaming text responses. ```ts highlight={"6-9"} import { smoothStream, streamText } from 'ai'; const result = streamText({ model, prompt, experimental_transform: smoothStream({ delayInMs: 20, // optional: defaults to 10ms chunking: 'line', // optional: defaults to 'word' }), }); ``` ## Import <Snippet text={`import { smoothStream } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'delayInMs', type: 'number | null', isOptional: true, description: 'The delay in milliseconds between outputting each chunk. Defaults to 10ms. Set to `null` to disable delays.', }, { name: 'chunking', type: '"word" | "line" | RegExp | (buffer: string) => string | undefined | null', isOptional: true, description: 'Controls how the text is chunked for streaming. Use "word" to stream word by word (default), "line" to stream line by line, or provide a custom callback or RegExp pattern for custom chunking.', }, ]} /> #### Word chunking caveats with non-latin languages The word based chunking **does not work well** with the following languages that do not delimit words with spaces: For these languages we recommend using a custom regex, like the following: - Chinese - `/[\u4E00-\u9FFF]|\S+\s+/` - Japanese - `/[\u3040-\u309F\u30A0-\u30FF]|\S+\s+/` ```tsx filename="Japanese example" import { smoothStream, streamText } from 'ai'; const result = streamText({ model: 'openai/gpt-4.1', prompt: 'Your prompt here', experimental_transform: smoothStream({ chunking: /[\u3040-\u309F\u30A0-\u30FF]|\S+\s+/, }), }); ``` ```tsx filename="Chinese example" import { smoothStream, streamText } from 'ai'; const result = streamText({ model: 'openai/gpt-4.1', prompt: 'Your prompt here', experimental_transform: smoothStream({ chunking: /[\u4E00-\u9FFF]|\S+\s+/, }), }); ``` For these languages you could pass your own language aware chunking function: - Vietnamese - Thai - Javanese (Aksara Jawa) #### Regex based chunking To use regex based chunking, pass a `RegExp` to the `chunking` option. ```ts // To split on underscores: smoothStream({ chunking: /_+/, }); // Also can do it like this, same behavior smoothStream({ chunking: /[^_]*_/, }); ``` #### Custom callback chunking To use a custom callback for chunking, pass a function to the `chunking` option. ```ts smoothStream({ chunking: text => { const findString = 'some string'; const index = text.indexOf(findString); if (index === -1) { return null; } return text.slice(0, index) + findString; }, }); ``` ### Returns Returns a `TransformStream` that: - Buffers incoming text chunks - Releases text when the chunking pattern is encountered - Adds configurable delays between chunks for smooth output - Passes through non-text chunks (like step-finish events) immediately --- File: /ai/content/docs/07-reference/01-ai-sdk-core/90-generate-id.mdx --- --- title: generateId description: Generate a unique identifier (API Reference) --- # `generateId()` Generates a unique identifier. You can optionally provide the length of the ID. This is the same id generator used by the AI SDK. ```ts import { generateId } from 'ai'; const id = generateId(); ``` ## Import <Snippet text={`import { generateId } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'size', type: 'number', description: 'The length of the generated ID. It defaults to 16. This parameter is deprecated and will be removed in the next major version.', }, ]} /> ### Returns A string representing the generated ID. ## See also - [`createIdGenerator()`](/docs/reference/ai-sdk-core/create-id-generator) --- File: /ai/content/docs/07-reference/01-ai-sdk-core/91-create-id-generator.mdx --- --- title: createIdGenerator description: Create a customizable unique identifier generator (API Reference) --- # `createIdGenerator()` Creates a customizable ID generator function. You can configure the alphabet, prefix, separator, and default size of the generated IDs. ```ts import { createIdGenerator } from 'ai'; const generateCustomId = createIdGenerator({ prefix: 'user', separator: '_', }); const id = generateCustomId(); // Example: "user_1a2b3c4d5e6f7g8h" ``` ## Import <Snippet text={`import { createIdGenerator } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'options', type: 'object', description: 'Optional configuration object with the following properties:', }, { name: 'options.alphabet', type: 'string', description: 'The characters to use for generating the random part of the ID. Defaults to alphanumeric characters (0-9, A-Z, a-z).', }, { name: 'options.prefix', type: 'string', description: 'A string to prepend to all generated IDs. Defaults to none.', }, { name: 'options.separator', type: 'string', description: 'The character(s) to use between the prefix and the random part. Defaults to "-".', }, { name: 'options.size', type: 'number', description: 'The default length of the random part of the ID. Defaults to 16.', }, ]} /> ### Returns Returns a function that generates IDs based on the configured options. ### Notes - The generator uses non-secure random generation and should not be used for security-critical purposes. - The separator character must not be part of the alphabet to ensure reliable prefix checking. ## Example ```ts // Create a custom ID generator for user IDs const generateUserId = createIdGenerator({ prefix: 'user', separator: '_', size: 8, }); // Generate IDs const id1 = generateUserId(); // e.g., "user_1a2b3c4d" ``` ## See also - [`generateId()`](/docs/reference/ai-sdk-core/generate-id) --- File: /ai/content/docs/07-reference/01-ai-sdk-core/index.mdx --- --- title: AI SDK Core description: Reference documentation for the AI SDK Core collapsed: true --- # AI SDK Core [AI SDK Core](/docs/ai-sdk-core) is a set of functions that allow you to interact with language models and other AI models. These functions are designed to be easy-to-use and flexible, allowing you to generate text, structured data, and embeddings from language models and other AI models. AI SDK Core contains the following main functions: <IndexCards cards={[ { title: 'generateText()', description: 'Generate text and call tools from a language model.', href: '/docs/reference/ai-sdk-core/generate-text', }, { title: 'streamText()', description: 'Stream text and call tools from a language model.', href: '/docs/reference/ai-sdk-core/stream-text', }, { title: 'generateObject()', description: 'Generate structured data from a language model.', href: '/docs/reference/ai-sdk-core/generate-object', }, { title: 'streamObject()', description: 'Stream structured data from a language model.', href: '/docs/reference/ai-sdk-core/stream-object', }, { title: 'embed()', description: 'Generate an embedding for a single value using an embedding model.', href: '/docs/reference/ai-sdk-core/embed', }, { title: 'embedMany()', description: 'Generate embeddings for several values using an embedding model (batch embedding).', href: '/docs/reference/ai-sdk-core/embed-many', }, { title: 'experimental_generateImage()', description: 'Generate images based on a given prompt using an image model.', href: '/docs/reference/ai-sdk-core/generate-image', }, { title: 'experimental_transcribe()', description: 'Generate a transcript from an audio file.', href: '/docs/reference/ai-sdk-core/transcribe', }, { title: 'experimental_generateSpeech()', description: 'Generate speech audio from text.', href: '/docs/reference/ai-sdk-core/generate-speech', }, ]} /> It also contains the following helper functions: <IndexCards cards={[ { title: 'tool()', description: 'Type inference helper function for tools.', href: '/docs/reference/ai-sdk-core/tool', }, { title: 'experimental_createMCPClient()', description: 'Creates a client for connecting to MCP servers.', href: '/docs/reference/ai-sdk-core/create-mcp-client', }, { title: 'jsonSchema()', description: 'Creates AI SDK compatible JSON schema objects.', href: '/docs/reference/ai-sdk-core/json-schema', }, { title: 'zodSchema()', description: 'Creates AI SDK compatible Zod schema objects.', href: '/docs/reference/ai-sdk-core/zod-schema', }, { title: 'createProviderRegistry()', description: 'Creates a registry for using models from multiple providers.', href: '/docs/reference/ai-sdk-core/provider-registry', }, { title: 'cosineSimilarity()', description: 'Calculates the cosine similarity between two vectors, e.g. embeddings.', href: '/docs/reference/ai-sdk-core/cosine-similarity', }, { title: 'simulateReadableStream()', description: 'Creates a ReadableStream that emits values with configurable delays.', href: '/docs/reference/ai-sdk-core/simulate-readable-stream', }, { title: 'wrapLanguageModel()', description: 'Wraps a language model with middleware.', href: '/docs/reference/ai-sdk-core/wrap-language-model', }, { title: 'extractReasoningMiddleware()', description: 'Extracts reasoning from the generated text and exposes it as a `reasoning` property on the result.', href: '/docs/reference/ai-sdk-core/extract-reasoning-middleware', }, { title: 'simulateStreamingMiddleware()', description: 'Simulates streaming behavior with responses from non-streaming language models.', href: '/docs/reference/ai-sdk-core/simulate-streaming-middleware', }, { title: 'defaultSettingsMiddleware()', description: 'Applies default settings to a language model.', href: '/docs/reference/ai-sdk-core/default-settings-middleware', }, { title: 'smoothStream()', description: 'Smooths text streaming output.', href: '/docs/reference/ai-sdk-core/smooth-stream', }, { title: 'generateId()', description: 'Helper function for generating unique IDs', href: '/docs/reference/ai-sdk-core/generate-id', }, { title: 'createIdGenerator()', description: 'Creates an ID generator', href: '/docs/reference/ai-sdk-core/create-id-generator', }, ]} /> --- File: /ai/content/docs/07-reference/02-ai-sdk-ui/01-use-chat.mdx --- --- title: useChat description: API reference for the useChat hook. --- # `useChat()` Allows you to easily create a conversational user interface for your chatbot application. It enables the streaming of chat messages from your AI provider, manages the chat state, and updates the UI automatically as new messages are received. <Note> The `useChat` API has been significantly updated in AI SDK 5.0. It now uses a transport-based architecture and no longer manages input state internally. See the [migration guide](/docs/migration-guides/migration-guide-5-0#usechat-changes) for details. </Note> ## Import <Tabs items={['React', 'Svelte', 'Vue']}> <Tab> <Snippet text="import { useChat } from '@ai-sdk/react'" dark prompt={false} /> </Tab> <Tab> <Snippet text="import { Chat } from '@ai-sdk/svelte'" dark prompt={false} /> </Tab> <Tab> <Snippet text="import { Chat } from '@ai-sdk/vue'" dark prompt={false} /> </Tab> </Tabs> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'chat', type: 'Chat<UIMessage>', isOptional: true, description: 'An existing Chat instance to use. If provided, other parameters are ignored.', }, { name: 'transport', type: 'ChatTransport', isOptional: true, description: 'The transport to use for sending messages. Defaults to DefaultChatTransport with `/api/chat` endpoint.', properties: [ { type: 'DefaultChatTransport', parameters: [ { name: 'api', type: "string = '/api/chat'", isOptional: true, description: 'The API endpoint for chat requests.', }, { name: 'credentials', type: 'RequestCredentials', isOptional: true, description: 'The credentials mode for fetch requests.', }, { name: 'headers', type: 'Record<string, string> | Headers', isOptional: true, description: 'HTTP headers to send with requests.', }, { name: 'body', type: 'object', isOptional: true, description: 'Extra body object to send with requests.', }, { name: 'prepareSendMessagesRequest', type: 'PrepareSendMessagesRequest', isOptional: true, description: 'A function to customize the request before chat API calls.', properties: [ { type: 'PrepareSendMessagesRequest', parameters: [ { name: 'options', type: 'PrepareSendMessageRequestOptions', description: 'Options for preparing the request', properties: [ { type: 'PrepareSendMessageRequestOptions', parameters: [ { name: 'id', type: 'string', description: 'The chat ID', }, { name: 'messages', type: 'UIMessage[]', description: 'Current messages in the chat', }, { name: 'requestMetadata', type: 'unknown', description: 'The request metadata', }, { name: 'body', type: 'Record<string, any> | undefined', description: 'The request body', }, { name: 'credentials', type: 'RequestCredentials | undefined', description: 'The request credentials', }, { name: 'headers', type: 'HeadersInit | undefined', description: 'The request headers', }, { name: 'api', type: 'string', description: `The API endpoint to use for the request. If not specified, it defaults to the transport’s API endpoint: /api/chat.`, }, { name: 'trigger', type: "'submit-message' | 'regenerate-message'", description: 'The trigger for the request', }, { name: 'messageId', type: 'string | undefined', description: 'The message ID if applicable', }, ], }, ], }, ], }, ], }, { name: 'prepareReconnectToStreamRequest', type: 'PrepareReconnectToStreamRequest', isOptional: true, description: 'A function to customize the request before reconnect API call.', properties: [ { type: 'PrepareReconnectToStreamRequest', parameters: [ { name: 'options', type: 'PrepareReconnectToStreamRequestOptions', description: 'Options for preparing the reconnect request', properties: [ { type: 'PrepareReconnectToStreamRequestOptions', parameters: [ { name: 'id', type: 'string', description: 'The chat ID', }, { name: 'requestMetadata', type: 'unknown', description: 'The request metadata', }, { name: 'body', type: 'Record<string, any> | undefined', description: 'The request body', }, { name: 'credentials', type: 'RequestCredentials | undefined', description: 'The request credentials', }, { name: 'headers', type: 'HeadersInit | undefined', description: 'The request headers', }, { name: 'api', type: 'string', description: `The API endpoint to use for the request. If not specified, it defaults to the transport’s API endpoint combined with the chat ID: /api/chat/{chatId}/stream.`, }, ], }, ], }, ], }, ], }, ], }, ], }, { name: 'id', type: 'string', isOptional: true, description: 'A unique identifier for the chat. If not provided, a random one will be generated.', }, { name: 'messages', type: 'UIMessage[]', isOptional: true, description: 'Initial chat messages to populate the conversation with.', }, { name: 'onToolCall', type: '({toolCall: ToolCall}) => void | Promise<void>', isOptional: true, description: 'Optional callback function that is invoked when a tool call is received. You must call addToolResult to provide the tool result.', }, { name: 'sendAutomaticallyWhen', type: '(options: { messages: UIMessage[] }) => boolean | PromiseLike<boolean>', isOptional: true, description: 'When provided, this function will be called when the stream is finished or a tool call is added to determine if the current messages should be resubmitted. You can use the lastAssistantMessageIsCompleteWithToolCalls helper for common scenarios.', }, { name: 'onFinish', type: '(options: {message: UIMessage}) => void', isOptional: true, description: 'Optional callback function that is called when the assistant message has finished streaming completely.', }, { name: 'onError', type: '(error: Error) => void', isOptional: true, description: 'Callback function to be called when an error is encountered.', }, { name: 'onData', type: '(dataPart: DataUIPart) => void', isOptional: true, description: 'Optional callback function that is called when a data part is received.', }, { name: 'experimental_throttle', type: 'number', isOptional: true, description: 'Custom throttle wait in ms for the chat messages and data updates. Default is undefined, which disables throttling.', }, { name: 'resume', type: 'boolean', isOptional: true, description: 'Whether to resume an ongoing chat generation stream. Defaults to false.', }, ]} /> ### Returns <PropertiesTable content={[ { name: 'id', type: 'string', description: 'The id of the chat.', }, { name: 'messages', type: 'UIMessage[]', description: 'The current array of chat messages.', properties: [ { type: 'UIMessage', parameters: [ { name: 'id', type: 'string', description: 'A unique identifier for the message.', }, { name: 'role', type: "'system' | 'user' | 'assistant'", description: 'The role of the message.', }, { name: 'parts', type: 'UIMessagePart[]', description: 'The parts of the message. Use this for rendering the message in the UI.', }, { name: 'metadata', type: 'unknown', isOptional: true, description: 'The metadata of the message.', }, ], }, ], }, { name: 'status', type: "'submitted' | 'streaming' | 'ready' | 'error'", description: 'The current status of the chat: "ready" (idle), "submitted" (request sent), "streaming" (receiving response), or "error" (request failed).', }, { name: 'error', type: 'Error | undefined', description: 'The error object if an error occurred.', }, { name: 'sendMessage', type: '(message: CreateUIMessage | string, options?: ChatRequestOptions) => void', description: 'Function to send a new message to the chat. This will trigger an API call to generate the assistant response.', properties: [ { type: 'ChatRequestOptions', parameters: [ { name: 'headers', type: 'Record<string, string> | Headers', description: 'Additional headers that should be to be passed to the API endpoint.', }, { name: 'body', type: 'object', description: 'Additional body JSON properties that should be sent to the API endpoint.', }, { name: 'data', type: 'JSONValue', description: 'Additional data to be sent to the API endpoint.', }, ], }, ], }, { name: 'regenerate', type: '(options?: { messageId?: string }) => void', description: 'Function to regenerate the last assistant message or a specific message. If no messageId is provided, regenerates the last assistant message.', }, { name: 'stop', type: '() => void', description: 'Function to abort the current streaming response from the assistant.', }, { name: 'clearError', type: '() => void', description: 'Clears the error state.', }, { name: 'resumeStream', type: '() => void', description: 'Function to resume an interrupted streaming response. Useful when a network error occurs during streaming.', }, { name: 'addToolResult', type: '(options: { tool: string; toolCallId: string; output: unknown }) => void', description: 'Function to add a tool result to the chat. This will update the chat messages with the tool result. If sendAutomaticallyWhen is configured, it may trigger an automatic submission.', }, { name: 'setMessages', type: '(messages: UIMessage[] | ((messages: UIMessage[]) => UIMessage[])) => void', description: 'Function to update the messages state locally without triggering an API call. Useful for optimistic updates.', }, ]} /> ## Learn more - [Chatbot](/docs/ai-sdk-ui/chatbot) - [Chatbot with Tools](/docs/ai-sdk-ui/chatbot-with-tool-calling) - [UIMessage](/docs/reference/ai-sdk-core/ui-message) --- File: /ai/content/docs/07-reference/02-ai-sdk-ui/02-use-completion.mdx --- --- title: useCompletion description: API reference for the useCompletion hook. --- # `useCompletion()` Allows you to create text completion based capabilities for your application. It enables the streaming of text completions from your AI provider, manages the state for chat input, and updates the UI automatically as new messages are received. ## Import <Tabs items={['React', 'Svelte', 'Vue']}> <Tab> <Snippet text="import { useCompletion } from '@ai-sdk/react'" dark prompt={false} /> </Tab> <Tab> <Snippet text="import { Completion } from '@ai-sdk/svelte'" dark prompt={false} /> </Tab> <Tab> <Snippet text="import { useCompletion } from '@ai-sdk/vue'" dark prompt={false} /> </Tab> </Tabs> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'api', type: "string = '/api/completion'", description: 'The API endpoint that is called to generate text. It can be a relative path (starting with `/`) or an absolute URL.', }, { name: 'id', type: 'string', description: 'An unique identifier for the completion. If not provided, a random one will be generated. When provided, the `useCompletion` hook with the same `id` will have shared states across components. This is useful when you have multiple components showing the same chat stream', }, { name: 'initialInput', type: 'string', description: 'An optional string for the initial prompt input.', }, { name: 'initialCompletion', type: 'string', description: 'An optional string for the initial completion result.', }, { name: 'onResponse', type: '(response: Response) => void', description: 'An optional callback function that is called with the response from the API endpoint. Useful for throwing customized errors or logging.', }, { name: 'onFinish', type: '(prompt: string, completion: string) => void', description: 'An optional callback function that is called when the completion stream ends.', }, { name: 'onError', type: '(error: Error) => void', description: 'An optional callback that will be called when the chat stream encounters an error.', }, { name: 'headers', type: 'Record<string, string> | Headers', description: 'An optional object of headers to be passed to the API endpoint.', }, { name: 'body', type: 'any', description: 'An optional, additional body object to be passed to the API endpoint.', }, { name: 'credentials', type: "'omit' | 'same-origin' | 'include'", description: 'An optional literal that sets the mode of credentials to be used on the request. Defaults to same-origin.', }, { name: 'streamProtocol', type: "'text' | 'data'", isOptional: true, description: 'An optional literal that sets the type of stream to be used. Defaults to `data`. If set to `text`, the stream will be treated as a text stream.', }, { name: 'fetch', type: 'FetchFunction', isOptional: true, description: 'Optional. A custom fetch function to be used for the API call. Defaults to the global fetch function.', }, { name: 'experimental_throttle', type: 'number', isOptional: true, description: 'React only. Custom throttle wait time in milliseconds for the completion and data updates. When specified, throttles how often the UI updates during streaming. Default is undefined, which disables throttling.', }, ]} /> ### Returns <PropertiesTable content={[ { name: 'completion', type: 'string', description: 'The current text completion.', }, { name: 'complete', type: '(prompt: string, options: { headers, body }) => void', description: 'Function to execute text completion based on the provided prompt.', }, { name: 'error', type: 'undefined | Error', description: 'The error thrown during the completion process, if any.', }, { name: 'setCompletion', type: '(completion: string) => void', description: 'Function to update the `completion` state.', }, { name: 'stop', type: '() => void', description: 'Function to abort the current API request.', }, { name: 'input', type: 'string', description: 'The current value of the input field.', }, { name: 'setInput', type: 'React.Dispatch<React.SetStateAction<string>>', description: 'The current value of the input field.', }, { name: 'handleInputChange', type: '(event: any) => void', description: "Handler for the `onChange` event of the input field to control the input's value.", }, { name: 'handleSubmit', type: '(event?: { preventDefault?: () => void }) => void', description: 'Form submission handler that automatically resets the input field and appends a user message.', }, { name: 'isLoading', type: 'boolean', description: 'Boolean flag indicating whether a fetch operation is currently in progress.', }, ]} /> --- File: /ai/content/docs/07-reference/02-ai-sdk-ui/03-use-object.mdx --- --- title: useObject description: API reference for the useObject hook. --- # `experimental_useObject()` <Note> `useObject` is an experimental feature and only available in React and Svelte. </Note> Allows you to consume text streams that represent a JSON object and parse them into a complete object based on a schema. You can use it together with [`streamObject`](/docs/reference/ai-sdk-core/stream-object) in the backend. ```tsx 'use client'; import { experimental_useObject as useObject } from '@ai-sdk/react'; export default function Page() { const { object, submit } = useObject({ api: '/api/use-object', schema: z.object({ content: z.string() }), }); return ( <div> <button onClick={() => submit('example input')}>Generate</button> {object?.content && <p>{object.content}</p>} </div> ); } ``` ## Import <Snippet text="import { experimental_useObject as useObject } from '@ai-sdk/react'" dark prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'api', type: 'string', description: 'The API endpoint that is called to generate objects. It should stream JSON that matches the schema as chunked text. It can be a relative path (starting with `/`) or an absolute URL.', }, { name: 'schema', type: 'Zod Schema | JSON Schema', description: 'A schema that defines the shape of the complete object. You can either pass in a Zod schema or a JSON schema (using the `jsonSchema` function).', }, { name: 'id?', type: 'string', description: 'A unique identifier. If not provided, a random one will be generated. When provided, the `useObject` hook with the same `id` will have shared states across components.', }, { name: 'initialValue', type: 'DeepPartial<RESULT> | undefined', isOptional: true, description: 'An value for the initial object. Optional.', }, { name: 'fetch', type: 'FetchFunction', isOptional: true, description: 'A custom fetch function to be used for the API call. Defaults to the global fetch function. Optional.', }, { name: 'headers', type: 'Record<string, string> | Headers', isOptional: true, description: 'A headers object to be passed to the API endpoint. Optional.', }, { name: 'credentials', type: 'RequestCredentials', isOptional: true, description: 'The credentials mode to be used for the fetch request. Possible values are: "omit", "same-origin", "include". Optional.', }, { name: 'onError', type: '(error: Error) => void', isOptional: true, description: 'Callback function to be called when an error is encountered. Optional.', }, { name: 'onFinish', type: '(result: OnFinishResult) => void', isOptional: true, description: 'Called when the streaming response has finished.', properties: [ { type: 'OnFinishResult', parameters: [ { name: 'object', type: 'T | undefined', description: 'The generated object (typed according to the schema). Can be undefined if the final object does not match the schema.', }, { name: 'error', type: 'unknown | undefined', description: 'Optional error object. This is e.g. a TypeValidationError when the final object does not match the schema.', }, ], }, ], }, ]} /> ### Returns <PropertiesTable content={[ { name: 'submit', type: '(input: INPUT) => void', description: 'Calls the API with the provided input as JSON body.', }, { name: 'object', type: 'DeepPartial<RESULT> | undefined', description: 'The current value for the generated object. Updated as the API streams JSON chunks.', }, { name: 'error', type: 'Error | unknown', description: 'The error object if the API call fails.', }, { name: 'isLoading', type: 'boolean', description: 'Boolean flag indicating whether a request is currently in progress.', }, { name: 'stop', type: '() => void', description: 'Function to abort the current API request.', }, { name: 'clear', type: '() => void', description: 'Function to clear the object state.', }, ]} /> ## Examples <ExampleLinks examples={[ { title: 'Streaming Object Generation with useObject', link: '/examples/next-pages/basics/streaming-object-generation', }, ]} /> --- File: /ai/content/docs/07-reference/02-ai-sdk-ui/31-convert-to-model-messages.mdx --- --- title: convertToModelMessages description: Convert useChat messages to ModelMessages for AI functions (API Reference) --- # `convertToModelMessages()` The `convertToModelMessages` function is used to transform an array of UI messages from the `useChat` hook into an array of `ModelMessage` objects. These `ModelMessage` objects are compatible with AI core functions like `streamText`. ```ts filename="app/api/chat/route.ts" import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText } from 'ai'; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } ``` ## Import <Snippet text={`import { convertToModelMessages } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'messages', type: 'Message[]', description: 'An array of UI messages from the useChat hook to be converted', }, { name: 'options', type: '{ tools?: ToolSet }', description: 'Optional configuration object. Provide tools to enable multi-modal tool responses.', }, ]} /> ### Returns An array of [`ModelMessage`](/docs/reference/ai-sdk-core/model-message) objects. <PropertiesTable content={[ { name: 'ModelMessage[]', type: 'Array', description: 'An array of ModelMessage objects', }, ]} /> ## Multi-modal Tool Responses The `convertToModelMessages` function supports tools that can return multi-modal content. This is useful when tools need to return non-text content like images. ```ts import { tool } from 'ai'; import { z } from 'zod'; const screenshotTool = tool({ parameters: z.object({}), execute: async () => 'imgbase64', toModelOutput: result => [{ type: 'image', data: result }], }); const result = streamText({ model: openai('gpt-4'), messages: convertToModelMessages(messages, { tools: { screenshot: screenshotTool, }, }), }); ``` Tools can implement the optional `toModelOutput` method to transform their results into multi-modal content. The content is an array of content parts, where each part has a `type` (e.g., 'text', 'image') and corresponding data. --- File: /ai/content/docs/07-reference/02-ai-sdk-ui/40-create-ui-message-stream.mdx --- --- title: createUIMessageStream description: API Reference for createUIMessageStream. --- # `createUIMessageStream` The `createUIMessageStream` function allows you to create a readable stream for UI messages with advanced features like message merging, error handling, and finish callbacks. ## Import <Snippet text={`import { createUIMessageStream } from "ai"`} prompt={false} /> ## Example ```tsx const existingMessages: UIMessage[] = [ /* ... */ ]; const stream = createUIMessageStream({ async execute({ writer }) { // Write a message chunk writer.write({ type: 'text-delta', textDelta: 'Hello', }); // Merge another stream from streamText const result = streamText({ model: openai('gpt-4o'), prompt: 'Write a haiku about AI', }); writer.merge(result.toUIMessageStream()); }, onError: error => `Custom error: ${error.message}`, originalMessages: existingMessages, onFinish: ({ messages, isContinuation, responseMessage }) => { console.log('Stream finished with messages:', messages); }, }); ``` ## API Signature ### Parameters <PropertiesTable content={[ { name: 'execute', type: '(options: { writer: UIMessageStreamWriter }) => Promise<void> | void', description: 'A function that receives a writer instance and can use it to write UI message chunks to the stream.', properties: [ { type: 'UIMessageStreamWriter', parameters: [ { name: 'write', type: '(part: UIMessageChunk) => void', description: 'Writes a UI message chunk to the stream.', }, { name: 'merge', type: '(stream: ReadableStream<UIMessageChunk>) => void', description: 'Merges the contents of another UI message stream into this stream.', }, { name: 'onError', type: '(error: unknown) => string', description: 'Error handler that is used by the stream writer for handling errors in merged streams.', }, ], }, ], }, { name: 'onError', type: '(error: unknown) => string', description: 'A function that handles errors and returns an error message string. By default, it returns the error message.', }, { name: 'originalMessages', type: 'UIMessage[] | undefined', description: 'The original messages. If provided, persistence mode is assumed and a message ID is provided for the response message.', }, { name: 'onFinish', type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage }) => void | undefined', description: 'A callback function that is called when the stream finishes.', properties: [ { type: 'FinishOptions', parameters: [ { name: 'messages', type: 'UIMessage[]', description: 'The updated list of UI messages.', }, { name: 'isContinuation', type: 'boolean', description: 'Indicates whether the response message is a continuation of the last original message, or if a new message was created.', }, { name: 'responseMessage', type: 'UIMessage', description: 'The message that was sent to the client as a response (including the original message if it was extended).', }, ], }, ], }, { name: 'generateId', type: 'IdGenerator | undefined', description: 'A function to generate unique IDs for messages. Uses the default ID generator if not provided.', }, ]} /> ### Returns `ReadableStream<UIMessageChunk>` A readable stream that emits UI message chunks. The stream automatically handles error propagation, merging of multiple streams, and proper cleanup when all operations are complete. --- File: /ai/content/docs/07-reference/02-ai-sdk-ui/41-create-ui-message-stream-response.mdx --- --- title: createUIMessageStreamResponse description: API Reference for createUIMessageStreamResponse. --- # `createUIMessageStreamResponse` The `createUIMessageStreamResponse` function creates a Response object that streams UI messages to the client. ## Import <Snippet text={`import { createUIMessageStreamResponse } from "ai"`} prompt={false} /> ## Example ```tsx import { createUIMessageStream, createUIMessageStreamResponse } from 'ai'; const response = createUIMessageStreamResponse({ status: 200, statusText: 'OK', headers: { 'Custom-Header': 'value', }, stream: createUIMessageStream({ execute({ writer }) { // Write custom data writer.write({ type: 'data', value: { message: 'Hello' }, }); // Write text content writer.write({ type: 'text', value: 'Hello, world!', }); // Write source information writer.write({ type: 'source-url', value: { type: 'source', id: 'source-1', url: 'https://example.com', title: 'Example Source', }, }); // Merge with LLM stream const result = streamText({ model: openai('gpt-4'), prompt: 'Say hello', }); writer.merge(result.toUIMessageStream()); }, }), }); ``` ## API Signature ### Parameters <PropertiesTable content={[ { name: 'stream', type: 'ReadableStream<UIMessageChunk>', description: 'The UI message stream to send to the client.', }, { name: 'status', type: 'number', isOptional: true, description: 'The status code for the response. Defaults to 200.', }, { name: 'statusText', type: 'string', isOptional: true, description: 'The status text for the response.', }, { name: 'headers', type: 'Headers | Record<string, string>', isOptional: true, description: 'Additional headers for the response.', }, { name: 'consumeSseStream', type: '(options: { stream: ReadableStream<string> }) => PromiseLike<void> | void', isOptional: true, description: 'Optional callback to consume the Server-Sent Events stream.', }, ]} /> ### Returns `Response` A Response object that streams UI message chunks with the specified status, headers, and content. --- File: /ai/content/docs/07-reference/02-ai-sdk-ui/42-pipe-ui-message-stream-to-response.mdx --- --- title: pipeUIMessageStreamToResponse description: Learn to use pipeUIMessageStreamToResponse helper function to pipe streaming data to a ServerResponse object. --- # `pipeUIMessageStreamToResponse` The `pipeUIMessageStreamToResponse` function pipes streaming data to a Node.js ServerResponse object (see [Streaming Data](/docs/ai-sdk-ui/streaming-data)). ## Import <Snippet text={`import { pipeUIMessageStreamToResponse } from "ai"`} prompt={false} /> ## Example ```tsx pipeUIMessageStreamToResponse({ response: serverResponse, status: 200, statusText: 'OK', headers: { 'Custom-Header': 'value', }, stream: myUIMessageStream, consumeSseStream: ({ stream }) => { // Optional: consume the SSE stream independently console.log('Consuming SSE stream:', stream); }, }); ``` ## API Signature ### Parameters <PropertiesTable content={[ { name: 'response', type: 'ServerResponse', description: 'The Node.js ServerResponse object to pipe the data to.', }, { name: 'stream', type: 'ReadableStream<UIMessageChunk>', description: 'The UI message stream to pipe to the response.', }, { name: 'status', type: 'number', description: 'The status code for the response.', }, { name: 'statusText', type: 'string', description: 'The status text for the response.', }, { name: 'headers', type: 'Headers | Record<string, string>', description: 'Additional headers for the response.', }, { name: 'consumeSseStream', type: '({ stream }: { stream: ReadableStream }) => void', description: 'Optional function to consume the SSE stream independently. The stream is teed and this function receives a copy.', }, ]} /> --- File: /ai/content/docs/07-reference/02-ai-sdk-ui/43-read-ui-message-stream.mdx --- --- title: readUIMessageStream description: API Reference for readUIMessageStream. --- # readUIMessageStream Transforms a stream of `UIMessageChunk`s into an `AsyncIterableStream` of `UIMessage`s. UI message streams are useful outside of Chat use cases, e.g. for terminal UIs, custom stream consumption on the client, or RSC (React Server Components). ## Import ```tsx import { readUIMessageStream } from 'ai'; ``` ## API Signature ### Parameters <PropertiesTable content={[ { name: 'message', type: 'UIMessage', isOptional: true, description: 'The last assistant message to use as a starting point when the conversation is resumed. Otherwise undefined.', }, { name: 'stream', type: 'ReadableStream<UIMessageChunk>', description: 'The stream of UIMessageChunk objects to read.', }, { name: 'onError', type: '(error: unknown) => void', isOptional: true, description: 'A function that is called when an error occurs during stream processing.', }, { name: 'terminateOnError', type: 'boolean', isOptional: true, description: 'Whether to terminate the stream if an error occurs. Defaults to false.', }, ]} /> ### Returns An `AsyncIterableStream` of `UIMessage`s. Each stream part represents a different state of the same message as it is being completed. For comprehensive examples and use cases, see [Reading UI Message Streams](/docs/ai-sdk-ui/reading-ui-message-streams). --- File: /ai/content/docs/07-reference/02-ai-sdk-ui/46-infer-ui-tools.mdx --- --- title: InferUITools description: API Reference for InferUITools. --- # InferUITools Infers the input and output types of a `ToolSet`. This type helper is useful when working with tools in TypeScript to ensure type safety for your tool inputs and outputs in `UIMessage`s. ## Import ```tsx import { InferUITools } from 'ai'; ``` ## API Signature ### Type Parameters <PropertiesTable content={[ { name: 'TOOLS', type: 'ToolSet', description: 'The tool set to infer types from.', }, ]} /> ### Returns A type that maps each tool in the tool set to its inferred input and output types. The resulting type has the shape: ```typescript { [NAME in keyof TOOLS & string]: { input: InferToolInput<TOOLS[NAME]>; output: InferToolOutput<TOOLS[NAME]>; }; } ``` ## Examples ### Basic Usage ```tsx import { InferUITools } from 'ai'; import { z } from 'zod'; const tools = { weather: { description: 'Get the current weather', parameters: z.object({ location: z.string().describe('The city and state'), }), execute: async ({ location }) => { return `The weather in ${location} is sunny.`; }, }, calculator: { description: 'Perform basic arithmetic', parameters: z.object({ operation: z.enum(['add', 'subtract', 'multiply', 'divide']), a: z.number(), b: z.number(), }), execute: async ({ operation, a, b }) => { switch (operation) { case 'add': return a + b; case 'subtract': return a - b; case 'multiply': return a * b; case 'divide': return a / b; } }, }, }; // Infer the types from the tool set type MyUITools = InferUITools<typeof tools>; // This creates a type with: // { // weather: { input: { location: string }; output: string }; // calculator: { input: { operation: 'add' | 'subtract' | 'multiply' | 'divide'; a: number; b: number }; output: number }; // } ``` ## Related - [`InferUITool`](/docs/reference/ai-sdk-ui/infer-ui-tool) - Infer types for a single tool - [`useChat`](/docs/reference/ai-sdk-ui/use-chat) - Chat hook that supports typed tools --- File: /ai/content/docs/07-reference/02-ai-sdk-ui/47-infer-ui-tool.mdx --- --- title: InferUITool description: API Reference for InferUITool. --- # InferUITool Infers the input and output types of a tool. This type helper is useful when working with individual tools to ensure type safety for your tool inputs and outputs in `UIMessage`s. ## Import ```tsx import { InferUITool } from 'ai'; ``` ## API Signature ### Type Parameters <PropertiesTable content={[ { name: 'TOOL', type: 'Tool', description: 'The tool to infer types from.', }, ]} /> ### Returns A type that contains the inferred input and output types of the tool. The resulting type has the shape: ```typescript { input: InferToolInput<TOOL>; output: InferToolOutput<TOOL>; } ``` ## Examples ### Basic Usage ```tsx import { InferUITool } from 'ai'; import { z } from 'zod'; const weatherTool = { description: 'Get the current weather', parameters: z.object({ location: z.string().describe('The city and state'), }), execute: async ({ location }) => { return `The weather in ${location} is sunny.`; }, }; // Infer the types from the tool type WeatherUITool = InferUITool<typeof weatherTool>; // This creates a type with: // { // input: { location: string }; // output: string; // } ``` ## Related - [`InferUITools`](/docs/reference/ai-sdk-ui/infer-ui-tools) - Infer types for a tool set - [`ToolUIPart`](/docs/reference/ai-sdk-ui/tool-ui-part) - Tool part type for UI messages --- File: /ai/content/docs/07-reference/02-ai-sdk-ui/index.mdx --- --- title: AI SDK UI description: Reference documentation for the AI SDK UI collapsed: true --- # AI SDK UI [AI SDK UI](/docs/ai-sdk-ui) is designed to help you build interactive chat, completion, and assistant applications with ease. It is framework-agnostic toolkit, streamlining the integration of advanced AI functionalities into your applications. AI SDK UI contains the following hooks: <IndexCards cards={[ { title: 'useChat', description: 'Use a hook to interact with language models in a chat interface.', href: '/docs/reference/ai-sdk-ui/use-chat', }, { title: 'useCompletion', description: 'Use a hook to interact with language models in a completion interface.', href: '/docs/reference/ai-sdk-ui/use-completion', }, { title: 'useObject', description: 'Use a hook for consuming a streamed JSON objects.', href: '/docs/reference/ai-sdk-ui/use-object', }, { title: 'convertToModelMessages', description: 'Convert useChat messages to ModelMessages for AI functions.', href: '/docs/reference/ai-sdk-ui/convert-to-model-messages', }, { title: 'createUIMessageStream', description: 'Create a UI message stream to stream additional data to the client.', href: '/docs/reference/ai-sdk-ui/create-ui-message-stream', }, { title: 'createUIMessageStreamResponse', description: 'Create a response object to stream UI messages to the client.', href: '/docs/reference/ai-sdk-ui/create-ui-message-stream-response', }, { title: 'pipeUIMessageStreamToResponse', description: 'Pipe a UI message stream to a Node.js ServerResponse object.', href: '/docs/reference/ai-sdk-ui/pipe-ui-message-stream-to-response', }, { title: 'readUIMessageStream', description: 'Transform a stream of UIMessageChunk objects into an AsyncIterableStream of UIMessage objects.', href: '/docs/reference/ai-sdk-ui/read-ui-message-stream', }, ]} /> ## UI Framework Support AI SDK UI supports the following frameworks: [React](https://react.dev/), [Svelte](https://svelte.dev/), and [Vue.js](https://vuejs.org/). Here is a comparison of the supported functions across these frameworks: | Function | React | Svelte | Vue.js | | --------------------------------------------------------- | ------------------- | ------------------------------------ | ------------------- | | [useChat](/docs/reference/ai-sdk-ui/use-chat) | <Check size={18} /> | <Check size={18} /> Chat | <Check size={18} /> | | [useCompletion](/docs/reference/ai-sdk-ui/use-completion) | <Check size={18} /> | <Check size={18} /> Completion | <Check size={18} /> | | [useObject](/docs/reference/ai-sdk-ui/use-object) | <Check size={18} /> | <Check size={18} /> StructuredObject | <Cross size={18} /> | <Note> [Contributions](https://github.com/vercel/ai/blob/main/CONTRIBUTING.md) are welcome to implement missing features for non-React frameworks. </Note> --- File: /ai/content/docs/07-reference/03-ai-sdk-rsc/01-stream-ui.mdx --- --- title: streamUI description: Reference for the streamUI function from the AI SDK RSC --- # `streamUI` <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> A helper function to create a streamable UI from LLM providers. This function is similar to AI SDK Core APIs and supports the same model interfaces. To see `streamUI` in action, check out [these examples](#examples). ## Import <Snippet text={`import { streamUI } from "@ai-sdk/rsc"`} prompt={false} /> ## Parameters <PropertiesTable content={[ { name: 'model', type: 'LanguageModel', description: 'The language model to use. Example: openai("gpt-4.1")', }, { name: 'initial', isOptional: true, type: 'ReactNode', description: 'The initial UI to render.', }, { name: 'system', type: 'string', description: 'The system prompt to use that specifies the behavior of the model.', }, { name: 'prompt', type: 'string', description: 'The input prompt to generate the text from.', }, { name: 'messages', type: 'Array<CoreSystemMessage | CoreUserMessage | CoreAssistantMessage | CoreToolMessage> | Array<UIMessage>', description: 'A list of messages that represent a conversation. Automatically converts UI messages from the useChat hook.', properties: [ { type: 'CoreSystemMessage', parameters: [ { name: 'role', type: "'system'", description: 'The role for the system message.', }, { name: 'content', type: 'string', description: 'The content of the message.', }, ], }, { type: 'CoreUserMessage', parameters: [ { name: 'role', type: "'user'", description: 'The role for the user message.', }, { name: 'content', type: 'string | Array<TextPart | ImagePart | FilePart>', description: 'The content of the message.', properties: [ { type: 'TextPart', parameters: [ { name: 'type', type: "'text'", description: 'The type of the message part.', }, { name: 'text', type: 'string', description: 'The text content of the message part.', }, ], }, { type: 'ImagePart', parameters: [ { name: 'type', type: "'image'", description: 'The type of the message part.', }, { name: 'image', type: 'string | Uint8Array | Buffer | ArrayBuffer | URL', description: 'The image content of the message part. String are either base64 encoded content, base64 data URLs, or http(s) URLs.', }, { name: 'mediaType', type: 'string', isOptional: true, description: 'The IANA media type of the image. Optional.', }, ], }, { type: 'FilePart', parameters: [ { name: 'type', type: "'file'", description: 'The type of the message part.', }, { name: 'data', type: 'string | Uint8Array | Buffer | ArrayBuffer | URL', description: 'The file content of the message part. String are either base64 encoded content, base64 data URLs, or http(s) URLs.', }, { name: 'mediaType', type: 'string', description: 'The IANA media type of the file.', }, ], }, ], }, ], }, { type: 'CoreAssistantMessage', parameters: [ { name: 'role', type: "'assistant'", description: 'The role for the assistant message.', }, { name: 'content', type: 'string | Array<TextPart | ToolCallPart>', description: 'The content of the message.', properties: [ { type: 'TextPart', parameters: [ { name: 'type', type: "'text'", description: 'The type of the message part.', }, { name: 'text', type: 'string', description: 'The text content of the message part.', }, ], }, { type: 'ToolCallPart', parameters: [ { name: 'type', type: "'tool-call'", description: 'The type of the message part.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call.', }, { name: 'toolName', type: 'string', description: 'The name of the tool, which typically would be the name of the function.', }, { name: 'args', type: 'object based on zod schema', description: 'Parameters generated by the model to be used by the tool.', }, ], }, ], }, ], }, { type: 'CoreToolMessage', parameters: [ { name: 'role', type: "'tool'", description: 'The role for the assistant message.', }, { name: 'content', type: 'Array<ToolResultPart>', description: 'The content of the message.', properties: [ { type: 'ToolResultPart', parameters: [ { name: 'type', type: "'tool-result'", description: 'The type of the message part.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call the result corresponds to.', }, { name: 'toolName', type: 'string', description: 'The name of the tool the result corresponds to.', }, { name: 'result', type: 'unknown', description: 'The result returned by the tool after execution.', }, { name: 'isError', type: 'boolean', isOptional: true, description: 'Whether the result is an error or an error message.', }, ], }, ], }, ], }, ], }, { name: 'maxOutputTokens', type: 'number', isOptional: true, description: 'Maximum number of tokens to generate.', }, { name: 'temperature', type: 'number', isOptional: true, description: 'Temperature setting. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both.', }, { name: 'topP', type: 'number', isOptional: true, description: 'Nucleus sampling. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both.', }, { name: 'topK', type: 'number', isOptional: true, description: 'Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature.', }, { name: 'presencePenalty', type: 'number', isOptional: true, description: 'Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The value is passed through to the provider. The range depends on the provider and model.', }, { name: 'frequencyPenalty', type: 'number', isOptional: true, description: 'Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The value is passed through to the provider. The range depends on the provider and model.', }, { name: 'stopSequences', type: 'string[]', isOptional: true, description: 'Sequences that will stop the generation of the text. If the model generates any of these sequences, it will stop generating further text.', }, { name: 'seed', type: 'number', isOptional: true, description: 'The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results.', }, { name: 'maxRetries', type: 'number', isOptional: true, description: 'Maximum number of retries. Set to 0 to disable retries. Default: 2.', }, { name: 'abortSignal', type: 'AbortSignal', isOptional: true, description: 'An optional abort signal that can be used to cancel the call.', }, { name: 'headers', type: 'Record<string, string>', isOptional: true, description: 'Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.', }, { name: 'tools', type: 'ToolSet', description: 'Tools that are accessible to and can be called by the model.', properties: [ { type: 'Tool', parameters: [ { name: 'description', isOptional: true, type: 'string', description: 'Information about the purpose of the tool including details on how and when it can be used by the model.', }, { name: 'parameters', type: 'zod schema', description: 'The typed schema that describes the parameters of the tool that can also be used to validation and error handling.', }, { name: 'generate', isOptional: true, type: '(async (parameters) => ReactNode) | AsyncGenerator<ReactNode, ReactNode, void>', description: 'A function or a generator function that is called with the arguments from the tool call and yields React nodes as the UI.', }, ], }, ], }, { name: 'toolChoice', isOptional: true, type: '"auto" | "none" | "required" | { "type": "tool", "toolName": string }', description: 'The tool choice setting. It specifies how tools are selected for execution. The default is "auto". "none" disables tool execution. "required" requires tools to be executed. { "type": "tool", "toolName": string } specifies a specific tool to execute.', }, { name: 'text', isOptional: true, type: '(Text) => ReactNode', description: 'Callback to handle the generated tokens from the model.', properties: [ { type: 'Text', parameters: [ { name: 'content', type: 'string', description: 'The full content of the completion.', }, { name: 'delta', type: 'string', description: 'The delta.' }, { name: 'done', type: 'boolean', description: 'Is it done?' }, ], }, ], }, { name: 'providerOptions', type: 'Record<string,Record<string,JSONValue>> | undefined', isOptional: true, description: 'Provider-specific options. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.', }, { name: 'onFinish', type: '(result: OnFinishResult) => void', isOptional: true, description: 'Callback that is called when the LLM response and all request tool executions (for tools that have a `generate` function) are finished.', properties: [ { type: 'OnFinishResult', parameters: [ { name: 'usage', type: 'TokenUsage', description: 'The token usage of the generated text.', properties: [ { type: 'TokenUsage', parameters: [ { name: 'promptTokens', type: 'number', description: 'The total number of tokens in the prompt.', }, { name: 'completionTokens', type: 'number', description: 'The total number of tokens in the completion.', }, { name: 'totalTokens', type: 'number', description: 'The total number of tokens generated.', }, ], }, ], }, { name: 'value', type: 'ReactNode', description: 'The final ui node that was generated.', }, { name: 'warnings', type: 'Warning[] | undefined', description: 'Warnings from the model provider (e.g. unsupported settings).', }, { name: 'response', type: 'Response', description: 'Optional response data.', properties: [ { type: 'Response', parameters: [ { name: 'headers', isOptional: true, type: 'Record<string, string>', description: 'Response headers.', }, ], }, ], }, ], }, ], }, ]} /> ## Returns <PropertiesTable content={[ { name: 'value', type: 'ReactNode', description: 'The user interface based on the stream output.', }, { name: 'response', type: 'Response', isOptional: true, description: 'Optional response data.', properties: [ { type: 'Response', parameters: [ { name: 'headers', isOptional: true, type: 'Record<string, string>', description: 'Response headers.', }, ], }, ], }, { name: 'warnings', type: 'Warning[] | undefined', description: 'Warnings from the model provider (e.g. unsupported settings).', }, { name: 'stream', type: 'AsyncIterable<StreamPart> & ReadableStream<StreamPart>', description: 'A stream with all events, including text deltas, tool calls, tool results, and errors. You can use it as either an AsyncIterable or a ReadableStream. When an error occurs, the stream will throw the error.', properties: [ { type: 'StreamPart', parameters: [ { name: 'type', type: "'text-delta'", description: 'The type to identify the object as text delta.', }, { name: 'textDelta', type: 'string', description: 'The text delta.', }, ], }, { type: 'StreamPart', parameters: [ { name: 'type', type: "'tool-call'", description: 'The type to identify the object as tool call.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call.', }, { name: 'toolName', type: 'string', description: 'The name of the tool, which typically would be the name of the function.', }, { name: 'args', type: 'object based on zod schema', description: 'Parameters generated by the model to be used by the tool.', }, ], }, { type: 'StreamPart', parameters: [ { name: 'type', type: "'error'", description: 'The type to identify the object as error.', }, { name: 'error', type: 'Error', description: 'Describes the error that may have occurred during execution.', }, ], }, { type: 'StreamPart', parameters: [ { name: 'type', type: "'finish'", description: 'The type to identify the object as finish.', }, { name: 'finishReason', type: "'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error' | 'other' | 'unknown'", description: 'The reason the model finished generating the text.', }, { name: 'usage', type: 'TokenUsage', description: 'The token usage of the generated text.', properties: [ { type: 'TokenUsage', parameters: [ { name: 'promptTokens', type: 'number', description: 'The total number of tokens in the prompt.', }, { name: 'completionTokens', type: 'number', description: 'The total number of tokens in the completion.', }, { name: 'totalTokens', type: 'number', description: 'The total number of tokens generated.', }, ], }, ], }, ], }, ], }, ]} /> ## Examples <ExampleLinks examples={[ { title: 'Learn to render a React component as a function call using a language model in Next.js', link: '/examples/next-app/state-management/ai-ui-states', }, { title: 'Learn to persist and restore states UI/AI states in Next.js', link: '/examples/next-app/state-management/save-and-restore-states', }, { title: 'Learn to route React components using a language model in Next.js', link: '/examples/next-app/interface/route-components', }, { title: 'Learn to stream component updates to the client in Next.js', link: '/examples/next-app/interface/stream-component-updates', }, ]} /> --- File: /ai/content/docs/07-reference/03-ai-sdk-rsc/02-create-ai.mdx --- --- title: createAI description: Reference for the createAI function from the AI SDK RSC --- # `createAI` <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> Creates a client-server context provider that can be used to wrap parts of your application tree to easily manage both UI and AI states of your application. ## Import <Snippet text={`import { createAI } from "@ai-sdk/rsc"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'actions', type: 'Record<string, Action>', description: 'Server side actions that can be called from the client.', }, { name: 'initialAIState', type: 'any', description: 'Initial AI state to be used in the client.', }, { name: 'initialUIState', type: 'any', description: 'Initial UI state to be used in the client.', }, { name: 'onGetUIState', type: '() => UIState', description: 'is called during SSR to compare and update UI state.', }, { name: 'onSetAIState', type: '(Event) => void', description: 'is triggered whenever an update() or done() is called by the mutable AI state in your action, so you can safely store your AI state in the database.', properties: [ { type: 'Event', parameters: [ { name: 'state', type: 'AIState', description: 'The resulting AI state after the update.', }, { name: 'done', type: 'boolean', description: 'Whether the AI state updates have been finalized or not.', }, ], }, ], }, ]} /> ### Returns It returns an `<AI/>` context provider. ## Examples <ExampleLinks examples={[ { title: 'Learn to manage AI and UI states in Next.js', link: '/examples/next-app/state-management/ai-ui-states', }, { title: 'Learn to persist and restore states UI/AI states in Next.js', link: '/examples/next-app/state-management/save-and-restore-states', }, ]} /> --- File: /ai/content/docs/07-reference/03-ai-sdk-rsc/03-create-streamable-ui.mdx --- --- title: createStreamableUI description: Reference for the createStreamableUI function from the AI SDK RSC --- # `createStreamableUI` <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> Create a stream that sends UI from the server to the client. On the client side, it can be rendered as a normal React node. ## Import <Snippet text={`import { createStreamableUI } from "@ai-sdk/rsc"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'initialValue', type: 'ReactNode', isOptional: true, description: 'The initial value of the streamable UI.', }, ]} /> ### Returns <PropertiesTable content={[ { name: 'value', type: 'ReactNode', description: 'The value of the streamable UI. This can be returned from a Server Action and received by the client.', }, ]} /> ### Methods <PropertiesTable content={[ { name: 'update', type: '(ReactNode) => void', description: 'Updates the current UI node. It takes a new UI node and replaces the old one.', }, { name: 'append', type: '(ReactNode) => void', description: 'Appends a new UI node to the end of the old one. Once appended a new UI node, the previous UI node cannot be updated anymore.', }, { name: 'done', type: '(ReactNode | null) => void', description: 'Marks the UI node as finalized and closes the stream. Once called, the UI node cannot be updated or appended anymore. This method is always required to be called, otherwise the response will be stuck in a loading state.', }, { name: 'error', type: '(Error) => void', description: 'Signals that there is an error in the UI stream. It will be thrown on the client side and caught by the nearest error boundary component.', }, ]} /> ## Examples <ExampleLinks examples={[ { title: 'Render a React component during a tool call', link: '/examples/next-app/tools/render-interface-during-tool-call', }, ]} /> --- File: /ai/content/docs/07-reference/03-ai-sdk-rsc/04-create-streamable-value.mdx --- --- title: createStreamableValue description: Reference for the createStreamableValue function from the AI SDK RSC --- # `createStreamableValue` <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> Create a stream that sends values from the server to the client. The value can be any serializable data. ## Import <Snippet text={`import { createStreamableValue } from "@ai-sdk/rsc"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'value', type: 'any', description: 'Any data that RSC supports. Example, JSON.', }, ]} /> ### Returns <PropertiesTable content={[ { name: 'value', type: 'streamable', description: 'This creates a special value that can be returned from Actions to the client. It holds the data inside and can be updated via the update method.', }, ]} /> --- File: /ai/content/docs/07-reference/03-ai-sdk-rsc/05-read-streamable-value.mdx --- --- title: readStreamableValue description: Reference for the readStreamableValue function from the AI SDK RSC --- # `readStreamableValue` <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> It is a function that helps you read the streamable value from the client that was originally created using [`createStreamableValue`](/docs/reference/ai-sdk-rsc/create-streamable-value) on the server. ## Import <Snippet text={`import { readStreamableValue } from "@ai-sdk/rsc"`} prompt={false} /> ## Example ```ts filename="app/actions.ts" async function generate() { 'use server'; const streamable = createStreamableValue(); streamable.update(1); streamable.update(2); streamable.done(3); return streamable.value; } ``` ```tsx filename="app/page.tsx" highlight="12" import { readStreamableValue } from '@ai-sdk/rsc'; export default function Page() { const [generation, setGeneration] = useState(''); return ( <div> <button onClick={async () => { const stream = await generate(); for await (const delta of readStreamableValue(stream)) { setGeneration(generation => generation + delta); } }} > Generate </button> </div> ); } ``` ## API Signature ### Parameters <PropertiesTable content={[ { name: 'stream', type: 'StreamableValue', description: 'The streamable value to read from.', }, ]} /> ### Returns It returns an async iterator that contains the values emitted by the streamable value. --- File: /ai/content/docs/07-reference/03-ai-sdk-rsc/06-get-ai-state.mdx --- --- title: getAIState description: Reference for the getAIState function from the AI SDK RSC --- # `getAIState` <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> Get the current AI state. ## Import <Snippet text={`import { getAIState } from "@ai-sdk/rsc"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'key', type: 'string', isOptional: true, description: "Returns the value of the specified key in the AI state, if it's an object.", }, ]} /> ### Returns The AI state. ## Examples <ExampleLinks examples={[ { title: 'Learn to render a React component during a tool call made by a language model in Next.js', link: '/examples/next-app/tools/render-interface-during-tool-call', }, ]} /> --- File: /ai/content/docs/07-reference/03-ai-sdk-rsc/07-get-mutable-ai-state.mdx --- --- title: getMutableAIState description: Reference for the getMutableAIState function from the AI SDK RSC --- # `getMutableAIState` <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> Get a mutable copy of the AI state. You can use this to update the state in the server. ## Import <Snippet text={`import { getMutableAIState } from "@ai-sdk/rsc"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'key', isOptional: true, type: 'string', description: "Returns the value of the specified key in the AI state, if it's an object.", }, ]} /> ### Returns The mutable AI state. ### Methods <PropertiesTable content={[ { name: 'update', type: '(newState: any) => void', description: 'Updates the AI state with the new state.', }, { name: 'done', type: '(newState: any) => void', description: 'Updates the AI state with the new state, marks it as finalized and closes the stream.', }, ]} /> ## Examples <ExampleLinks examples={[ { title: 'Learn to persist and restore states AI and UI states in Next.js', link: '/examples/next-app/state-management/save-and-restore-states', }, ]} /> --- File: /ai/content/docs/07-reference/03-ai-sdk-rsc/08-use-ai-state.mdx --- --- title: useAIState description: Reference for the useAIState function from the AI SDK RSC --- # `useAIState` <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> It is a hook that enables you to read and update the AI state. The AI state is shared globally between all `useAIState` hooks under the same `<AI/>` provider. The AI state is intended to contain context and information shared with the AI model, such as system messages, function responses, and other relevant data. ## Import <Snippet text={`import { useAIState } from "@ai-sdk/rsc"`} prompt={false} /> ## API Signature ### Returns A single element array where the first element is the current AI state. --- File: /ai/content/docs/07-reference/03-ai-sdk-rsc/09-use-actions.mdx --- --- title: useActions description: Reference for the useActions function from the AI SDK RSC --- # `useActions` <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> It is a hook to help you access your Server Actions from the client. This is particularly useful for building interfaces that require user interactions with the server. It is required to access these server actions via this hook because they are patched when passed through the context. Accessing them directly may result in a [Cannot find Client Component error](/docs/troubleshooting/common-issues/server-actions-in-client-components). ## Import <Snippet text={`import { useActions } from "@ai-sdk/rsc"`} prompt={false} /> ## API Signature ### Returns `Record<string, Action>`, a dictionary of server actions. ## Examples <ExampleLinks examples={[ { title: 'Learn to manage AI and UI states in Next.js', link: '/examples/next-app/state-management/ai-ui-states', }, { title: 'Learn to route React components using a language model in Next.js', link: '/examples/next-app/interface/route-components', }, ]} /> --- File: /ai/content/docs/07-reference/03-ai-sdk-rsc/10-use-ui-state.mdx --- --- title: useUIState description: Reference for the useUIState function from the AI SDK RSC --- # `useUIState` <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> It is a hook that enables you to read and update the UI State. The state is client-side and can contain functions, React nodes, and other data. UIState is the visual representation of the AI state. ## Import <Snippet text={`import { useUIState } from "@ai-sdk/rsc"`} prompt={false} /> ## API Signature ### Returns Similar to useState, it is an array, where the first element is the current UI state and the second element is the function that updates the state. ## Examples <ExampleLinks examples={[ { title: 'Learn to manage AI and UI states in Next.js', link: '/examples/next-app/state-management/ai-ui-states', }, ]} /> --- File: /ai/content/docs/07-reference/03-ai-sdk-rsc/11-use-streamable-value.mdx --- --- title: useStreamableValue description: Reference for the useStreamableValue function from the AI SDK RSC --- # `useStreamableValue` <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> It is a React hook that takes a streamable value created using [`createStreamableValue`](/docs/reference/ai-sdk-rsc/create-streamable-value) and returns the current value, error, and pending state. ## Import <Snippet text={`import { useStreamableValue } from "@ai-sdk/rsc"`} prompt={false} /> ## Example This is useful for consuming streamable values received from a component's props. ```tsx function MyComponent({ streamableValue }) { const [data, error, pending] = useStreamableValue(streamableValue); if (pending) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; return <div>Data: {data}</div>; } ``` ## API Signature ### Parameters It accepts a streamable value created using `createStreamableValue`. ### Returns It is an array, where the first element contains the data, the second element contains an error if it is thrown anytime during the stream, and the third is a boolean indicating if the value is pending. --- File: /ai/content/docs/07-reference/03-ai-sdk-rsc/20-render.mdx --- --- title: render (Removed) description: Reference for the render function from the AI SDK RSC --- # `render` (Removed) <Note type="warning">"render" has been removed in AI SDK 4.0.</Note> <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> A helper function to create a streamable UI from LLM providers. This function is similar to AI SDK Core APIs and supports the same model interfaces. > **Note**: `render` has been deprecated in favor of [`streamUI`](/docs/reference/ai-sdk-rsc/stream-ui). During migration, please ensure that the `messages` parameter follows the updated [specification](/docs/reference/ai-sdk-rsc/stream-ui#messages). ## Import <Snippet text={`import { render } from "@ai-sdk/rsc"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'model', type: 'string', description: 'Model identifier, must be OpenAI SDK compatible.', }, { name: 'provider', type: 'provider client', description: 'Currently the only provider available is OpenAI. This needs to match the model name.', }, { name: 'initial', isOptional: true, type: 'ReactNode', description: 'The initial UI to render.', }, { name: 'messages', type: 'Array<SystemMessage | UserMessage | AssistantMessage | ToolMessage>', description: 'A list of messages that represent a conversation.', properties: [ { type: 'SystemMessage', parameters: [ { name: 'role', type: "'system'", description: 'The role for the system message.', }, { name: 'content', type: 'string', description: 'The content of the message.', }, ], }, { type: 'UserMessage', parameters: [ { name: 'role', type: "'user'", description: 'The role for the user message.', }, { name: 'content', type: 'string', description: 'The content of the message.', }, ], }, { type: 'AssistantMessage', parameters: [ { name: 'role', type: "'assistant'", description: 'The role for the assistant message.', }, { name: 'content', type: 'string', description: 'The content of the message.', }, { name: 'tool_calls', type: 'ToolCall[]', description: 'A list of tool calls made by the model.', properties: [ { type: 'ToolCall', parameters: [ { name: 'id', type: 'string', description: 'The id of the tool call.', }, { name: 'type', type: "'function'", description: 'The type of the tool call.', }, { name: 'function', type: 'Function', description: 'The function to call.', properties: [ { type: 'Function', parameters: [ { name: 'name', type: 'string', description: 'The name of the function.', }, { name: 'arguments', type: 'string', description: 'The arguments of the function.', }, ], }, ], }, ], }, ], }, ], }, { type: 'ToolMessage', parameters: [ { name: 'role', type: "'tool'", description: 'The role for the tool message.', }, { name: 'content', type: 'string', description: 'The content of the message.', }, { name: 'toolCallId', type: 'string', description: 'The id of the tool call.', }, ], }, ], }, { name: 'functions', type: 'ToolSet', isOptional: true, description: 'Tools that are accessible to and can be called by the model.', properties: [ { type: 'Tool', parameters: [ { name: 'description', isOptional: true, type: 'string', description: 'Information about the purpose of the tool including details on how and when it can be used by the model.', }, { name: 'parameters', type: 'zod schema', description: 'The typed schema that describes the parameters of the tool that can also be used to validation and error handling.', }, { name: 'render', isOptional: true, type: 'async (parameters) => any', description: 'An async function that is called with the arguments from the tool call and produces a result.', }, ], }, ], }, { name: 'tools', type: 'ToolSet', isOptional: true, description: 'Tools that are accessible to and can be called by the model.', properties: [ { type: 'Tool', parameters: [ { name: 'description', isOptional: true, type: 'string', description: 'Information about the purpose of the tool including details on how and when it can be used by the model.', }, { name: 'parameters', type: 'zod schema', description: 'The typed schema that describes the parameters of the tool that can also be used to validation and error handling.', }, { name: 'render', isOptional: true, type: 'async (parameters) => any', description: 'An async function that is called with the arguments from the tool call and produces a result.', }, ], }, ], }, { name: 'text', isOptional: true, type: '(Text) => ReactNode', description: 'Callback to handle the generated tokens from the model.', properties: [ { type: 'Text', parameters: [ { name: 'content', type: 'string', description: 'The full content of the completion.', }, { name: 'delta', type: 'string', description: 'The delta.' }, { name: 'done', type: 'boolean', description: 'Is it done?' }, ], }, ], }, { name: 'temperature', isOptional: true, type: 'number', description: 'The temperature to use for the model.', }, ]} /> ### Returns It can return any valid ReactNode. --- File: /ai/content/docs/07-reference/03-ai-sdk-rsc/index.mdx --- --- title: AI SDK RSC description: Reference documentation for the AI SDK UI collapsed: true --- # AI SDK RSC <Note type="warning"> AI SDK RSC is currently experimental. We recommend using [AI SDK UI](/docs/ai-sdk-ui/overview) for production. For guidance on migrating from RSC to UI, see our [migration guide](/docs/ai-sdk-rsc/migrating-to-ui). </Note> <IndexCards cards={[ { title: 'streamUI', description: 'Use a helper function that streams React Server Components on tool execution.', href: '/docs/reference/ai-sdk-rsc/stream-ui', }, { title: 'createAI', description: 'Create a context provider that wraps your application and shares state between the client and language model on the server.', href: '/docs/reference/ai-sdk-rsc/create-ai', }, { title: 'createStreamableUI', description: 'Create a streamable UI component that can be rendered on the server and streamed to the client.', href: '/docs/reference/ai-sdk-rsc/create-streamable-ui', }, { title: 'createStreamableValue', description: 'Create a streamable value that can be rendered on the server and streamed to the client.', href: '/docs/reference/ai-sdk-rsc/create-streamable-value', }, { title: 'getAIState', description: 'Read the AI state on the server.', href: '/docs/reference/ai-sdk-rsc/get-ai-state', }, { title: 'getMutableAIState', description: 'Read and update the AI state on the server.', href: '/docs/reference/ai-sdk-rsc/get-mutable-ai-state', }, { title: 'useAIState', description: 'Get the AI state on the client from the context provider.', href: '/docs/reference/ai-sdk-rsc/use-ai-state', }, { title: 'useUIState', description: 'Get the UI state on the client from the context provider.', href: '/docs/reference/ai-sdk-rsc/use-ui-state', }, { title: 'useActions', description: 'Call server actions from the client.', href: '/docs/reference/ai-sdk-rsc/use-actions', }, ]} /> --- File: /ai/content/docs/07-reference/04-stream-helpers/01-ai-stream.mdx --- --- title: AIStream description: Learn to use AIStream helper function in your application. --- # `AIStream` <Note type="warning"> AIStream has been removed in AI SDK 4.0. Use `streamText.toDataStreamResponse()` instead. </Note> Creates a readable stream for AI responses. This is based on the responses returned by fetch and serves as the basis for the OpenAIStream and AnthropicStream. It allows you to handle AI response streams in a controlled and customized manner that will work with useChat and useCompletion. AIStream will throw an error if response doesn't have a 2xx status code. This is to ensure that the stream is only created for successful responses. ## Import ### React <Snippet text={`import { AIStream } from "ai"`} prompt={false} /> ## API Signature <PropertiesTable content={[ { name: 'response', type: 'Response', description: "This is the response object returned by fetch. It's used as the source of the readable stream.", }, { name: 'customParser', type: '(AIStreamParser) => void', description: 'This is a function that is used to parse the events in the stream. It should return a function that receives a stringified chunk from the LLM and extracts the message content. The function is expected to return nothing (void) or a string.', properties: [ { type: 'AIStreamParser', parameters: [ { name: '', type: '(data: string) => string | void', }, ], }, ], }, { name: 'callbacks', type: 'AIStreamCallbacksAndOptions', properties: [ { type: 'AIStreamCallbacksAndOptions', parameters: [ { name: 'onStart', type: '() => Promise<void>', description: 'An optional function that is called at the start of the stream processing.', }, { name: 'onCompletion', type: '(completion: string) => Promise<void>', description: "An optional function that is called for every completion. It's passed the completion as a string.", }, { name: 'onFinal', type: '(completion: string) => Promise<void>', description: 'An optional function that is called once when the stream is closed with the final completion message.', }, { name: 'onToken', type: '(token: string) => Promise<void>', description: "An optional function that is called for each token in the stream. It's passed the token as a string.", }, ], }, ], }, ]} /> --- File: /ai/content/docs/07-reference/04-stream-helpers/02-streaming-text-response.mdx --- --- title: StreamingTextResponse description: Learn to use StreamingTextResponse helper function in your application. --- # `StreamingTextResponse` <Note type="warning"> `StreamingTextResponse` has been removed in AI SDK 4.0. Use [`streamText.toDataStreamResponse()`](/docs/reference/ai-sdk-core/stream-text) instead. </Note> It is a utility class that simplifies the process of returning a ReadableStream of text in HTTP responses. It is a lightweight wrapper around the native Response class, automatically setting the status code to 200 and the Content-Type header to 'text/plain; charset=utf-8'. ## Import <Snippet text={`import { StreamingTextResponse } from "ai"`} prompt={false} /> ## API Signature ## Parameters <PropertiesTable content={[ { name: 'stream', type: 'ReadableStream', description: 'The stream of content which represents the HTTP response.', }, { name: 'init', isOptional: true, type: 'ResponseInit', description: 'It can be used to customize the properties of the HTTP response. It is an object that corresponds to the ResponseInit object used in the Response constructor.', properties: [ { type: 'ResponseInit', parameters: [ { name: 'status', type: 'number', isOptional: true, description: 'The status code for the response. StreamingTextResponse will overwrite this value with 200.', }, { name: 'statusText', type: 'string', isOptional: true, description: 'The status message associated with the status code.', }, { name: 'headers', type: 'HeadersInit', isOptional: true, description: "Any headers you want to add to your response. StreamingTextResponse will add 'Content-Type': 'text/plain; charset=utf-8' to these headers.", }, ], }, ], }, { name: 'data', isOptional: true, type: 'StreamData', description: 'StreamData object that you are using to generate additional data for the response.', }, ]} /> ### Returns An instance of Response with the provided ReadableStream as the body, the status set to 200, and the Content-Type header set to 'text/plain; charset=utf-8'. Additional headers and properties can be added using the init parameter --- File: /ai/content/docs/07-reference/04-stream-helpers/05-stream-to-response.mdx --- --- title: streamToResponse description: Learn to use streamToResponse helper function in your application. --- # `streamToResponse` <Note type="warning"> `streamToResponse` has been removed in AI SDK 4.0. Use `pipeDataStreamToResponse` from [streamText](/docs/reference/ai-sdk-core/stream-text) instead. </Note> `streamToResponse` pipes a data stream to a Node.js `ServerResponse` object and sets the status code and headers. This is useful to create data stream responses in environments that use `ServerResponse` objects, such as Node.js HTTP servers. The status code and headers can be configured using the `options` parameter. By default, the status code is set to 200 and the Content-Type header is set to `text/plain; charset=utf-8`. ## Import <Snippet text={`import { streamToResponse } from "ai"`} prompt={false} /> ## Example You can e.g. use `streamToResponse` to pipe a data stream to a Node.js HTTP server response: ```ts import { openai } from '@ai-sdk/openai'; import { StreamData, streamText, streamToResponse } from 'ai'; import { createServer } from 'http'; createServer(async (req, res) => { const result = streamText({ model: openai('gpt-4.1'), prompt: 'What is the weather in San Francisco?', }); // use stream data const data = new StreamData(); data.append('initialized call'); streamToResponse( result.toAIStream({ onFinal() { data.append('call completed'); data.close(); }, }), res, {}, data, ); }).listen(8080); ``` ## API Signature ### Parameters <PropertiesTable content={[ { name: 'stream', type: 'ReadableStream', description: 'The Web Stream to pipe to the response. It can be the return value of OpenAIStream, HuggingFaceStream, AnthropicStream, or an AIStream instance.', }, { name: 'response', type: 'ServerResponse', description: 'The Node.js ServerResponse object to pipe the stream to. This is usually the second argument of a Node.js HTTP request handler.', }, { name: 'options', type: 'Options', description: 'Configure the response', properties: [ { type: 'Options', parameters: [ { name: 'status', type: 'number', description: 'The status code to set on the response. Defaults to `200`.', }, { name: 'headers', type: 'Record<string, string>', description: "Additional headers to set on the response. Defaults to `{ 'Content-Type': 'text/plain; charset=utf-8' }`.", }, ], }, ], }, { name: 'data', type: 'StreamData', description: 'StreamData object for forwarding additional data to the client.', }, ]} /> --- File: /ai/content/docs/07-reference/04-stream-helpers/07-openai-stream.mdx --- --- title: OpenAIStream description: Learn to use OpenAIStream helper function in your application. --- # `OpenAIStream` <Note type="warning">OpenAIStream has been removed in AI SDK 4.0</Note> <Note type="warning"> OpenAIStream is part of the legacy OpenAI integration. It is not compatible with the AI SDK 3.1 functions. It is recommended to use the [AI SDK OpenAI Provider](/providers/ai-sdk-providers/openai) instead. </Note> Transforms the response from OpenAI's language models into a ReadableStream. Note: Prior to v4, the official OpenAI API SDK does not support the Edge Runtime and only works in serverless environments. The openai-edge package is based on fetch instead of axios (and thus works in the Edge Runtime) so we recommend using openai v4+ or openai-edge. ## Import ### React <Snippet text={`import { OpenAIStream } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'response', type: 'Response', description: 'The response object returned by a call made by the Provider SDK.', }, { name: 'callbacks', type: 'AIStreamCallbacksAndOptions', isOptional: true, description: 'An object containing callback functions to handle the start, each token, and completion of the AI response. In the absence of this parameter, default behavior is implemented.', properties: [ { type: 'AIStreamCallbacksAndOptions', parameters: [ { name: 'onStart', type: '() => Promise<void>', description: 'An optional function that is called at the start of the stream processing.', }, { name: 'onCompletion', type: '(completion: string) => Promise<void>', description: "An optional function that is called for every completion. It's passed the completion as a string.", }, { name: 'onFinal', type: '(completion: string) => Promise<void>', description: 'An optional function that is called once when the stream is closed with the final completion message.', }, { name: 'onToken', type: '(token: string) => Promise<void>', description: "An optional function that is called for each token in the stream. It's passed the token as a string.", }, ], }, ], }, ]} /> --- File: /ai/content/docs/07-reference/04-stream-helpers/08-anthropic-stream.mdx --- --- title: AnthropicStream description: Learn to use AnthropicStream helper function in your application. --- # `AnthropicStream` <Note type="warning">AnthropicStream has been removed in AI SDK 4.0.</Note> <Note type="warning"> AnthropicStream is part of the legacy Anthropic integration. It is not compatible with the AI SDK 3.1 functions. It is recommended to use the [AI SDK Anthropic Provider](/providers/ai-sdk-providers/anthropic) instead. </Note> It is a utility function that transforms the output from Anthropic's SDK into a ReadableStream. It uses AIStream under the hood, applying a specific parser for the Anthropic's response data structure. ## Import ### React <Snippet text={`import { AnthropicStream } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'response', type: 'Response', description: 'The response object returned by a call made by the Provider SDK.', }, { name: 'callbacks', type: 'AIStreamCallbacksAndOptions', isOptional: true, description: 'An object containing callback functions to handle the start, each token, and completion of the AI response. In the absence of this parameter, default behavior is implemented.', properties: [ { type: 'AIStreamCallbacksAndOptions', parameters: [ { name: 'onStart', type: '() => Promise<void>', description: 'An optional function that is called at the start of the stream processing.', }, { name: 'onCompletion', type: '(completion: string) => Promise<void>', description: "An optional function that is called for every completion. It's passed the completion as a string.", }, { name: 'onFinal', type: '(completion: string) => Promise<void>', description: 'An optional function that is called once when the stream is closed with the final completion message.', }, { name: 'onToken', type: '(token: string) => Promise<void>', description: "An optional function that is called for each token in the stream. It's passed the token as a string.", }, ], }, ], }, ]} /> ### Returns A `ReadableStream`. --- File: /ai/content/docs/07-reference/04-stream-helpers/09-aws-bedrock-stream.mdx --- --- title: AWSBedrockStream description: Learn to use AWSBedrockStream helper function in your application. --- # `AWSBedrockStream` <Note type="warning">AWSBedrockStream has been removed in AI SDK 4.0.</Note> <Note type="warning"> AWSBedrockStream is part of the legacy AWS Bedrock integration. It is not compatible with the AI SDK 3.1 functions. </Note> The AWS Bedrock stream functions are utilties that transform the outputs from the AWS Bedrock API into a ReadableStream. It uses AIStream under the hood and handle parsing Bedrock's response. ## Import ### React <Snippet text={`import { AWSBedrockStream } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'response', type: 'AWSBedrockResponse', description: 'The response object returned from AWS Bedrock.', properties: [ { type: 'AWSBedrockResponse', parameters: [ { name: 'body', isOptional: true, type: 'AsyncIterable<{ chunk?: { bytes?: Uint8Array } }>', description: 'An optional async iterable of objects containing optional binary data chunks.', }, ], }, ], }, { name: 'callbacks', type: 'AIStreamCallbacksAndOptions', isOptional: true, description: 'An object containing callback functions to handle the start, each token, and completion of the AI response. In the absence of this parameter, default behavior is implemented.', properties: [ { type: 'AIStreamCallbacksAndOptions', parameters: [ { name: 'onStart', type: '() => Promise<void>', description: 'An optional function that is called at the start of the stream processing.', }, { name: 'onCompletion', type: '(completion: string) => Promise<void>', description: "An optional function that is called for every completion. It's passed the completion as a string.", }, { name: 'onFinal', type: '(completion: string) => Promise<void>', description: 'An optional function that is called once when the stream is closed with the final completion message.', }, { name: 'onToken', type: '(token: string) => Promise<void>', description: "An optional function that is called for each token in the stream. It's passed the token as a string.", }, ], }, ], }, ]} /> ### Returns A `ReadableStream`. --- File: /ai/content/docs/07-reference/04-stream-helpers/10-aws-bedrock-anthropic-stream.mdx --- --- title: AWSBedrockAnthropicStream description: Learn to use AWSBedrockAnthropicStream helper function in your application. --- # `AWSBedrockAnthropicStream` <Note type="warning"> AWSBedrockAnthropicStream has been removed in AI SDK 4.0. </Note> <Note type="warning"> AWSBedrockAnthropicStream is part of the legacy AWS Bedrock integration. It is not compatible with the AI SDK 3.1 functions. </Note> The AWS Bedrock stream functions are utilties that transform the outputs from the AWS Bedrock API into a ReadableStream. It uses AIStream under the hood and handle parsing Bedrock's response. ## Import ### React <Snippet text={`import { AWSBedrockAnthropicStream } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'response', type: 'AWSBedrockResponse', description: 'The response object returned from AWS Bedrock.', properties: [ { type: 'AWSBedrockResponse', parameters: [ { name: 'body', isOptional: true, type: 'AsyncIterable<{ chunk?: { bytes?: Uint8Array } }>', description: 'An optional async iterable of objects containing optional binary data chunks.', }, ], }, ], }, { name: 'callbacks', type: 'AIStreamCallbacksAndOptions', isOptional: true, description: 'An object containing callback functions to handle the start, each token, and completion of the AI response. In the absence of this parameter, default behavior is implemented.', properties: [ { type: 'AIStreamCallbacksAndOptions', parameters: [ { name: 'onStart', type: '() => Promise<void>', description: 'An optional function that is called at the start of the stream processing.', }, { name: 'onCompletion', type: '(completion: string) => Promise<void>', description: "An optional function that is called for every completion. It's passed the completion as a string.", }, { name: 'onFinal', type: '(completion: string) => Promise<void>', description: 'An optional function that is called once when the stream is closed with the final completion message.', }, { name: 'onToken', type: '(token: string) => Promise<void>', description: "An optional function that is called for each token in the stream. It's passed the token as a string.", }, ], }, ], }, ]} /> ### Returns A `ReadableStream`. --- File: /ai/content/docs/07-reference/04-stream-helpers/10-aws-bedrock-messages-stream.mdx --- --- title: AWSBedrockAnthropicMessagesStream description: Learn to use AWSBedrockAnthropicMessagesStream helper function in your application. --- # `AWSBedrockAnthropicMessagesStream` <Note type="warning"> AWSBedrockAnthropicMessagesStream has been removed in AI SDK 4.0. </Note> <Note type="warning"> AWSBedrockAnthropicMessagesStream is part of the legacy AWS Bedrock integration. It is not compatible with the AI SDK 3.1 functions. </Note> The AWS Bedrock stream functions are utilties that transform the outputs from the AWS Bedrock API into a ReadableStream. It uses AIStream under the hood and handle parsing Bedrock's response. ## Import ### React <Snippet text={`import { AWSBedrockAnthropicMessagesStream } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'response', type: 'AWSBedrockResponse', description: 'The response object returned from AWS Bedrock.', properties: [ { type: 'AWSBedrockResponse', parameters: [ { name: 'body', isOptional: true, type: 'AsyncIterable<{ chunk?: { bytes?: Uint8Array } }>', description: 'An optional async iterable of objects containing optional binary data chunks.', }, ], }, ], }, { name: 'callbacks', type: 'AIStreamCallbacksAndOptions', isOptional: true, description: 'An object containing callback functions to handle the start, each token, and completion of the AI response. In the absence of this parameter, default behavior is implemented.', properties: [ { type: 'AIStreamCallbacksAndOptions', parameters: [ { name: 'onStart', type: '() => Promise<void>', description: 'An optional function that is called at the start of the stream processing.', }, { name: 'onCompletion', type: '(completion: string) => Promise<void>', description: "An optional function that is called for every completion. It's passed the completion as a string.", }, { name: 'onFinal', type: '(completion: string) => Promise<void>', description: 'An optional function that is called once when the stream is closed with the final completion message.', }, { name: 'onToken', type: '(token: string) => Promise<void>', description: "An optional function that is called for each token in the stream. It's passed the token as a string.", }, ], }, ], }, ]} /> ### Returns A `ReadableStream`. --- File: /ai/content/docs/07-reference/04-stream-helpers/11-aws-bedrock-cohere-stream.mdx --- --- title: AWSBedrockCohereStream description: Learn to use AWSBedrockCohereStream helper function in your application. --- # `AWSBedrockCohereStream` <Note type="warning"> AWSBedrockCohereStream has been removed in AI SDK 4.0. </Note> <Note type="warning"> AWSBedrockCohereStream is part of the legacy AWS Bedrock integration. It is not compatible with the AI SDK 3.1 functions. </Note> ## Import The AWS Bedrock stream functions are utilties that transform the outputs from the AWS Bedrock API into a ReadableStream. It uses AIStream under the hood and handles parsing Bedrock's response. ### React <Snippet text={`import { AWSBedrockCohereStream } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'response', type: 'AWSBedrockResponse', description: 'The response object returned from AWS Bedrock.', properties: [ { type: 'AWSBedrockResponse', parameters: [ { name: 'body', isOptional: true, type: 'AsyncIterable<{ chunk?: { bytes?: Uint8Array } }>', description: 'An optional async iterable of objects containing optional binary data chunks.', }, ], }, ], }, { name: 'callbacks', type: 'AIStreamCallbacksAndOptions', isOptional: true, description: 'An object containing callback functions to handle the start, each token, and completion of the AI response. In the absence of this parameter, default behavior is implemented.', properties: [ { type: 'AIStreamCallbacksAndOptions', parameters: [ { name: 'onStart', type: '() => Promise<void>', description: 'An optional function that is called at the start of the stream processing.', }, { name: 'onCompletion', type: '(completion: string) => Promise<void>', description: "An optional function that is called for every completion. It's passed the completion as a string.", }, { name: 'onFinal', type: '(completion: string) => Promise<void>', description: 'An optional function that is called once when the stream is closed with the final completion message.', }, { name: 'onToken', type: '(token: string) => Promise<void>', description: "An optional function that is called for each token in the stream. It's passed the token as a string.", }, ], }, ], }, ]} /> ### Returns A `ReadableStream`. --- File: /ai/content/docs/07-reference/04-stream-helpers/12-aws-bedrock-llama-2-stream.mdx --- --- title: AWSBedrockLlama2Stream description: Learn to use AWSBedrockLlama2Stream helper function in your application. --- # `AWSBedrockLlama2Stream` <Note type="warning"> AWSBedrockLlama2Stream has been removed in AI SDK 4.0. </Note> <Note type="warning"> AWSBedrockLlama2Stream is part of the legacy AWS Bedrock integration. It is not compatible with the AI SDK 3.1 functions. </Note> The AWS Bedrock stream functions are utilties that transform the outputs from the AWS Bedrock API into a ReadableStream. It uses AIStream under the hood and handle parsing Bedrock's response. ## Import ### React <Snippet text={`import { AWSBedrockLlama2Stream } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'response', type: 'AWSBedrockResponse', description: 'The response object returned from AWS Bedrock.', properties: [ { type: 'AWSBedrockResponse', parameters: [ { name: 'body', isOptional: true, type: 'AsyncIterable<{ chunk?: { bytes?: Uint8Array } }>', description: 'An optional async iterable of objects containing optional binary data chunks.', }, ], }, ], }, { name: 'callbacks', type: 'AIStreamCallbacksAndOptions', isOptional: true, description: 'An object containing callback functions to handle the start, each token, and completion of the AI response. In the absence of this parameter, default behavior is implemented.', properties: [ { type: 'AIStreamCallbacksAndOptions', parameters: [ { name: 'onStart', type: '() => Promise<void>', description: 'An optional function that is called at the start of the stream processing.', }, { name: 'onCompletion', type: '(completion: string) => Promise<void>', description: "An optional function that is called for every completion. It's passed the completion as a string.", }, { name: 'onFinal', type: '(completion: string) => Promise<void>', description: 'An optional function that is called once when the stream is closed with the final completion message.', }, { name: 'onToken', type: '(token: string) => Promise<void>', description: "An optional function that is called for each token in the stream. It's passed the token as a string.", }, ], }, ], }, ]} /> ### Returns A `ReadableStream`. --- File: /ai/content/docs/07-reference/04-stream-helpers/13-cohere-stream.mdx --- --- title: CohereStream description: Learn to use CohereStream helper function in your application. --- # `CohereStream` <Note type="warning">CohereStream has been removed in AI SDK 4.0.</Note> <Note type="warning"> CohereStream is part of the legacy Cohere integration. It is not compatible with the AI SDK 3.1 functions. </Note> The CohereStream function is a utility that transforms the output from Cohere's API into a ReadableStream. It uses AIStream under the hood, applying a specific parser for the Cohere's response data structure. This works with the official Cohere API, and it's supported in both Node.js, the Edge Runtime, and browser environments. ## Import ### React <Snippet text={`import { CohereStream } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'response', type: 'Response', description: 'The response object returned by a call made by the Provider SDK.', }, { name: 'callbacks', type: 'AIStreamCallbacksAndOptions', isOptional: true, description: 'An object containing callback functions to handle the start, each token, and completion of the AI response. In the absence of this parameter, default behavior is implemented.', properties: [ { type: 'AIStreamCallbacksAndOptions', parameters: [ { name: 'onStart', type: '() => Promise<void>', description: 'An optional function that is called at the start of the stream processing.', }, { name: 'onCompletion', type: '(completion: string) => Promise<void>', description: "An optional function that is called for every completion. It's passed the completion as a string.", }, { name: 'onFinal', type: '(completion: string) => Promise<void>', description: 'An optional function that is called once when the stream is closed with the final completion message.', }, { name: 'onToken', type: '(token: string) => Promise<void>', description: "An optional function that is called for each token in the stream. It's passed the token as a string.", }, ], }, ], }, ]} /> ### Returns A `ReadableStream`. --- File: /ai/content/docs/07-reference/04-stream-helpers/14-google-generative-ai-stream.mdx --- --- title: GoogleGenerativeAIStream description: Learn to use GoogleGenerativeAIStream helper function in your application. --- # `GoogleGenerativeAIStream` <Note type="warning"> GoogleGenerativeAIStream has been removed in AI SDK 4.0. </Note> <Note type="warning"> GoogleGenerativeAIStream is part of the legacy Google Generative AI integration. It is not compatible with the AI SDK 3.1 functions. It is recommended to use the [AI SDK Google Generative AI Provider](/providers/ai-sdk-providers/google-generative-ai) instead. </Note> The GoogleGenerativeAIStream function is a utility that transforms the output from Google's Generative AI SDK into a ReadableStream. It uses AIStream under the hood, applying a specific parser for the Google's response data structure. This works with the official Generative AI SDK, and it's supported in both Node.js, Edge Runtime, and browser environments. ## Import ### React <Snippet text={`import { GoogleGenerativeAIStream } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'response', type: '{ stream: AsyncIterable<GenerateContentResponse> }', description: 'The response object returned by the Google Generative AI API.', }, { name: 'callbacks', type: 'AIStreamCallbacksAndOptions', isOptional: true, description: 'An object containing callback functions to handle the start, each token, and completion of the AI response. In the absence of this parameter, default behavior is implemented.', properties: [ { type: 'AIStreamCallbacksAndOptions', parameters: [ { name: 'onStart', type: '() => Promise<void>', description: 'An optional function that is called at the start of the stream processing.', }, { name: 'onCompletion', type: '(completion: string) => Promise<void>', description: "An optional function that is called for every completion. It's passed the completion as a string.", }, { name: 'onFinal', type: '(completion: string) => Promise<void>', description: 'An optional function that is called once when the stream is closed with the final completion message.', }, { name: 'onToken', type: '(token: string) => Promise<void>', description: "An optional function that is called for each token in the stream. It's passed the token as a string.", }, ], }, ], }, ]} /> ### Returns A `ReadableStream`. --- File: /ai/content/docs/07-reference/04-stream-helpers/15-hugging-face-stream.mdx --- --- title: HuggingFaceStream description: Learn to use HuggingFaceStream helper function in your application. --- # `HuggingFaceStream` <Note type="warning">HuggingFaceStream has been removed in AI SDK 4.0.</Note> <Note type="warning"> HuggingFaceStream is part of the legacy Hugging Face integration. It is not compatible with the AI SDK 3.1 functions. </Note> Converts the output from language models hosted on Hugging Face into a ReadableStream. While HuggingFaceStream is compatible with most Hugging Face language models, the rapidly evolving landscape of models may result in certain new or niche models not being supported. If you encounter a model that isn't supported, we encourage you to open an issue. To ensure that AI responses are comprised purely of text without any delimiters that could pose issues when rendering in chat or completion modes, we standardize and remove special end-of-response tokens. If your use case requires a different handling of responses, you can fork and modify this stream to meet your specific needs. Currently, `</s>` and `<|endoftext|>` are recognized as end-of-stream tokens. ## Import ### React <Snippet text={`import { HuggingFaceStream } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'iter', type: 'AsyncGenerator<any>', description: 'This parameter should be the generator function returned by the hf.textGenerationStream method in the Hugging Face Inference SDK.', }, { name: 'callbacks', type: 'AIStreamCallbacksAndOptions', isOptional: true, description: 'An object containing callback functions to handle the start, each token, and completion of the AI response. In the absence of this parameter, default behavior is implemented.', properties: [ { type: 'AIStreamCallbacksAndOptions', parameters: [ { name: 'onStart', type: '() => Promise<void>', description: 'An optional function that is called at the start of the stream processing.', }, { name: 'onCompletion', type: '(completion: string) => Promise<void>', description: "An optional function that is called for every completion. It's passed the completion as a string.", }, { name: 'onFinal', type: '(completion: string) => Promise<void>', description: 'An optional function that is called once when the stream is closed with the final completion message.', }, { name: 'onToken', type: '(token: string) => Promise<void>', description: "An optional function that is called for each token in the stream. It's passed the token as a string.", }, ], }, ], }, ]} /> ### Returns A `ReadableStream`. --- File: /ai/content/docs/07-reference/04-stream-helpers/16-langchain-adapter.mdx --- --- title: '@ai-sdk/langchain Adapter' description: API Reference for the LangChain Adapter. --- # `@ai-sdk/langchain` The `@ai-sdk/langchain` module provides helper functions to transform LangChain output streams into data streams and data stream responses. See the [LangChain Adapter documentation](/providers/adapters/langchain) for more information. It supports: - LangChain StringOutputParser streams - LangChain AIMessageChunk streams - LangChain StreamEvents v2 streams ## Import <Snippet text={`import { toDataStreamResponse } from "@ai-sdk/langchain"`} prompt={false} /> ## API Signature ### Methods <PropertiesTable content={[ { name: 'toDataStream', type: '(stream: ReadableStream<LangChainAIMessageChunk> | ReadableStream<string>, AIStreamCallbacksAndOptions) => AIStream', description: 'Converts LangChain output streams to data stream.', }, { name: 'toDataStreamResponse', type: '(stream: ReadableStream<LangChainAIMessageChunk> | ReadableStream<string>, options?: {init?: ResponseInit, data?: StreamData, callbacks?: AIStreamCallbacksAndOptions}) => Response', description: 'Converts LangChain output streams to data stream response.', }, { name: 'mergeIntoDataStream', type: '(stream: ReadableStream<LangChainStreamEvent> | ReadableStream<LangChainAIMessageChunk> | ReadableStream<string>, options: { dataStream: DataStreamWriter; callbacks?: StreamCallbacks }) => void', description: 'Merges LangChain output streams into an existing data stream.', }, ]} /> ## Examples ### Convert LangChain Expression Language Stream ```tsx filename="app/api/completion/route.ts" highlight={"14"} import { ChatOpenAI } from '@langchain/openai'; import { toDataStreamResponse } from '@ai-sdk/langchain'; export async function POST(req: Request) { const { prompt } = await req.json(); const model = new ChatOpenAI({ model: 'gpt-3.5-turbo-0125', temperature: 0, }); const stream = await model.stream(prompt); return toDataStreamResponse(stream); } ``` ### Convert StringOutputParser Stream ```tsx filename="app/api/completion/route.ts" highlight={"16"} import { ChatOpenAI } from '@langchain/openai'; import { toDataStreamResponse } from '@ai-sdk/langchain'; import { StringOutputParser } from '@langchain/core/output_parsers'; export async function POST(req: Request) { const { prompt } = await req.json(); const model = new ChatOpenAI({ model: 'gpt-3.5-turbo-0125', temperature: 0, }); const parser = new StringOutputParser(); const stream = await model.pipe(parser).stream(prompt); return toDataStreamResponse(stream); } ``` --- File: /ai/content/docs/07-reference/04-stream-helpers/16-llamaindex-adapter.mdx --- --- title: '@ai-sdk/llamaindex Adapter' description: API Reference for the LlamaIndex Adapter. --- # `@ai-sdk/llamaindex` The `@ai-sdk/llamaindex` package provides helper functions to transform LlamaIndex output streams into data streams and data stream responses. See the [LlamaIndex Adapter documentation](/providers/adapters/llamaindex) for more information. It supports: - LlamaIndex ChatEngine streams - LlamaIndex QueryEngine streams ## Import <Snippet text={`import { toDataResponse } from "@ai-sdk/llamaindex"`} prompt={false} /> ## API Signature ### Methods <PropertiesTable content={[ { name: 'toDataStream', type: '(stream: AsyncIterable<EngineResponse>, AIStreamCallbacksAndOptions) => AIStream', description: 'Converts LlamaIndex output streams to data stream.', }, { name: 'toDataStreamResponse', type: '(stream: AsyncIterable<EngineResponse>, options?: {init?: ResponseInit, data?: StreamData, callbacks?: AIStreamCallbacksAndOptions}) => Response', description: 'Converts LlamaIndex output streams to data stream response.', }, { name: 'mergeIntoDataStream', type: '(stream: AsyncIterable<EngineResponse>, options: { dataStream: DataStreamWriter; callbacks?: StreamCallbacks }) => void', description: 'Merges LlamaIndex output streams into an existing data stream.', }, ]} /> ## Examples ### Convert LlamaIndex ChatEngine Stream ```tsx filename="app/api/completion/route.ts" highlight="15" import { OpenAI, SimpleChatEngine } from 'llamaindex'; import { toDataStreamResponse } from '@ai-sdk/llamaindex'; export async function POST(req: Request) { const { prompt } = await req.json(); const llm = new OpenAI({ model: 'gpt-4o' }); const chatEngine = new SimpleChatEngine({ llm }); const stream = await chatEngine.chat({ message: prompt, stream: true, }); return toDataStreamResponse(stream); } ``` --- File: /ai/content/docs/07-reference/04-stream-helpers/17-mistral-stream.mdx --- --- title: MistralStream description: Learn to use MistralStream helper function in your application. --- # `MistralStream` <Note type="warning">MistralStream has been removed in AI SDK 4.0.</Note> <Note type="warning"> MistralStream is part of the legacy Mistral integration. It is not compatible with the AI SDK 3.1 functions. It is recommended to use the [AI SDK Mistral Provider](/providers/ai-sdk-providers/mistral) instead. </Note> Transforms the output from Mistral's language models into a ReadableStream. This works with the official Mistral API, and it's supported in both Node.js, the Edge Runtime, and browser environments. ## Import ### React <Snippet text={`import { MistralStream } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'response', type: 'Response', description: 'The response object returned by a call made by the Provider SDK.', }, { name: 'callbacks', type: 'AIStreamCallbacksAndOptions', isOptional: true, description: 'An object containing callback functions to handle the start, each token, and completion of the AI response. In the absence of this parameter, default behavior is implemented.', properties: [ { type: 'AIStreamCallbacksAndOptions', parameters: [ { name: 'onStart', type: '() => Promise<void>', description: 'An optional function that is called at the start of the stream processing.', }, { name: 'onCompletion', type: '(completion: string) => Promise<void>', description: "An optional function that is called for every completion. It's passed the completion as a string.", }, { name: 'onFinal', type: '(completion: string) => Promise<void>', description: 'An optional function that is called once when the stream is closed with the final completion message.', }, { name: 'onToken', type: '(token: string) => Promise<void>', description: "An optional function that is called for each token in the stream. It's passed the token as a string.", }, ], }, ], }, ]} /> ### Returns A `ReadableStream`. --- File: /ai/content/docs/07-reference/04-stream-helpers/18-replicate-stream.mdx --- --- title: ReplicateStream description: Learn to use ReplicateStream helper function in your application. --- # `ReplicateStream` <Note type="warning">ReplicateStream has been removed in AI SDK 4.0.</Note> <Note type="warning"> ReplicateStream is part of the legacy Replicate integration. It is not compatible with the AI SDK 3.1 functions. </Note> The ReplicateStream function is a utility that handles extracting the stream from the output of [Replicate](https://replicate.com)'s API. It expects a Prediction object as returned by the [Replicate JavaScript SDK](https://github.com/replicate/replicate-javascript), and returns a ReadableStream. Unlike other wrappers, ReplicateStream returns a Promise because it makes a fetch call to the [Replicate streaming API](https://github.com/replicate/replicate-javascript#streaming) under the hood. ## Import ### React <Snippet text={`import { ReplicateStream } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'pre', type: 'Prediction', description: 'Object returned by the Replicate JavaScript SDK.', }, { name: 'callbacks', type: 'AIStreamCallbacksAndOptions', isOptional: true, description: 'An object containing callback functions to handle the start, each token, and completion of the AI response. In the absence of this parameter, default behavior is implemented.', properties: [ { type: 'AIStreamCallbacksAndOptions', parameters: [ { name: 'onStart', type: '() => Promise<void>', description: 'An optional function that is called at the start of the stream processing.', }, { name: 'onCompletion', type: '(completion: string) => Promise<void>', description: "An optional function that is called for every completion. It's passed the completion as a string.", }, { name: 'onFinal', type: '(completion: string) => Promise<void>', description: 'An optional function that is called once when the stream is closed with the final completion message.', }, { name: 'onToken', type: '(token: string) => Promise<void>', description: "An optional function that is called for each token in the stream. It's passed the token as a string.", }, ], }, ], }, { name: 'options', type: '{ headers?: Record<string, string> }', isOptiona: true, description: 'An optional parameter for passing additional headers.', }, ]} /> ### Returns A `ReadableStream` wrapped in a promise. --- File: /ai/content/docs/07-reference/04-stream-helpers/19-inkeep-stream.mdx --- --- title: InkeepStream description: Learn to use InkeepStream helper function in your application. --- # `InkeepStream` <Note type="warning">InkeepStream has been removed in AI SDK 4.0.</Note> <Note type="warning"> InkeepStream is part of the legacy Inkeep integration. It is not compatible with the AI SDK 3.1 functions. </Note> The InkeepStream function is a utility that transforms the output from [Inkeep](https://inkeep.com)'s API into a ReadableStream. It uses AIStream under the hood, applying a specific parser for the Inkeep's response data structure. This works with the official Inkeep API, and it's supported in both Node.js, the Edge Runtime, and browser environments. ## Import ### React <Snippet text={`import { InkeepStream } from "ai"`} prompt={false} /> ## API Signature ### Parameters <PropertiesTable content={[ { name: 'response', type: 'Response', description: 'The response object returned by a call made by the Provider SDK.', }, { name: 'callbacks', type: 'AIStreamCallbacksAndOptions', isOptional: true, description: 'An object containing callback functions to handle the start, each token, and completion of the AI response. In the absence of this parameter, default behavior is implemented.', properties: [ { type: 'AIStreamCallbacksAndOptions', parameters: [ { name: 'onStart', type: '() => Promise<void>', description: 'An optional function that is called at the start of the stream processing.', }, { name: 'onCompletion', type: '(completion: string) => Promise<void>', description: "An optional function that is called for every completion. It's passed the completion as a string.", }, { name: 'onFinal', type: '(completion: string) => Promise<void>', description: 'An optional function that is called once when the stream is closed with the final completion message.', }, { name: 'onToken', type: '(token: string) => Promise<void>', description: "An optional function that is called for each token in the stream. It's passed the token as a string.", }, ], }, ], }, ]} /> ### Returns A `ReadableStream`. --- File: /ai/content/docs/07-reference/04-stream-helpers/index.mdx --- --- title: Stream Helpers description: Learn to use help functions that help stream generations from different providers. collapsed: true --- <IndexCards cards={[ { title: 'AIStream', description: 'Create a readable stream for AI responses.', href: '/docs/reference/stream-helpers/ai-stream', }, { title: 'StreamingTextResponse', description: 'Create a streaming response for text generations.', href: '/docs/reference/stream-helpers/streaming-text-response', }, { title: 'streamtoResponse', description: 'Pipe a ReadableStream to a Node.js ServerResponse object.', href: '/docs/reference/stream-helpers/stream-to-response', }, { title: 'OpenAIStream', description: "Transforms the response from OpenAI's language models into a readable stream.", href: '/docs/reference/stream-helpers/openai-stream', }, { title: 'AnthropicStream', description: "Transforms the response from Anthropic's language models into a readable stream.", href: '/docs/reference/stream-helpers/anthropic-stream', }, { title: 'AWSBedrockStream', description: "Transforms the response from AWS Bedrock's language models into a readable stream.", href: '/docs/reference/stream-helpers/aws-bedrock-stream', }, { title: 'AWSBedrockMessagesStream', description: "Transforms the response from AWS Bedrock Message's language models into a readable stream.", href: '/docs/reference/stream-helpers/aws-bedrock-messages-stream', }, { title: 'AWSBedrockCohereStream', description: "Transforms the response from AWS Bedrock Cohere's language models into a readable stream.", href: '/docs/reference/stream-helpers/aws-bedrock-cohere-stream', }, { title: 'AWSBedrockLlama-2Stream', description: "Transforms the response from AWS Bedrock Llama-2's language models into a readable stream.", href: '/docs/reference/stream-helpers/aws-bedrock-llama-2-stream', }, { title: 'CohereStream', description: "Transforms the response from Cohere's language models into a readable stream.", href: '/docs/reference/stream-helpers/cohere-stream', }, { title: 'GoogleGenerativeAIStream', description: "Transforms the response from Google's language models into a readable stream.", href: '/docs/reference/stream-helpers/google-generative-ai-stream', }, { title: 'HuggingFaceStream', description: "Transforms the response from Hugging Face's language models into a readable stream.", href: '/docs/reference/stream-helpers/hugging-face-stream', }, { title: 'LangChainStream', description: "Transforms the response from LangChain's language models into a readable stream.", href: '/docs/reference/stream-helpers/langchain-stream', }, { title: 'MistralStream', description: "Transforms the response from Mistral's language models into a readable stream.", href: '/docs/reference/stream-helpers/mistral-stream', }, { title: 'ReplicateStream', description: "Transforms the response from Replicate's language models into a readable stream.", href: '/docs/reference/stream-helpers/replicate-stream', }, { title: 'InkeepsStream', description: "Transforms the response from Inkeeps's language models into a readable stream.", href: '/docs/reference/stream-helpers/inkeep-stream', }, ]} /> --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-api-call-error.mdx --- --- title: AI_APICallError description: Learn how to fix AI_APICallError --- # AI_APICallError This error occurs when an API call fails. ## Properties - `url`: The URL of the API request that failed - `requestBodyValues`: The request body values sent to the API - `statusCode`: The HTTP status code returned by the API - `responseHeaders`: The response headers returned by the API - `responseBody`: The response body returned by the API - `isRetryable`: Whether the request can be retried based on the status code - `data`: Any additional data associated with the error ## Checking for this Error You can check if an error is an instance of `AI_APICallError` using: ```typescript import { APICallError } from 'ai'; if (APICallError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-download-error.mdx --- --- title: AI_DownloadError description: Learn how to fix AI_DownloadError --- # AI_DownloadError This error occurs when a download fails. ## Properties - `url`: The URL that failed to download - `statusCode`: The HTTP status code returned by the server - `statusText`: The HTTP status text returned by the server - `message`: The error message containing details about the download failure ## Checking for this Error You can check if an error is an instance of `AI_DownloadError` using: ```typescript import { DownloadError } from 'ai'; if (DownloadError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-empty-response-body-error.mdx --- --- title: AI_EmptyResponseBodyError description: Learn how to fix AI_EmptyResponseBodyError --- # AI_EmptyResponseBodyError This error occurs when the server returns an empty response body. ## Properties - `message`: The error message ## Checking for this Error You can check if an error is an instance of `AI_EmptyResponseBodyError` using: ```typescript import { EmptyResponseBodyError } from 'ai'; if (EmptyResponseBodyError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-invalid-argument-error.mdx --- --- title: AI_InvalidArgumentError description: Learn how to fix AI_InvalidArgumentError --- # AI_InvalidArgumentError This error occurs when an invalid argument was provided. ## Properties - `parameter`: The name of the parameter that is invalid - `value`: The invalid value - `message`: The error message ## Checking for this Error You can check if an error is an instance of `AI_InvalidArgumentError` using: ```typescript import { InvalidArgumentError } from 'ai'; if (InvalidArgumentError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-invalid-data-content-error.mdx --- --- title: AI_InvalidDataContentError description: How to fix AI_InvalidDataContentError --- # AI_InvalidDataContentError This error occurs when the data content provided in a multi-modal message part is invalid. Check out the [ prompt examples for multi-modal messages ](/docs/foundations/prompts#message-prompts). ## Properties - `content`: The invalid content value - `message`: The error message describing the expected and received content types ## Checking for this Error You can check if an error is an instance of `AI_InvalidDataContentError` using: ```typescript import { InvalidDataContentError } from 'ai'; if (InvalidDataContentError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-invalid-data-content.mdx --- --- title: AI_InvalidDataContent description: Learn how to fix AI_InvalidDataContent --- # AI_InvalidDataContent This error occurs when invalid data content is provided. ## Properties - `content`: The invalid content value - `message`: The error message - `cause`: The cause of the error ## Checking for this Error You can check if an error is an instance of `AI_InvalidDataContent` using: ```typescript import { InvalidDataContent } from 'ai'; if (InvalidDataContent.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-invalid-message-role-error.mdx --- --- title: AI_InvalidMessageRoleError description: Learn how to fix AI_InvalidMessageRoleError --- # AI_InvalidMessageRoleError This error occurs when an invalid message role is provided. ## Properties - `role`: The invalid role value - `message`: The error message ## Checking for this Error You can check if an error is an instance of `AI_InvalidMessageRoleError` using: ```typescript import { InvalidMessageRoleError } from 'ai'; if (InvalidMessageRoleError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-invalid-prompt-error.mdx --- --- title: AI_InvalidPromptError description: Learn how to fix AI_InvalidPromptError --- # AI_InvalidPromptError This error occurs when the prompt provided is invalid. ## Properties - `prompt`: The invalid prompt value - `message`: The error message - `cause`: The cause of the error ## Checking for this Error You can check if an error is an instance of `AI_InvalidPromptError` using: ```typescript import { InvalidPromptError } from 'ai'; if (InvalidPromptError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-invalid-response-data-error.mdx --- --- title: AI_InvalidResponseDataError description: Learn how to fix AI_InvalidResponseDataError --- # AI_InvalidResponseDataError This error occurs when the server returns a response with invalid data content. ## Properties - `data`: The invalid response data value - `message`: The error message ## Checking for this Error You can check if an error is an instance of `AI_InvalidResponseDataError` using: ```typescript import { InvalidResponseDataError } from 'ai'; if (InvalidResponseDataError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-invalid-tool-arguments-error.mdx --- --- title: AI_InvalidToolArgumentsError description: Learn how to fix AI_InvalidToolArgumentsError --- # AI_InvalidToolArgumentsError This error occurs when invalid tool argument was provided. ## Properties - `toolName`: The name of the tool with invalid arguments - `toolArgs`: The invalid tool arguments - `message`: The error message - `cause`: The cause of the error ## Checking for this Error You can check if an error is an instance of `AI_InvalidToolArgumentsError` using: ```typescript import { InvalidToolArgumentsError } from 'ai'; if (InvalidToolArgumentsError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-json-parse-error.mdx --- --- title: AI_JSONParseError description: Learn how to fix AI_JSONParseError --- # AI_JSONParseError This error occurs when JSON fails to parse. ## Properties - `text`: The text value that could not be parsed - `message`: The error message including parse error details ## Checking for this Error You can check if an error is an instance of `AI_JSONParseError` using: ```typescript import { JSONParseError } from 'ai'; if (JSONParseError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-load-api-key-error.mdx --- --- title: AI_LoadAPIKeyError description: Learn how to fix AI_LoadAPIKeyError --- # AI_LoadAPIKeyError This error occurs when API key is not loaded successfully. ## Properties - `message`: The error message ## Checking for this Error You can check if an error is an instance of `AI_LoadAPIKeyError` using: ```typescript import { LoadAPIKeyError } from 'ai'; if (LoadAPIKeyError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-load-setting-error.mdx --- --- title: AI_LoadSettingError description: Learn how to fix AI_LoadSettingError --- # AI_LoadSettingError This error occurs when a setting is not loaded successfully. ## Properties - `message`: The error message ## Checking for this Error You can check if an error is an instance of `AI_LoadSettingError` using: ```typescript import { LoadSettingError } from 'ai'; if (LoadSettingError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-message-conversion-error.mdx --- --- title: AI_MessageConversionError description: Learn how to fix AI_MessageConversionError --- # AI_MessageConversionError This error occurs when message conversion fails. ## Properties - `originalMessage`: The original message that failed conversion - `message`: The error message ## Checking for this Error You can check if an error is an instance of `AI_MessageConversionError` using: ```typescript import { MessageConversionError } from 'ai'; if (MessageConversionError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-no-audio-generated-error.mdx --- --- title: AI_NoAudioGeneratedError description: Learn how to fix AI_NoAudioGeneratedError --- # AI_NoAudioGeneratedError This error occurs when no audio could be generated from the input. ## Properties - `responses`: Array of responses - `message`: The error message ## Checking for this Error You can check if an error is an instance of `AI_NoAudioGeneratedError` using: ```typescript import { NoAudioGeneratedError } from 'ai'; if (NoAudioGeneratedError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-no-content-generated-error.mdx --- --- title: AI_NoContentGeneratedError description: Learn how to fix AI_NoContentGeneratedError --- # AI_NoContentGeneratedError This error occurs when the AI provider fails to generate content. ## Properties - `message`: The error message ## Checking for this Error You can check if an error is an instance of `AI_NoContentGeneratedError` using: ```typescript import { NoContentGeneratedError } from 'ai'; if (NoContentGeneratedError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-no-image-generated-error.mdx --- --- title: AI_NoImageGeneratedError description: Learn how to fix AI_NoImageGeneratedError --- # AI_NoImageGeneratedError This error occurs when the AI provider fails to generate an image. It can arise due to the following reasons: - The model failed to generate a response. - The model generated an invalid response. ## Properties - `message`: The error message. - `responses`: Metadata about the image model responses, including timestamp, model, and headers. - `cause`: The cause of the error. You can use this for more detailed error handling. ## Checking for this Error You can check if an error is an instance of `AI_NoImageGeneratedError` using: ```typescript import { generateImage, NoImageGeneratedError } from 'ai'; try { await generateImage({ model, prompt }); } catch (error) { if (NoImageGeneratedError.isInstance(error)) { console.log('NoImageGeneratedError'); console.log('Cause:', error.cause); console.log('Responses:', error.responses); } } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-no-object-generated-error.mdx --- --- title: AI_NoObjectGeneratedError description: Learn how to fix AI_NoObjectGeneratedError --- # AI_NoObjectGeneratedError This error occurs when the AI provider fails to generate a parsable object that conforms to the schema. It can arise due to the following reasons: - The model failed to generate a response. - The model generated a response that could not be parsed. - The model generated a response that could not be validated against the schema. ## Properties - `message`: The error message. - `text`: The text that was generated by the model. This can be the raw text or the tool call text, depending on the object generation mode. - `response`: Metadata about the language model response, including response id, timestamp, and model. - `usage`: Request token usage. - `finishReason`: Request finish reason. For example 'length' if model generated maximum number of tokens, this could result in a JSON parsing error. - `cause`: The cause of the error (e.g. a JSON parsing error). You can use this for more detailed error handling. ## Checking for this Error You can check if an error is an instance of `AI_NoObjectGeneratedError` using: ```typescript import { generateObject, NoObjectGeneratedError } from 'ai'; try { await generateObject({ model, schema, prompt }); } catch (error) { if (NoObjectGeneratedError.isInstance(error)) { console.log('NoObjectGeneratedError'); console.log('Cause:', error.cause); console.log('Text:', error.text); console.log('Response:', error.response); console.log('Usage:', error.usage); console.log('Finish Reason:', error.finishReason); } } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-no-output-specified-error.mdx --- --- title: AI_NoOutputSpecifiedError description: Learn how to fix AI_NoOutputSpecifiedError --- # AI_NoOutputSpecifiedError This error occurs when no output format was specified for the AI response, and output-related methods are called. ## Properties - `message`: The error message (defaults to 'No output specified.') ## Checking for this Error You can check if an error is an instance of `AI_NoOutputSpecifiedError` using: ```typescript import { NoOutputSpecifiedError } from 'ai'; if (NoOutputSpecifiedError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-no-such-model-error.mdx --- --- title: AI_NoSuchModelError description: Learn how to fix AI_NoSuchModelError --- # AI_NoSuchModelError This error occurs when a model ID is not found. ## Properties - `modelId`: The ID of the model that was not found - `modelType`: The type of model - `message`: The error message ## Checking for this Error You can check if an error is an instance of `AI_NoSuchModelError` using: ```typescript import { NoSuchModelError } from 'ai'; if (NoSuchModelError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-no-such-provider-error.mdx --- --- title: AI_NoSuchProviderError description: Learn how to fix AI_NoSuchProviderError --- # AI_NoSuchProviderError This error occurs when a provider ID is not found. ## Properties - `providerId`: The ID of the provider that was not found - `availableProviders`: Array of available provider IDs - `modelId`: The ID of the model - `modelType`: The type of model - `message`: The error message ## Checking for this Error You can check if an error is an instance of `AI_NoSuchProviderError` using: ```typescript import { NoSuchProviderError } from 'ai'; if (NoSuchProviderError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-no-such-tool-error.mdx --- --- title: AI_NoSuchToolError description: Learn how to fix AI_NoSuchToolError --- # AI_NoSuchToolError This error occurs when a model tries to call an unavailable tool. ## Properties - `toolName`: The name of the tool that was not found - `availableTools`: Array of available tool names - `message`: The error message ## Checking for this Error You can check if an error is an instance of `AI_NoSuchToolError` using: ```typescript import { NoSuchToolError } from 'ai'; if (NoSuchToolError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-no-transcript-generated-error.mdx --- --- title: AI_NoTranscriptGeneratedError description: Learn how to fix AI_NoTranscriptGeneratedError --- # AI_NoTranscriptGeneratedError This error occurs when no transcript could be generated from the input. ## Properties - `responses`: Array of responses - `message`: The error message ## Checking for this Error You can check if an error is an instance of `AI_NoTranscriptGeneratedError` using: ```typescript import { NoTranscriptGeneratedError } from 'ai'; if (NoTranscriptGeneratedError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-retry-error.mdx --- --- title: AI_RetryError description: Learn how to fix AI_RetryError --- # AI_RetryError This error occurs when a retry operation fails. ## Properties - `reason`: The reason for the retry failure - `lastError`: The most recent error that occurred during retries - `errors`: Array of all errors that occurred during retry attempts - `message`: The error message ## Checking for this Error You can check if an error is an instance of `AI_RetryError` using: ```typescript import { RetryError } from 'ai'; if (RetryError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-too-many-embedding-values-for-call-error.mdx --- --- title: AI_TooManyEmbeddingValuesForCallError description: Learn how to fix AI_TooManyEmbeddingValuesForCallError --- # AI_TooManyEmbeddingValuesForCallError This error occurs when too many values are provided in a single embedding call. ## Properties - `provider`: The AI provider name - `modelId`: The ID of the embedding model - `maxEmbeddingsPerCall`: The maximum number of embeddings allowed per call - `values`: The array of values that was provided ## Checking for this Error You can check if an error is an instance of `AI_TooManyEmbeddingValuesForCallError` using: ```typescript import { TooManyEmbeddingValuesForCallError } from 'ai'; if (TooManyEmbeddingValuesForCallError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-tool-call-repair-error.mdx --- --- title: ToolCallRepairError description: Learn how to fix AI SDK ToolCallRepairError --- # ToolCallRepairError This error occurs when there is a failure while attempting to repair an invalid tool call. This typically happens when the AI attempts to fix either a `NoSuchToolError` or `InvalidToolArgumentsError`. ## Properties - `originalError`: The original error that triggered the repair attempt (either `NoSuchToolError` or `InvalidToolArgumentsError`) - `message`: The error message - `cause`: The underlying error that caused the repair to fail ## Checking for this Error You can check if an error is an instance of `ToolCallRepairError` using: ```typescript import { ToolCallRepairError } from 'ai'; if (ToolCallRepairError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-tool-execution-error.mdx --- --- title: AI_ToolExecutionError description: Learn how to fix AI_ToolExecutionError --- # AI_ToolExecutionError This error occurs when there is a failure during the execution of a tool. ## Properties - `toolName`: The name of the tool that failed - `toolArgs`: The arguments passed to the tool - `toolCallId`: The ID of the tool call that failed - `message`: The error message - `cause`: The underlying error that caused the tool execution to fail ## Checking for this Error You can check if an error is an instance of `AI_ToolExecutionError` using: ```typescript import { ToolExecutionError } from 'ai'; if (ToolExecutionError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-type-validation-error.mdx --- --- title: AI_TypeValidationError description: Learn how to fix AI_TypeValidationError --- # AI_TypeValidationError This error occurs when type validation fails. ## Properties - `value`: The value that failed validation - `message`: The error message including validation details ## Checking for this Error You can check if an error is an instance of `AI_TypeValidationError` using: ```typescript import { TypeValidationError } from 'ai'; if (TypeValidationError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/ai-unsupported-functionality-error.mdx --- --- title: AI_UnsupportedFunctionalityError description: Learn how to fix AI_UnsupportedFunctionalityError --- # AI_UnsupportedFunctionalityError This error occurs when functionality is not unsupported. ## Properties - `functionality`: The name of the unsupported functionality - `message`: The error message ## Checking for this Error You can check if an error is an instance of `AI_UnsupportedFunctionalityError` using: ```typescript import { UnsupportedFunctionalityError } from 'ai'; if (UnsupportedFunctionalityError.isInstance(error)) { // Handle the error } ``` --- File: /ai/content/docs/07-reference/05-ai-sdk-errors/index.mdx --- --- title: AI SDK Errors description: Troubleshooting information for common AI SDK errors. collapsed: true --- # AI SDK Errors - [AI_APICallError](/docs/reference/ai-sdk-errors/ai-api-call-error) - [AI_DownloadError](/docs/reference/ai-sdk-errors/ai-download-error) - [AI_EmptyResponseBodyError](/docs/reference/ai-sdk-errors/ai-empty-response-body-error) - [AI_InvalidArgumentError](/docs/reference/ai-sdk-errors/ai-invalid-argument-error) - [AI_InvalidDataContent](/docs/reference/ai-sdk-errors/ai-invalid-data-content) - [AI_InvalidDataContentError](/docs/reference/ai-sdk-errors/ai-invalid-data-content-error) - [AI_InvalidMessageRoleError](/docs/reference/ai-sdk-errors/ai-invalid-message-role-error) - [AI_InvalidPromptError](/docs/reference/ai-sdk-errors/ai-invalid-prompt-error) - [AI_InvalidResponseDataError](/docs/reference/ai-sdk-errors/ai-invalid-response-data-error) - [AI_InvalidToolArgumentsError](/docs/reference/ai-sdk-errors/ai-invalid-tool-arguments-error) - [AI_JSONParseError](/docs/reference/ai-sdk-errors/ai-json-parse-error) - [AI_LoadAPIKeyError](/docs/reference/ai-sdk-errors/ai-load-api-key-error) - [AI_LoadSettingError](/docs/reference/ai-sdk-errors/ai-load-setting-error) - [AI_MessageConversionError](/docs/reference/ai-sdk-errors/ai-message-conversion-error) - [AI_NoAudioGeneratedError](/docs/reference/ai-sdk-errors/ai-no-audio-generated-error) - [AI_NoContentGeneratedError](/docs/reference/ai-sdk-errors/ai-no-content-generated-error) - [AI_NoImageGeneratedError](/docs/reference/ai-sdk-errors/ai-no-image-generated-error) - [AI_NoTranscriptGeneratedError](/docs/reference/ai-sdk-errors/ai-no-transcript-generated-error) - [AI_NoObjectGeneratedError](/docs/reference/ai-sdk-errors/ai-no-object-generated-error) - [AI_NoOutputSpecifiedError](/docs/reference/ai-sdk-errors/ai-no-output-specified-error) - [AI_NoSuchModelError](/docs/reference/ai-sdk-errors/ai-no-such-model-error) - [AI_NoSuchProviderError](/docs/reference/ai-sdk-errors/ai-no-such-provider-error) - [AI_NoSuchToolError](/docs/reference/ai-sdk-errors/ai-no-such-tool-error) - [AI_RetryError](/docs/reference/ai-sdk-errors/ai-retry-error) - [AI_ToolCallRepairError](/docs/reference/ai-sdk-errors/ai-tool-call-repair-error) - [AI_ToolExecutionError](/docs/reference/ai-sdk-errors/ai-tool-execution-error) - [AI_TooManyEmbeddingValuesForCallError](/docs/reference/ai-sdk-errors/ai-too-many-embedding-values-for-call-error) - [AI_TypeValidationError](/docs/reference/ai-sdk-errors/ai-type-validation-error) - [AI_UnsupportedFunctionalityError](/docs/reference/ai-sdk-errors/ai-unsupported-functionality-error) --- File: /ai/content/docs/07-reference/index.mdx --- --- title: Reference description: Reference documentation for the AI SDK --- # API Reference <IndexCards cards={[ { title: 'AI SDK Core', description: 'Switch between model providers without changing your code.', href: '/docs/reference/ai-sdk-core', }, { title: 'AI SDK RSC', description: 'Use React Server Components to stream user interfaces to the client.', href: '/docs/reference/ai-sdk-rsc', }, { title: 'AI SDK UI', description: 'Use hooks to integrate user interfaces that interact with language models.', href: '/docs/reference/ai-sdk-ui', }, { title: 'Stream Helpers', description: 'Use special functions that help stream model generations from various providers.', href: '/docs/reference/stream-helpers', }, ]} /> --- File: /ai/content/docs/08-migration-guides/00-versioning.mdx --- --- title: Versioning description: Understand how the AI SDK approaches versioning. --- # Versioning Each version number follows the format: `MAJOR.MINOR.PATCH` - **Major**: Breaking API updates that require code changes. - **Minor**: Blog post that aggregates new features and improvements into a public release that highlights benefits. - **Patch**: New features and bug fixes. ## API Stability We communicate the stability of our APIs as follows: ### Stable APIs All APIs without special prefixes are considered stable and ready for production use. We maintain backward compatibility for stable features and only introduce breaking changes in major releases. ### Experimental APIs APIs prefixed with `experimental_` or `Experimental_` (e.g. `experimental_generateImage()`) are in development and can change in any releases. To use experimental APIs safely: 1. Test them first in development, not production 2. Review release notes before upgrading 3. Prepare for potential code updates <Note type="warning"> If you use experimental APIs, make sure to pin your AI SDK version number exactly (avoid using ^ or ~ version ranges) to prevent unexpected breaking changes. </Note> ### Deprecated APIs APIs marked as `deprecated` will be removed in future major releases. You can wait until the major release to update your code. To handle deprecations: 1. Switch to the recommended alternative API 2. Follow the migration guide (released alongside major releases) <Note> For major releases, we provide automated codemods where possible to help migrate your code to the new version. </Note> --- File: /ai/content/docs/08-migration-guides/26-migration-guide-5-0.mdx --- --- title: Migrate AI SDK 4.0 to 5.0 description: Learn how to upgrade AI SDK 4.0 to 5.0. --- # Migrate AI SDK 4.0 to 5.0 ## Recommended Migration Process 1. Backup your project. If you use a versioning control system, make sure all previous versions are committed. 1. Upgrade to AI SDK 5.0. 1. Automatically migrate your code using [codemods](#codemods). > If you don't want to use codemods, we recommend resolving all deprecation warnings before upgrading to AI SDK 5.0. 1. Follow the breaking changes guide below. 1. Verify your project is working as expected. 1. Commit your changes. ## AI SDK 5.0 Package Versions You need to update the following packages to the following versions in your `package.json` file(s): - `ai` package: `5.0.0` - `@ai-sdk/provider` package: `2.0.0` - `@ai-sdk/provider-utils` package: `3.0.0` - `@ai-sdk/*` packages: `2.0.0` (other `@ai-sdk` packages) Additionally, you need to update the following peer dependencies: - `zod` package: `3.25.0` or later An example upgrade command would be: ``` npm install ai @ai-sdk/react @ai-sdk/openai zod@3.25.0 ``` ## Codemods The AI SDK provides Codemod transformations to help upgrade your codebase when a feature is deprecated, removed, or otherwise changed. Codemods are transformations that run on your codebase automatically. They allow you to easily apply many changes without having to manually go through every file. <Note> Codemods are intended as a tool to help you with the upgrade process. They may not cover all of the changes you need to make. You may need to make additional changes manually. </Note> You can run all codemods provided as part of the 5.0 upgrade process by running the following command from the root of your project: ```sh npx @ai-sdk/codemod upgrade ``` To run only the v5 codemods (v4 → v5 migration): ```sh npx @ai-sdk/codemod v5 ``` Individual codemods can be run by specifying the name of the codemod: ```sh npx @ai-sdk/codemod <codemod-name> <path> ``` For example, to run a specific v5 codemod: ```sh npx @ai-sdk/codemod v5/rename-format-stream-part src/ ``` See also the [table of codemods](#codemod-table). In addition, the latest set of codemods can be found in the [`@ai-sdk/codemod`](https://github.com/vercel/ai/tree/main/packages/codemod/src/codemods) repository. ## AI SDK Core Changes ### generateText and streamText Changes #### Maximum Output Tokens The `maxTokens` parameter has been renamed to `maxOutputTokens` for clarity. ```tsx filename="AI SDK 4.0" const result = await generateText({ model: openai('gpt-4.1'), maxTokens: 1024, prompt: 'Hello, world!', }); ``` ```tsx filename="AI SDK 5.0" const result = await generateText({ model: openai('gpt-4.1'), maxOutputTokens: 1024, prompt: 'Hello, world!', }); ``` ### Message and Type System Changes #### Core Type Renames ##### `CoreMessage` → `ModelMessage` ```tsx filename="AI SDK 4.0" import { CoreMessage } from 'ai'; ``` ```tsx filename="AI SDK 5.0" import { ModelMessage } from 'ai'; ``` ##### `Message` → `UIMessage` ```tsx filename="AI SDK 4.0" import { Message, CreateMessage } from 'ai'; ``` ```tsx filename="AI SDK 5.0" import { UIMessage, CreateUIMessage } from 'ai'; ``` ##### `convertToCoreMessages` → `convertToModelMessages` ```tsx filename="AI SDK 4.0" import { convertToCoreMessages, streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = await streamText({ model: openai('gpt-4'), messages: convertToCoreMessages(messages), }); ``` ```tsx filename="AI SDK 5.0" import { convertToModelMessages, streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = await streamText({ model: openai('gpt-4'), messages: convertToModelMessages(messages), }); ``` <Note> For more information about model messages, see the [Model Message reference](/docs/reference/ai-sdk-core/model-message). </Note> ### UIMessage Changes #### Content → Parts Array For `UIMessage`s (previously called `Message`), the `.content` property has been replaced with a `parts` array structure. ```tsx filename="AI SDK 4.0" import { type Message } from 'ai'; // v4 Message type // Messages (useChat) - had content property const message: Message = { id: '1', role: 'user', content: 'Bonjour!', }; ``` ```tsx filename="AI SDK 5.0" import { type UIMessage, type ModelMessage } from 'ai'; // UIMessages (useChat) - now use parts array const uiMessage: UIMessage = { id: '1', role: 'user', parts: [{ type: 'text', text: 'Bonjour!' }], }; ``` #### Data Role Removed The `data` role has been removed from UI messages. ```tsx filename="AI SDK 4.0" const message = { role: 'data', content: 'Some content', data: { customField: 'value' }, }; ``` ```tsx filename="AI SDK 5.0" // V5: Use UI message streams with custom data parts const stream = createUIMessageStream({ execute({ writer }) { // Write custom data instead of message annotations writer.write({ type: 'data-custom', id: 'custom-1', data: { customField: 'value' }, }); }, }); ``` #### UIMessage Reasoning Structure The reasoning property on UI messages has been moved to parts. ```tsx filename="AI SDK 4.0" const message: Message = { role: 'assistant', content: 'Hello', reasoning: 'I will greet the user', }; ``` ```tsx filename="AI SDK 5.0" const message: UIMessage = { role: 'assistant', parts: [ { type: 'reasoning', text: 'I will greet the user', }, { type: 'text', text: 'Hello', }, ], }; ``` #### Reasoning Part Property Rename The `reasoning` property on reasoning UI parts has been renamed to `text`. ```tsx filename="AI SDK 4.0" { message.parts.map((part, index) => { if (part.type === 'reasoning') { return ( <div key={index} className="reasoning-display"> {part.reasoning} </div> ); } }); } ``` ```tsx filename="AI SDK 5.0" { message.parts.map((part, index) => { if (part.type === 'reasoning') { return ( <div key={index} className="reasoning-display"> {part.text} </div> ); } }); } ``` ### File Part Changes File parts now use `.url` instead of `.data` and `.mimeType`. ```tsx filename="AI SDK 4.0" { messages.map(message => ( <div key={message.id}> {message.parts.map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } else if (part.type === 'file' && part.mimeType.startsWith('image/')) { return ( <img key={index} src={`data:${part.mimeType};base64,${part.data}`} /> ); } })} </div> )); } ``` ```tsx filename="AI SDK 5.0" { messages.map(message => ( <div key={message.id}> {message.parts.map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } else if ( part.type === 'file' && part.mediaType.startsWith('image/') ) { return <img key={index} src={part.url} />; } })} </div> )); } ``` ### Stream Data Removal The `StreamData` class has been completely removed and replaced with UI message streams for custom data. ```tsx filename="AI SDK 4.0" import { StreamData } from 'ai'; const streamData = new StreamData(); streamData.append('custom-data'); streamData.close(); ``` ```tsx filename="AI SDK 5.0" import { createUIMessageStream, createUIMessageStreamResponse } from 'ai'; const stream = createUIMessageStream({ execute({ writer }) { // Write custom data parts writer.write({ type: 'data-custom', id: 'custom-1', data: 'custom-data', }); // Can merge with LLM streams const result = streamText({ model: openai('gpt-4.1'), messages, }); writer.merge(result.toUIMessageStream()); }, }); return createUIMessageStreamResponse({ stream }); ``` ### Custom Data Streaming: writeMessageAnnotation/writeData Removed The `writeMessageAnnotation` and `writeData` methods from `DataStreamWriter` have been removed. Instead, use custom data parts with the new `UIMessage` stream architecture. ```tsx filename="AI SDK 4.0" import { openai } from '@ai-sdk/openai'; import { createDataStreamResponse, streamText } from 'ai'; export async function POST(req: Request) { const { messages } = await req.json(); return createDataStreamResponse({ execute: dataStream => { // Write general data dataStream.writeData('call started'); const result = streamText({ model: openai('gpt-4o'), messages, onChunk() { // Write message annotations dataStream.writeMessageAnnotation({ status: 'streaming', timestamp: Date.now(), }); }, onFinish() { // Write final annotations dataStream.writeMessageAnnotation({ id: generateId(), completed: true, }); dataStream.writeData('call completed'); }, }); result.mergeIntoDataStream(dataStream); }, }); } ``` ```tsx filename="AI SDK 5.0" import { openai } from '@ai-sdk/openai'; import { createUIMessageStream, createUIMessageStreamResponse, streamText, generateId, } from 'ai'; export async function POST(req: Request) { const { messages } = await req.json(); const stream = createUIMessageStream({ execute: ({ writer }) => { const statusId = generateId(); // Write general data (transient - not added to message history) writer.write({ type: 'data-status', id: statusId, data: { status: 'call started' }, }); // Generate shared IDs for data parts const completionId = generateId(); const result = streamText({ model: openai('gpt-4o'), messages, onChunk() { // Write data parts that update during streaming writer.write({ type: 'data-status', id: statusId, data: { status: 'streaming', timestamp: Date.now(), }, }); }, onFinish() { // Write final data parts writer.write({ type: 'data-status', id: statusId, data: { status: 'completed', }, }); }, }); writer.merge(result.toUIMessageStream()); }, }); return createUIMessageStreamResponse({ stream }); } ``` <Note> For more detailed information about streaming custom data in v5, see the [Streaming Data guide](/docs/ai-sdk-ui/streaming-data). </Note> ##### Provider Metadata → Provider Options The `providerMetadata` input parameter has been renamed to `providerOptions`. Note that the returned metadata in results is still called `providerMetadata`. ```tsx filename="AI SDK 4.0" const result = await generateText({ model: openai('gpt-4'), prompt: 'Hello', providerMetadata: { openai: { store: false }, }, }); ``` ```tsx filename="AI SDK 5.0" const result = await generateText({ model: openai('gpt-4'), prompt: 'Hello', providerOptions: { // Input parameter renamed openai: { store: false }, }, }); // Returned metadata still uses providerMetadata: console.log(result.providerMetadata?.openai); ``` #### Tool Definition Changes (parameters → inputSchema) Tool definitions have been updated to use `inputSchema` instead of `parameters` and error classes have been renamed. ```tsx filename="AI SDK 4.0" import { tool } from 'ai'; const weatherTool = tool({ description: 'Get the weather for a city', parameters: z.object({ city: z.string(), }), execute: async ({ city }) => { return `Weather in ${city}`; }, }); ``` ```tsx filename="AI SDK 5.0" import { tool } from 'ai'; const weatherTool = tool({ description: 'Get the weather for a city', inputSchema: z.object({ city: z.string(), }), execute: async ({ city }) => { return `Weather in ${city}`; }, }); ``` #### Tool Result Content: experimental_toToolResultContent → toModelOutput The `experimental_toToolResultContent` option has been renamed to `toModelOutput` and is no longer experimental. ```tsx filename="AI SDK 4.0" const screenshotTool = tool({ description: 'Take a screenshot', parameters: z.object({}), execute: async () => { const imageData = await takeScreenshot(); return imageData; // base64 string }, experimental_toToolResultContent: result => [{ type: 'image', data: result }], }); ``` ```tsx filename="AI SDK 5.0" const screenshotTool = tool({ description: 'Take a screenshot', inputSchema: z.object({}), execute: async () => { const imageData = await takeScreenshot(); return imageData; }, toModelOutput: result => ({ type: 'content', value: [{ type: 'media', mediaType: 'image/png', data: result }], }), }); ``` ### Tool Property Changes (args/result → input/output) Tool call and result properties have been renamed for better consistency with schemas. ```tsx filename="AI SDK 4.0" // Tool calls used "args" and "result" for await (const part of result.fullStream) { switch (part.type) { case 'tool-call': console.log('Tool args:', part.args); break; case 'tool-result': console.log('Tool result:', part.result); break; } } ``` ```tsx filename="AI SDK 5.0" // Tool calls now use "input" and "output" for await (const part of result.fullStream) { switch (part.type) { case 'tool-call': console.log('Tool input:', part.input); break; case 'tool-result': console.log('Tool output:', part.output); break; } } ``` ### Tool Call Streaming Now Default (toolCallStreaming Removed) The `toolCallStreaming` option has been removed in AI SDK 5.0. Tool call streaming is now always enabled by default. ```tsx filename="AI SDK 4.0" const result = streamText({ model: openai('gpt-4o'), messages, toolCallStreaming: true, // Optional parameter to enable streaming tools: { weatherTool, searchTool, }, }); ``` ```tsx filename="AI SDK 5.0" const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), // toolCallStreaming removed - streaming is always enabled tools: { weatherTool, searchTool, }, }); ``` ### Tool Part Type Changes (UIMessage) In v5, UI tool parts use typed naming: `tool-${toolName}` instead of generic types. ```tsx filename="AI SDK 4.0" // Generic tool-invocation type { message.parts.map(part => { if (part.type === 'tool-invocation') { return <div>{part.toolInvocation.toolName}</div>; } }); } ``` ```tsx filename="AI SDK 5.0" // Type-safe tool parts with specific names { message.parts.map(part => { switch (part.type) { case 'tool-getWeatherInformation': return <div>Getting weather...</div>; case 'tool-askForConfirmation': return <div>Asking for confirmation...</div>; } }); } ``` ### Dynamic Tools Support AI SDK 5.0 introduces dynamic tools for handling tools with unknown types at development time, such as MCP tools without schemas or user-defined functions at runtime. #### New dynamicTool Helper The new `dynamicTool` helper function allows you to define tools where the input and output types are not known at compile time. ```tsx filename="AI SDK 5.0" import { dynamicTool } from 'ai'; import { z } from 'zod'; // Define a dynamic tool const runtimeTool = dynamicTool({ description: 'A tool defined at runtime', inputSchema: z.object({}), execute: async input => { // Input and output are typed as 'unknown' return { result: `Processed: ${input.query}` }; }, }); ``` #### MCP Tools Without Schemas MCP tools that don't provide schemas are now automatically treated as dynamic tools: ```tsx filename="AI SDK 5.0" import { MCPClient } from 'ai'; const client = new MCPClient({ /* ... */ }); const tools = await client.getTools(); // Tools without schemas are now 'dynamic' type // and won't break type inference when mixed with static tools ``` #### Type-Safe Handling with Mixed Tools When using both static and dynamic tools together, use the `dynamic` flag for type narrowing: ```tsx filename="AI SDK 5.0" const result = await generateText({ model: openai('gpt-4'), tools: { // Static tool with known types weather: weatherTool, // Dynamic tool with unknown types customDynamicTool: dynamicTool({ /* ... */ }), }, onStepFinish: step => { // Handle tool calls with type safety for (const toolCall of step.toolCalls) { if (toolCall.dynamic) { // Dynamic tool: input/output are 'unknown' console.log('Dynamic tool called:', toolCall.toolName); continue; } // Static tools have full type inference switch (toolCall.toolName) { case 'weather': // TypeScript knows the exact types console.log(toolCall.input.location); // string break; } } }, }); ``` #### New dynamic-tool UI Part UI messages now include a `dynamic-tool` part type for rendering dynamic tool invocations: ```tsx filename="AI SDK 5.0" { message.parts.map((part, index) => { switch (part.type) { // Static tools use specific types case 'tool-weather': return <div>Weather: {part.input.city}</div>; // Dynamic tools use the generic dynamic-tool type case 'dynamic-tool': return ( <div> Dynamic tool: {part.toolName} <pre>{JSON.stringify(part.input, null, 2)}</pre> </div> ); } }); } ``` #### Breaking Change: Type Narrowing Required for Tool Calls and Results When iterating over `toolCalls` and `toolResults`, you now need to check the `dynamic` flag first for proper type narrowing: ```tsx filename="AI SDK 4.0" // Direct type checking worked without dynamic flag onStepFinish: step => { for (const toolCall of step.toolCalls) { switch (toolCall.toolName) { case 'weather': console.log(toolCall.input.location); // typed as string break; case 'search': console.log(toolCall.input.query); // typed as string break; } } }; ``` ```tsx filename="AI SDK 5.0" // Must check dynamic flag first for type narrowing onStepFinish: step => { for (const toolCall of step.toolCalls) { // Check if it's a dynamic tool first if (toolCall.dynamic) { console.log('Dynamic tool:', toolCall.toolName); console.log('Input:', toolCall.input); // typed as unknown continue; } // Now TypeScript knows it's a static tool switch (toolCall.toolName) { case 'weather': console.log(toolCall.input.location); // typed as string break; case 'search': console.log(toolCall.input.query); // typed as string break; } } }; ``` ### Tool UI Part State Changes Tool UI parts now use more granular states that better represent the streaming lifecycle and error handling. ```tsx filename="AI SDK 4.0" // Old states { message.parts.map(part => { if (part.type === 'tool-invocation') { switch (part.toolInvocation.state) { case 'partial-call': return <div>Loading...</div>; case 'call': return ( <div> Tool called with {JSON.stringify(part.toolInvocation.args)} </div> ); case 'result': return <div>Result: {part.toolInvocation.result}</div>; } } }); } ``` ```tsx filename="AI SDK 5.0" // New granular states { message.parts.map(part => { switch (part.type) { case 'tool-getWeatherInformation': switch (part.state) { case 'input-streaming': return <pre>{JSON.stringify(part.input, null, 2)}</pre>; case 'input-available': return <div>Getting weather for {part.input.city}...</div>; case 'output-available': return <div>Weather: {part.output}</div>; case 'output-error': return <div>Error: {part.errorText}</div>; } } }); } ``` **State Changes:** - `partial-call` → `input-streaming` (tool input being streamed) - `call` → `input-available` (tool input complete, ready to execute) - `result` → `output-available` (tool execution successful) - New: `output-error` (tool execution failed) #### Media Type Standardization `mimeType` has been renamed to `mediaType` for consistency. Both image and file types are supported in model messages. ```tsx filename="AI SDK 4.0" const result = await generateText({ model: someModel, messages: [ { role: 'user', content: [ { type: 'text', text: 'What do you see?' }, { type: 'image', image: new Uint8Array([0, 1, 2, 3]), mimeType: 'image/png', }, { type: 'file', data: contents, mimeType: 'application/pdf', }, ], }, ], }); ``` ```tsx filename="AI SDK 5.0" const result = await generateText({ model: someModel, messages: [ { role: 'user', content: [ { type: 'text', text: 'What do you see?' }, { type: 'image', image: new Uint8Array([0, 1, 2, 3]), mediaType: 'image/png', }, { type: 'file', data: contents, mediaType: 'application/pdf', }, ], }, ], }); ``` ### Reasoning Support #### Reasoning Text Property Rename The `.reasoning` property has been renamed to `.reasoningText` for multi-step generations. ```tsx filename="AI SDK 4.0" for (const step of steps) { console.log(step.reasoning); } ``` ```tsx filename="AI SDK 5.0" for (const step of steps) { console.log(step.reasoningText); } ``` #### Generate Text Reasoning Property Changes In `generateText()` and `streamText()` results, reasoning properties have been renamed. ```tsx filename="AI SDK 4.0" const result = await generateText({ model: anthropic('claude-sonnet-4-20250514'), prompt: 'Explain your reasoning', }); console.log(result.reasoning); // String reasoning text console.log(result.reasoningDetails); // Array of reasoning details ``` ```tsx filename="AI SDK 5.0" const result = await generateText({ model: anthropic('claude-sonnet-4-20250514'), prompt: 'Explain your reasoning', }); console.log(result.reasoningText); // String reasoning text console.log(result.reasoning); // Array of reasoning details ``` ### Continuation Steps Removal The `experimental_continueSteps` option has been removed from `generateText()`. ```tsx filename="AI SDK 4.0" const result = await generateText({ experimental_continueSteps: true, // ... }); ``` ```tsx filename="AI SDK 5.0" const result = await generateText({ // experimental_continueSteps has been removed // Use newer models with higher output token limits instead // ... }); ``` ### Image Generation Changes Image model settings have been moved to `providerOptions`. ```tsx filename="AI SDK 4.0" await generateImage({ model: luma.image('photon-flash-1', { maxImagesPerCall: 5, pollIntervalMillis: 500, }), prompt, n: 10, }); ``` ```tsx filename="AI SDK 5.0" await generateImage({ model: luma.image('photon-flash-1'), prompt, n: 10, maxImagesPerCall: 5, providerOptions: { luma: { pollIntervalMillis: 500 }, }, }); ``` ### Step Result Changes #### Step Type Removal The `stepType` property has been removed from step results. ```tsx filename="AI SDK 4.0" steps.forEach(step => { switch (step.stepType) { case 'initial': console.log('Initial step'); break; case 'tool-result': console.log('Tool result step'); break; case 'done': console.log('Final step'); break; } }); ``` ```tsx filename="AI SDK 5.0" steps.forEach((step, index) => { if (index === 0) { console.log('Initial step'); } else if (step.toolResults.length > 0) { console.log('Tool result step'); } else { console.log('Final step'); } }); ``` ### Step Control: maxSteps → stopWhen For core functions like `generateText` and `streamText`, the `maxSteps` parameter has been replaced with `stopWhen`, which provides more flexible control over multi-step execution. The `stopWhen` parameter defines conditions for stopping the generation **when the last step contains tool results**. When multiple conditions are provided as an array, the generation stops if any condition is met. ```tsx filename="AI SDK 4.0" // V4: Simple numeric limit const result = await generateText({ model: openai('gpt-4'), messages, maxSteps: 5, // Stop after a maximum of 5 steps }); // useChat with maxSteps const { messages } = useChat({ maxSteps: 3, // Stop after a maximum of 3 steps }); ``` ```tsx filename="AI SDK 5.0" import { stepCountIs, hasToolCall } from 'ai'; // V5: Server-side - flexible stopping conditions with stopWhen const result = await generateText({ model: openai('gpt-4'), messages, // Only triggers when last step has tool results stopWhen: stepCountIs(5), // Stop at step 5 if tools were called }); // Server-side - stop when specific tool is called const result = await generateText({ model: openai('gpt-4'), messages, stopWhen: hasToolCall('finalizeTask'), // Stop when finalizeTask tool is called }); ``` **Common stopping patterns:** ```tsx filename="AI SDK 5.0" // Stop after N steps (equivalent to old maxSteps) // Note: Only applies when the last step has tool results stopWhen: stepCountIs(5); // Stop when specific tool is called stopWhen: hasToolCall('finalizeTask'); // Multiple conditions (stops if ANY condition is met) stopWhen: [ stepCountIs(10), // Maximum 10 steps hasToolCall('submitOrder'), // Or when order is submitted ]; // Custom condition based on step content stopWhen: ({ steps }) => { const lastStep = steps[steps.length - 1]; // Custom logic - only triggers if last step has tool results return lastStep?.text?.includes('COMPLETE'); }; ``` **Important:** The `stopWhen` conditions are only evaluated when the last step contains tool results. #### Usage vs Total Usage Usage properties now distinguish between single step and total usage. ```tsx filename="AI SDK 4.0" // usage contained total token usage across all steps console.log(result.usage); ``` ```tsx filename="AI SDK 5.0" // usage contains token usage from the final step only console.log(result.usage); // totalUsage contains total token usage across all steps console.log(result.totalUsage); ``` ## AI SDK UI Changes ### Package Structure Changes ### `@ai-sdk/rsc` Package Extraction The `ai/rsc` export has been extracted to a separate package `@ai-sdk/rsc`. ```tsx filename="AI SDK 4.0" import { createStreamableValue } from 'ai/rsc'; ``` ```tsx filename="AI SDK 5.0" import { createStreamableValue } from '@ai-sdk/rsc'; ``` <Note>Don't forget to install the new package: `npm install @ai-sdk/rsc`</Note> ### React UI Hooks Moved to `@ai-sdk/react` The deprecated `ai/react` export has been removed in favor of `@ai-sdk/react`. ```tsx filename="AI SDK 4.0" import { useChat } from 'ai/react'; ``` ```tsx filename="AI SDK 5.0" import { useChat } from '@ai-sdk/react'; ``` <Note> Don't forget to install the new package: `npm install @ai-sdk/react` </Note> ### useChat Changes The `useChat` hook has undergone significant changes in v5, with new transport architecture, removal of managed input state, and more. #### maxSteps Removal The `maxSteps` parameter has been removed from `useChat`. You should now use server-side `stopWhen` conditions for multi-step tool execution control, and manually submit tool results and trigger new messages for client-side tool calls. ```tsx filename="AI SDK 4.0" const { messages, sendMessage } = useChat({ maxSteps: 5, // Automatic tool result submission }); ``` ```tsx filename="AI SDK 5.0" // Server-side: Use stopWhen for multi-step control import { streamText, convertToModelMessages, stepCountIs } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = await streamText({ model: openai('gpt-4'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), // Stop after 5 steps with tool calls }); // Client-side: Configure automatic submission import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls, } from 'ai'; const { messages, sendMessage, addToolResult } = useChat({ // Automatically submit when all tool results are available sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, async onToolCall({ toolCall }) { const result = await executeToolCall(toolCall); // Important: Don't await addToolResult inside onToolCall to avoid deadlocks addToolResult({ tool: toolCall.toolName, toolCallId: toolCall.toolCallId, output: result, }); }, }); ``` <Note> Important: When using `sendAutomaticallyWhen`, don't use `await` with `addToolResult` inside `onToolCall` as it can cause deadlocks. The `await` is useful when you're not using automatic submission and need to ensure the messages are updated before manually calling `sendMessage()`. </Note> This change provides more flexibility for handling tool calls and aligns client behavior with server-side multi-step execution patterns. For more details on the new tool submission approach, see the [Tool Result Submission Changes](#tool-result-submission-changes) section below. #### Initial Messages Renamed The `initialMessages` option has been renamed to `messages`. ```tsx filename="AI SDK 4.0" import { useChat, type Message } from '@ai-sdk/react'; function ChatComponent({ initialMessages }: { initialMessages: Message[] }) { const { messages } = useChat({ initialMessages: initialMessages, // ... }); // your component } ``` ```tsx filename="AI SDK 5.0" import { useChat, type UIMessage } from '@ai-sdk/react'; function ChatComponent({ initialMessages }: { initialMessages: UIMessage[] }) { const { messages } = useChat({ messages: initialMessages, // ... }); // your component } ``` #### Chat Transport Architecture Configuration is now handled through transport objects instead of direct API options. ```tsx filename="AI SDK 4.0" import { useChat } from '@ai-sdk/react'; const { messages } = useChat({ api: '/api/chat', credentials: 'include', headers: { 'Custom-Header': 'value' }, }); ``` ```tsx filename="AI SDK 5.0" import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; const { messages } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', credentials: 'include', headers: { 'Custom-Header': 'value' }, }), }); ``` #### Removed Managed Input State The `useChat` hook no longer manages input state internally. You must now manage input state manually. ```tsx filename="AI SDK 4.0" import { useChat } from '@ai-sdk/react'; export default function Page() { const { messages, input, handleInputChange, handleSubmit } = useChat({ api: '/api/chat', }); return ( <form onSubmit={handleSubmit}> <input value={input} onChange={handleInputChange} /> <button type="submit">Send</button> </form> ); } ``` ```tsx filename="AI SDK 5.0" import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat' }), }); const handleSubmit = e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }; return ( <form onSubmit={handleSubmit}> <input value={input} onChange={e => setInput(e.target.value)} /> <button type="submit">Send</button> </form> ); } ``` #### Message Sending: `append` → `sendMessage` The `append` function has been replaced with `sendMessage` and requires structured message format. ```tsx filename="AI SDK 4.0" const { append } = useChat(); // Simple text message append({ role: 'user', content: 'Hello' }); // With custom body append( { role: 'user', content: 'Hello', }, { body: { imageUrl: 'https://...' } }, ); ``` ```tsx filename="AI SDK 5.0" const { sendMessage } = useChat(); // Simple text message (most common usage) sendMessage({ text: 'Hello' }); // Or with explicit parts array sendMessage({ parts: [{ type: 'text', text: 'Hello' }], }); // With custom body (via request options) sendMessage( { role: 'user', parts: [{ type: 'text', text: 'Hello' }] }, { body: { imageUrl: 'https://...' } }, ); ``` #### Message Regeneration: `reload` → `regenerate` The `reload` function has been renamed to `regenerate` with enhanced functionality. ```tsx filename="AI SDK 4.0" const { reload } = useChat(); // Regenerate last message reload(); ``` ```tsx filename="AI SDK 5.0" const { regenerate } = useChat(); // Regenerate last message regenerate(); // Regenerate specific message regenerate({ messageId: 'message-123' }); ``` #### onResponse Removal The `onResponse` callback has been removed from `useChat` and `useCompletion`. ```tsx filename="AI SDK 4.0" const { messages } = useChat({ onResponse(response) { // handle response }, }); ``` ```tsx filename="AI SDK 5.0" const { messages } = useChat({ // onResponse is no longer available }); ``` #### Send Extra Message Fields Default The `sendExtraMessageFields` option has been removed and is now the default behavior. ```tsx filename="AI SDK 4.0" const { messages } = useChat({ sendExtraMessageFields: true, }); ``` ```tsx filename="AI SDK 5.0" const { messages } = useChat({ // sendExtraMessageFields is now the default }); ``` #### Keep Last Message on Error Removal The `keepLastMessageOnError` option has been removed as it's no longer needed. ```tsx filename="AI SDK 4.0" const { messages } = useChat({ keepLastMessageOnError: true, }); ``` ```tsx filename="AI SDK 5.0" const { messages } = useChat({ // keepLastMessageOnError is no longer needed }); ``` #### Chat Request Options Changes The `data` and `allowEmptySubmit` options have been removed from `ChatRequestOptions`. ```tsx filename="AI SDK 4.0" handleSubmit(e, { data: { imageUrl: 'https://...' }, body: { custom: 'value' }, allowEmptySubmit: true, }); ``` ```tsx filename="AI SDK 5.0" sendMessage( { /* yourMessage */ }, { body: { custom: 'value', imageUrl: 'https://...', // Move data to body }, }, ); ``` #### Request Options Type Rename `RequestOptions` has been renamed to `CompletionRequestOptions`. ```tsx filename="AI SDK 4.0" import type { RequestOptions } from 'ai'; ``` ```tsx filename="AI SDK 5.0" import type { CompletionRequestOptions } from 'ai'; ``` #### addToolResult Changes In the `addToolResult` function, the `result` parameter has been renamed to `output` for consistency with other tool-related APIs. ```tsx filename="AI SDK 4.0" const { addToolResult } = useChat(); // Add tool result with 'result' parameter addToolResult({ toolCallId: 'tool-call-123', result: 'Weather: 72°F, sunny', }); ``` ```tsx filename="AI SDK 5.0" const { addToolResult } = useChat(); // Add tool result with 'output' parameter and 'tool' name for type safety addToolResult({ tool: 'getWeather', toolCallId: 'tool-call-123', output: 'Weather: 72°F, sunny', }); ``` #### Tool Result Submission Changes The automatic tool result submission behavior has been updated in `useChat` and the `Chat` component. You now have more control and flexibility over when tool results are submitted. - `onToolCall` no longer supports returning values to automatically submit tool results - You must explicitly call `addToolResult` to provide tool results - Use `sendAutomaticallyWhen` with `lastAssistantMessageIsCompleteWithToolCalls` helper for automatic submission - Important: Don't use `await` with `addToolResult` inside `onToolCall` to avoid deadlocks - The `maxSteps` parameter has been removed from the `Chat` component and `useChat` hook - For multi-step tool execution, use server-side `stopWhen` conditions instead (see [maxSteps Removal](#maxsteps-removal)) ```tsx filename="AI SDK 4.0" const { messages, sendMessage, addToolResult } = useChat({ maxSteps: 5, // Removed in v5 // Automatic submission by returning a value async onToolCall({ toolCall }) { if (toolCall.toolName === 'getLocation') { const cities = ['New York', 'Los Angeles', 'Chicago', 'San Francisco']; return cities[Math.floor(Math.random() * cities.length)]; } }, }); ``` ```tsx filename="AI SDK 5.0" import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls, } from 'ai'; const { messages, sendMessage, addToolResult } = useChat({ // Automatic submission with helper sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, async onToolCall({ toolCall }) { if (toolCall.toolName === 'getLocation') { const cities = ['New York', 'Los Angeles', 'Chicago', 'San Francisco']; // Important: Don't await inside onToolCall to avoid deadlocks addToolResult({ tool: 'getLocation', toolCallId: toolCall.toolCallId, output: cities[Math.floor(Math.random() * cities.length)], }); } }, }); ``` #### Loading State Changes The deprecated `isLoading` helper has been removed in favor of `status`. ```tsx filename="AI SDK 4.0" const { isLoading } = useChat(); ``` ```tsx filename="AI SDK 5.0" const { status } = useChat(); // Use state instead of isLoading for more granular control ``` #### Resume Stream Support The resume functionality has been moved from `experimental_resume` to `resumeStream`. ```tsx filename="AI SDK 4.0" // Resume was experimental const { messages } = useChat({ experimental_resume: true, }); ``` ```tsx filename="AI SDK 5.0" const { messages } = useChat({ resumeStream: true, // Resume interrupted streams }); ``` #### Dynamic Body Values In v4, the `body` option in useChat configuration would dynamically update with component state changes. In v5, the `body` value is only captured at the first render and remains static throughout the component lifecycle. ```tsx filename="AI SDK 4.0" const [temperature, setTemperature] = useState(0.7); const { messages } = useChat({ api: '/api/chat', body: { temperature, // This would update dynamically in v4 }, }); ``` ```tsx filename="AI SDK 5.0" const [temperature, setTemperature] = useState(0.7); // Option 1: Use request-level configuration (Recommended) const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat' }), }); // Pass dynamic values at request time sendMessage( { text: input }, { body: { temperature, // Current temperature value at request time }, }, ); // Option 2: Use function configuration with useRef const temperatureRef = useRef(temperature); temperatureRef.current = temperature; const { messages } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', body: () => ({ temperature: temperatureRef.current, }), }), }); ``` For more details on request configuration, see the [Chatbot guide](/docs/ai-sdk-ui/chatbot#request-configuration). ### `@ai-sdk/vue` Changes The Vue.js integration has been completely restructured, replacing the `useChat` composable with a `Chat` class. #### useChat Replaced with Chat Class ```typescript filename="@ai-sdk/vue v1" <script setup> import { useChat } from '@ai-sdk/vue'; const { messages, input, handleSubmit } = useChat({ api: '/api/chat', }); </script> ``` ```typescript filename="@ai-sdk/vue v2" <script setup> import { Chat } from '@ai-sdk/vue'; import { DefaultChatTransport } from 'ai'; import { ref } from 'vue'; const input = ref(''); const chat = new Chat({ transport: new DefaultChatTransport({ api: '/api/chat' }), }); const handleSubmit = (e: Event) => { e.preventDefault(); chat.sendMessage({ text: input.value }); input.value = ''; }; </script> ``` #### Message Structure Changes Messages now use a `parts` array instead of a `content` string. ```typescript filename="@ai-sdk/vue v1" <template> <div v-for="message in messages" :key="message.id"> <div>{{ message.role }}: {{ message.content }}</div> </div> </template> ``` ```typescript filename="@ai-sdk/vue v2" <template> <div v-for="message in chat.messages" :key="message.id"> <div>{{ message.role }}:</div> <div v-for="part in message.parts" :key="part.type"> <span v-if="part.type === 'text'">{{ part.text }}</span> </div> </div> </template> ``` ### `@ai-sdk/svelte` Changes The Svelte integration has also been updated with new constructor patterns and readonly properties. #### Constructor API Changes ```js filename="@ai-sdk/svelte v1" import { Chat } from '@ai-sdk/svelte'; const chatInstance = Chat({ api: '/api/chat', }); ``` ```js filename="@ai-sdk/svelte v2" import { Chat } from '@ai-sdk/svelte'; import { DefaultChatTransport } from 'ai'; const chatInstance = Chat(() => ({ transport: new DefaultChatTransport({ api: '/api/chat' }), })); ``` ##### Properties Made Readonly Properties are now readonly and must be updated using setter methods. ```js filename="@ai-sdk/svelte v1" // Direct property mutation was allowed chatInstance.messages = [...chatInstance.messages, newMessage]; ``` ```js filename="@ai-sdk/svelte v2" // Must use setter methods chatInstance.setMessages([...chatInstance.messages, newMessage]); ``` ##### Removed Managed Input Like React and Vue, input management has been removed from the Svelte integration. ```js filename="@ai-sdk/svelte v1" // Input was managed internally const { messages, input, handleSubmit } = chatInstance; ``` ```js filename="@ai-sdk/svelte v2" // Must manage input state manually let input = ''; const { messages, sendMessage } = chatInstance; const handleSubmit = () => { sendMessage({ text: input }); input = ''; }; ``` #### `@ai-sdk/ui-utils` Package Removal The `@ai-sdk/ui-utils` package has been removed and its exports moved to the main `ai` package. ```tsx filename="AI SDK 4.0" import { getTextFromDataUrl } from '@ai-sdk/ui-utils'; ``` ```tsx filename="AI SDK 5.0" import { getTextFromDataUrl } from 'ai'; ``` ### useCompletion Changes The `data` property has been removed from the `useCompletion` hook. ```tsx filename="AI SDK 4.0" const { completion, handleSubmit, data, // No longer available } = useCompletion(); ``` ```tsx filename="AI SDK 5.0" const { completion, handleSubmit, // data property removed entirely } = useCompletion(); ``` ### useAssistant Removal The `useAssistant` hook has been removed. ```tsx filename="AI SDK 4.0" import { useAssistant } from '@ai-sdk/react'; ``` ```tsx filename="AI SDK 5.0" // useAssistant has been removed // Use useChat with appropriate configuration instead ``` For an implementation of the assistant functionality with AI SDK v5, see this [example repository](https://github.com/vercel-labs/ai-sdk-openai-assistants-api). #### Attachments → File Parts The `experimental_attachments` property has been replaced with the parts array. ```tsx filename="AI SDK 4.0" { messages.map(message => ( <div className="flex flex-col gap-2"> {message.content} <div className="flex flex-row gap-2"> {message.experimental_attachments?.map((attachment, index) => attachment.contentType?.includes('image/') ? ( <img src={attachment.url} alt={attachment.name} /> ) : attachment.contentType?.includes('text/') ? ( <div className="w-32 h-24 p-2 overflow-hidden text-xs border rounded-md ellipsis text-zinc-500"> {getTextFromDataUrl(attachment.url)} </div> ) : null, )} </div> </div> )); } ``` ```tsx filename="AI SDK 5.0" { messages.map(message => ( <div> {message.parts.map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } if (part.type === 'file' && part.mediaType?.startsWith('image/')) { return ( <div key={index}> <img src={part.url} /> </div> ); } })} </div> )); } ``` ### Embedding Changes #### Provider Options for Embeddings Embedding model settings now use provider options instead of model parameters. ```tsx filename="AI SDK 4.0" const { embedding } = await embed({ model: openai('text-embedding-3-small', { dimensions: 10, }), }); ``` ```tsx filename="AI SDK 5.0" const { embedding } = await embed({ model: openai('text-embedding-3-small'), providerOptions: { openai: { dimensions: 10, }, }, }); ``` #### Raw Response → Response The `rawResponse` property has been renamed to `response`. ```tsx filename="AI SDK 4.0" const { rawResponse } = await embed(/* */); ``` ```tsx filename="AI SDK 5.0" const { response } = await embed(/* */); ``` #### Parallel Requests in embedMany `embedMany` now makes parallel requests with a configurable `maxParallelCalls` option. ```tsx filename="AI SDK 5.0" const { embeddings, usage } = await embedMany({ maxParallelCalls: 2, // Limit parallel requests model: openai.textEmbeddingModel('text-embedding-3-small'), values: [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ], }); ``` #### LangChain Adapter Moved to `@ai-sdk/langchain` The `LangChainAdapter` has been moved to `@ai-sdk/langchain` and the API has been updated to use UI message streams. ```tsx filename="AI SDK 4.0" import { LangChainAdapter } from 'ai'; const response = LangChainAdapter.toDataStreamResponse(stream); ``` ```tsx filename="AI SDK 5.0" import { toUIMessageStream } from '@ai-sdk/langchain'; import { createUIMessageStreamResponse } from 'ai'; const response = createUIMessageStreamResponse({ stream: toUIMessageStream(stream), }); ``` <Note> Don't forget to install the new package: `npm install @ai-sdk/langchain` </Note> #### LlamaIndex Adapter Moved to `@ai-sdk/llamaindex` The `LlamaIndexAdapter` has been extracted to a separate package `@ai-sdk/llamaindex` and follows the same UI message stream pattern. ```tsx filename="AI SDK 4.0" import { LlamaIndexAdapter } from 'ai'; const response = LlamaIndexAdapter.toDataStreamResponse(stream); ``` ```tsx filename="AI SDK 5.0" import { toUIMessageStream } from '@ai-sdk/llamaindex'; import { createUIMessageStreamResponse } from 'ai'; const response = createUIMessageStreamResponse({ stream: toUIMessageStream(stream), }); ``` <Note> Don't forget to install the new package: `npm install @ai-sdk/llamaindex` </Note> ## Streaming Architecture The streaming architecture has been completely redesigned in v5 to support better content differentiation, concurrent streaming of multiple parts, and improved real-time UX. ### Stream Protocol Changes #### Stream Protocol: Single Chunks → Start/Delta/End Pattern The fundamental streaming pattern has changed from single chunks to a three-phase pattern with unique IDs for each content block. ```tsx filename="AI SDK 4.0" for await (const chunk of result.fullStream) { switch (chunk.type) { case 'text-delta': { process.stdout.write(chunk.textDelta); break; } } } ``` ```tsx filename="AI SDK 5.0" for await (const chunk of result.fullStream) { switch (chunk.type) { case 'text-start': { // New: Initialize a text block with unique ID console.log(`Starting text block: ${chunk.id}`); break; } case 'text-delta': { // Changed: Now includes ID and uses 'delta' property process.stdout.write(chunk.delta); // Changed from 'textDelta' break; } case 'text-end': { // New: Finalize the text block console.log(`Completed text block: ${chunk.id}`); break; } } } ``` #### Reasoning Streaming Pattern Reasoning content now follows the same start/delta/end pattern: ```tsx filename="AI SDK 4.0" for await (const chunk of result.fullStream) { switch (chunk.type) { case 'reasoning': { // Single chunk with full reasoning text console.log('Reasoning:', chunk.text); break; } } } ``` ```tsx filename="AI SDK 5.0" for await (const chunk of result.fullStream) { switch (chunk.type) { case 'reasoning-start': { console.log(`Starting reasoning block: ${chunk.id}`); break; } case 'reasoning-delta': { process.stdout.write(chunk.delta); break; } case 'reasoning-end': { console.log(`Completed reasoning block: ${chunk.id}`); break; } } } ``` #### Tool Input Streaming Tool inputs can now be streamed as they're being generated: ```tsx filename="AI SDK 5.0" for await (const chunk of result.fullStream) { switch (chunk.type) { case 'tool-input-start': { console.log(`Starting tool input for ${chunk.toolName}: ${chunk.id}`); break; } case 'tool-input-delta': { // Stream the JSON input as it's being generated process.stdout.write(chunk.delta); break; } case 'tool-input-end': { console.log(`Completed tool input: ${chunk.id}`); break; } case 'tool-call': { // Final tool call with complete input console.log('Tool call:', chunk.toolName, chunk.input); break; } } } ``` #### onChunk Callback Changes The `onChunk` callback now receives the new streaming chunk types with IDs and the start/delta/end pattern. ```tsx filename="AI SDK 4.0" const result = streamText({ model: openai('gpt-4.1'), prompt: 'Write a story', onChunk({ chunk }) { switch (chunk.type) { case 'text-delta': { // Single property with text content console.log('Text delta:', chunk.textDelta); break; } } }, }); ``` ```tsx filename="AI SDK 5.0" const result = streamText({ model: openai('gpt-4.1'), prompt: 'Write a story', onChunk({ chunk }) { switch (chunk.type) { case 'text': { // Text chunks now use single 'text' type console.log('Text chunk:', chunk.text); break; } case 'reasoning': { // Reasoning chunks use single 'reasoning' type console.log('Reasoning chunk:', chunk.text); break; } case 'source': { console.log('Source chunk:', chunk); break; } case 'tool-call': { console.log('Tool call:', chunk.toolName, chunk.input); break; } case 'tool-input-start': { console.log( `Tool input started for ${chunk.toolName}:`, chunk.toolCallId, ); break; } case 'tool-input-delta': { console.log(`Tool input delta for ${chunk.toolCallId}:`, chunk.delta); break; } case 'tool-result': { console.log('Tool result:', chunk.output); break; } case 'raw': { console.log('Raw chunk:', chunk); break; } } }, }); ``` #### File Stream Parts Restructure File parts in streams have been flattened. ```tsx filename="AI SDK 4.0" for await (const chunk of result.fullStream) { switch (chunk.type) { case 'file': { console.log('Media type:', chunk.file.mediaType); console.log('File data:', chunk.file.data); break; } } } ``` ```tsx filename="AI SDK 5.0" for await (const chunk of result.fullStream) { switch (chunk.type) { case 'file': { console.log('Media type:', chunk.mediaType); console.log('File data:', chunk.data); break; } } } ``` #### Source Stream Parts Restructure Source stream parts have been flattened. ```tsx filename="AI SDK 4.0" for await (const part of result.fullStream) { if (part.type === 'source' && part.source.sourceType === 'url') { console.log('ID:', part.source.id); console.log('Title:', part.source.title); console.log('URL:', part.source.url); } } ``` ```tsx filename="AI SDK 5.0" for await (const part of result.fullStream) { if (part.type === 'source' && part.sourceType === 'url') { console.log('ID:', part.id); console.log('Title:', part.title); console.log('URL:', part.url); } } ``` #### Finish Event Changes Stream finish events have been renamed for consistency. ```tsx filename="AI SDK 4.0" for await (const part of result.fullStream) { switch (part.type) { case 'step-finish': { console.log('Step finished:', part.finishReason); break; } case 'finish': { console.log('Usage:', part.usage); break; } } } ``` ```tsx filename="AI SDK 5.0" for await (const part of result.fullStream) { switch (part.type) { case 'finish-step': { // Renamed from 'step-finish' console.log('Step finished:', part.finishReason); break; } case 'finish': { console.log('Total Usage:', part.totalUsage); // Changed from 'usage' break; } } } ``` ### Stream Protocol Changes #### Proprietary Protocol -> Server-Sent Events The data stream protocol has been updated to use Server-Sent Events. ```tsx filename="AI SDK 4.0" import { createDataStream, formatDataStreamPart } from 'ai'; const dataStream = createDataStream({ execute: writer => { writer.writeData('initialized call'); writer.write(formatDataStreamPart('text', 'Hello')); writer.writeSource({ type: 'source', sourceType: 'url', id: 'source-1', url: 'https://example.com', title: 'Example Source', }); }, }); ``` ```tsx filename="AI SDK 5.0" import { createUIMessageStream } from 'ai'; const stream = createUIMessageStream({ execute: ({ writer }) => { writer.write({ type: 'data', value: ['initialized call'] }); writer.write({ type: 'text', value: 'Hello' }); writer.write({ type: 'source-url', value: { type: 'source', id: 'source-1', url: 'https://example.com', title: 'Example Source', }, }); }, }); ``` #### Data Stream Response Helper Functions Renamed The streaming API has been completely restructured from data streams to UI message streams. ```tsx filename="AI SDK 4.0" // Express/Node.js servers app.post('/stream', async (req, res) => { const result = streamText({ model: openai('gpt-4.1'), prompt: 'Generate content', }); result.pipeDataStreamToResponse(res); }); // Next.js API routes const result = streamText({ model: openai('gpt-4.1'), prompt: 'Generate content', }); return result.toDataStreamResponse(); ``` ```tsx filename="AI SDK 5.0" // Express/Node.js servers app.post('/stream', async (req, res) => { const result = streamText({ model: openai('gpt-4.1'), prompt: 'Generate content', }); result.pipeUIMessageStreamToResponse(res); }); // Next.js API routes const result = streamText({ model: openai('gpt-4.1'), prompt: 'Generate content', }); return result.toUIMessageStreamResponse(); ``` #### Stream Transform Function Renaming Various stream-related functions have been renamed for consistency. ```tsx filename="AI SDK 4.0" import { DataStreamToSSETransformStream } from 'ai'; ``` ```tsx filename="AI SDK 5.0" import { JsonToSseTransformStream } from 'ai'; ``` #### Error Handling: getErrorMessage → onError The `getErrorMessage` option in `toDataStreamResponse` has been replaced with `onError` in `toUIMessageStreamResponse`, providing more control over error forwarding to the client. By default, error messages are NOT sent to the client to prevent leaking sensitive information. The `onError` callback allows you to explicitly control what error information is forwarded to the client. ```tsx filename="AI SDK 4.0" return result.toDataStreamResponse({ getErrorMessage: error => { // Return sanitized error data to send to client // Only return what you want the client to see! return { errorCode: 'STREAM_ERROR', message: 'An error occurred while processing your request', // In production, avoid sending error.message directly to prevent information leakage }; }, }); ``` ```tsx filename="AI SDK 5.0" return result.toUIMessageStreamResponse({ onError: error => { // Return sanitized error data to send to client // Only return what you want the client to see! return { errorCode: 'STREAM_ERROR', message: 'An error occurred while processing your request', // In production, avoid sending error.message directly to prevent information leakage }; }, }); ``` ### Utility Changes #### ID Generation Changes The `createIdGenerator()` function now requires a `size` argument. ```tsx filename="AI SDK 4.0" const generator = createIdGenerator({ prefix: 'msg' }); const id = generator(16); // Custom size at call time ``` ```tsx filename="AI SDK 5.0" const generator = createIdGenerator({ prefix: 'msg', size: 16 }); const id = generator(); // Fixed size from creation ``` #### IDGenerator → IdGenerator The type name has been updated. ```tsx filename="AI SDK 4.0" import { IDGenerator } from 'ai'; ``` ```tsx filename="AI SDK 5.0" import { IdGenerator } from 'ai'; ``` ### Provider Interface Changes #### Language Model V2 Import `LanguageModelV2` must now be imported from `@ai-sdk/provider`. ```tsx filename="AI SDK 4.0" import { LanguageModelV2 } from 'ai'; ``` ```tsx filename="AI SDK 5.0" import { LanguageModelV2 } from '@ai-sdk/provider'; ``` #### Middleware Rename `LanguageModelV1Middleware` has been renamed and moved. ```tsx filename="AI SDK 4.0" import { LanguageModelV1Middleware } from 'ai'; ``` ```tsx filename="AI SDK 5.0" import { LanguageModelV2Middleware } from '@ai-sdk/provider'; ``` #### Usage Token Properties Token usage properties have been renamed for consistency. ```tsx filename="AI SDK 4.0" // In language model implementations { usage: { promptTokens: 10, completionTokens: 20 } } ``` ```tsx filename="AI SDK 5.0" // In language model implementations { usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 // Now required } } ``` #### Stream Part Type Changes The `LanguageModelV2StreamPart` type has been expanded to support the new streaming architecture with start/delta/end patterns and IDs. ```tsx filename="AI SDK 4.0" // V4: Simple stream parts type LanguageModelV2StreamPart = | { type: 'text-delta'; textDelta: string } | { type: 'reasoning'; text: string } | { type: 'tool-call'; toolCallId: string; toolName: string; input: string }; ``` ```tsx filename="AI SDK 5.0" // V5: Enhanced stream parts with IDs and lifecycle events type LanguageModelV2StreamPart = // Text blocks with start/delta/end pattern | { type: 'text-start'; id: string; providerMetadata?: SharedV2ProviderMetadata; } | { type: 'text-delta'; id: string; delta: string; providerMetadata?: SharedV2ProviderMetadata; } | { type: 'text-end'; id: string; providerMetadata?: SharedV2ProviderMetadata; } // Reasoning blocks with start/delta/end pattern | { type: 'reasoning-start'; id: string; providerMetadata?: SharedV2ProviderMetadata; } | { type: 'reasoning-delta'; id: string; delta: string; providerMetadata?: SharedV2ProviderMetadata; } | { type: 'reasoning-end'; id: string; providerMetadata?: SharedV2ProviderMetadata; } // Tool input streaming | { type: 'tool-input-start'; id: string; toolName: string; providerMetadata?: SharedV2ProviderMetadata; } | { type: 'tool-input-delta'; id: string; delta: string; providerMetadata?: SharedV2ProviderMetadata; } | { type: 'tool-input-end'; id: string; providerMetadata?: SharedV2ProviderMetadata; } // Enhanced tool calls | { type: 'tool-call'; toolCallId: string; toolName: string; input: string; providerMetadata?: SharedV2ProviderMetadata; } // Stream lifecycle events | { type: 'stream-start'; warnings: Array<LanguageModelV2CallWarning> } | { type: 'finish'; usage: LanguageModelV2Usage; finishReason: LanguageModelV2FinishReason; providerMetadata?: SharedV2ProviderMetadata; }; ``` #### Raw Response → Response Provider response objects have been updated. ```tsx filename="AI SDK 4.0" // In language model implementations { rawResponse: { /* ... */ } } ``` ```tsx filename="AI SDK 5.0" // In language model implementations { response: { /* ... */ } } ``` #### `wrapLanguageModel` now stable ```tsx filename="AI SDK 4.0" import { experimental_wrapLanguageModel } from 'ai'; ``` ```tsx filename="AI SDK 5.0" import { wrapLanguageModel } from 'ai'; ``` #### `activeTools` No Longer Experimental ```tsx filename="AI SDK 4.0" const result = await generateText({ model: openai('gpt-4'), messages, tools: { weatherTool, locationTool }, experimental_activeTools: ['weatherTool'], }); ``` ```tsx filename="AI SDK 5.0" const result = await generateText({ model: openai('gpt-4'), messages, tools: { weatherTool, locationTool }, activeTools: ['weatherTool'], // No longer experimental }); ``` #### `prepareStep` No Longer Experimental The `experimental_prepareStep` option has been promoted and no longer requires the experimental prefix. ```tsx filename="AI SDK 4.0" const result = await generateText({ model: openai('gpt-4'), messages, tools: { weatherTool, locationTool }, experimental_prepareStep: ({ steps, stepNumber, model }) => { console.log('Preparing step:', stepNumber); return { activeTools: ['weatherTool'], system: 'Be helpful and concise.', }; }, }); ``` ```tsx filename="AI SDK 5.0" const result = await generateText({ model: openai('gpt-4'), messages, tools: { weatherTool, locationTool }, prepareStep: ({ steps, stepNumber, model }) => { console.log('Preparing step:', stepNumber); return { activeTools: ['weatherTool'], system: 'Be helpful and concise.', // Can also configure toolChoice, model, etc. }; }, }); ``` The `prepareStep` function receives `{ steps, stepNumber, model }` and can return: - `model`: Different model for this step - `activeTools`: Which tools to make available - `toolChoice`: Tool selection strategy - `system`: System message for this step - `undefined`: Use default settings ### Temperature Default Removal Temperature is no longer set to `0` by default. ```tsx filename="AI SDK 4.0" await generateText({ model: openai('gpt-4'), prompt: 'Write a creative story', // Implicitly temperature: 0 }); ``` ```tsx filename="AI SDK 5.0" await generateText({ model: openai('gpt-4'), prompt: 'Write a creative story', temperature: 0, // Must explicitly set }); ``` ## Message Persistence Changes In v4, you would typically use helper functions like `appendResponseMessages` or `appendClientMessage` to format messages in the `onFinish` callback of `streamText`: ```tsx filename="AI SDK 4.0" import { streamText, convertToModelMessages, appendClientMessage, appendResponseMessages, } from 'ai'; import { openai } from '@ai-sdk/openai'; const updatedMessages = appendClientMessage({ messages, message: lastUserMessage, }); const result = streamText({ model: openai('gpt-4o'), messages: updatedMessages, experimental_generateMessageId: () => generateId(), // ID generation on streamText onFinish: async ({ responseMessages, usage }) => { // Use helper functions to format messages const finalMessages = appendResponseMessages({ messages: updatedMessages, responseMessages, }); // Save formatted messages to database await saveMessages(finalMessages); }, }); ``` In v5, message persistence is now handled through the `toUIMessageStreamResponse` method, which automatically formats response messages in the `UIMessage` format: ```tsx filename="AI SDK 5.0" import { streamText, convertToModelMessages, UIMessage } from 'ai'; import { openai } from '@ai-sdk/openai'; const messages: UIMessage[] = [ // Your existing messages in UIMessage format ]; const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), // experimental_generateMessageId removed from here }); return result.toUIMessageStreamResponse({ originalMessages: messages, // Pass original messages for context generateMessageId: () => generateId(), // ID generation moved here for UI messages onFinish: ({ messages, responseMessage }) => { // messages contains all messages (original + response) in UIMessage format saveChat({ chatId, messages }); // responseMessage contains just the generated message in UIMessage format saveMessage({ chatId, message: responseMessage }); }, }); ``` ### Message ID Generation The `experimental_generateMessageId` option has been moved from `streamText` configuration to `toUIMessageStreamResponse`, as it's designed for use with `UIMessage`s rather than `ModelMessage`s. ```tsx filename="AI SDK 4.0" const result = streamText({ model: openai('gpt-4o'), messages, experimental_generateMessageId: () => generateId(), }); ``` ```tsx filename="AI SDK 5.0" const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ generateMessageId: () => generateId(), // No longer experimental // ... }); ``` For more details on message IDs and persistence, see the [Chatbot Message Persistence guide](/docs/ai-sdk-ui/chatbot-message-persistence#message-ids). ### Using createUIMessageStream For more complex scenarios, especially when working with data parts, you can use `createUIMessageStream`: ```tsx filename="AI SDK 5.0 - Advanced" import { createUIMessageStream, createUIMessageStreamResponse, streamText, convertToModelMessages, UIMessage, } from 'ai'; import { openai } from '@ai-sdk/openai'; const stream = createUIMessageStream({ originalMessages: messages, execute: ({ writer }) => { // Write custom data parts writer.write({ type: 'data', data: { status: 'processing', timestamp: Date.now() }, }); // Stream the AI response const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), }); writer.merge(result.toUIMessageStream()); }, onFinish: ({ messages }) => { // messages contains all messages (original + response + data parts) in UIMessage format saveChat({ chatId, messages }); }, }); return createUIMessageStreamResponse({ stream }); ``` ## Provider & Model Changes ### OpenAI #### Structured Outputs Default Structured outputs are now enabled by default for supported OpenAI models. ```tsx filename="AI SDK 4.0" const result = await generateText({ model: openai('gpt-4.1-2024-08-06', { structuredOutputs: true }), }); ``` ```tsx filename="AI SDK 5.0" const result = await generateText({ model: openai('gpt-4.1-2024-08-06'), // structuredOutputs: true is now the default }); ``` #### Compatibility Option Removal The `compatibility` option has been removed; strict mode is now the default. ```tsx filename="AI SDK 4.0" const openai = createOpenAI({ compatibility: 'strict', }); ``` ```tsx filename="AI SDK 5.0" const openai = createOpenAI({ // strict compatibility is now the default }); ``` #### Legacy Function Calls Removal The `useLegacyFunctionCalls` option has been removed. ```tsx filename="AI SDK 4.0" const result = streamText({ model: openai('gpt-4.1', { useLegacyFunctionCalls: true }), }); ``` ```tsx filename="AI SDK 5.0" const result = streamText({ model: openai('gpt-4.1'), }); ``` #### Simulate Streaming The `simulateStreaming` model option has been replaced with middleware. ```tsx filename="AI SDK 4.0" const result = generateText({ model: openai('gpt-4.1', { simulateStreaming: true }), prompt: 'Hello, world!', }); ``` ```tsx filename="AI SDK 5.0" import { simulateStreamingMiddleware, wrapLanguageModel } from 'ai'; const model = wrapLanguageModel({ model: openai('gpt-4.1'), middleware: simulateStreamingMiddleware(), }); const result = generateText({ model, prompt: 'Hello, world!', }); ``` ### Google #### Search Grounding is now a provider defined tool Search Grounding is now called "Google Search" and is now a provider defined tool. ```tsx filename="AI SDK 4.0" const { text, providerMetadata } = await generateText({ model: google('gemini-1.5-pro', { useSearchGrounding: true, }), prompt: 'List the top 5 San Francisco news from the past week.', }); ``` ```tsx filename="AI SDK 5.0" import { google } from '@ai-sdk/google'; const { text, sources, providerMetadata } = await generateText({ model: google('gemini-1.5-pro'), prompt: 'List the top 5 San Francisco news from the past week.' tools: { google_search: google.tools.googleSearch({}), }, }); ``` ### Amazon Bedrock #### Snake Case → Camel Case Provider options have been updated to use camelCase. ```tsx filename="AI SDK 4.0" const result = await generateText({ model: bedrock('amazon.titan-tg1-large'), prompt: 'Hello, world!', providerOptions: { bedrock: { reasoning_config: { /* ... */ }, }, }, }); ``` ```tsx filename="AI SDK 5.0" const result = await generateText({ model: bedrock('amazon.titan-tg1-large'), prompt: 'Hello, world!', providerOptions: { bedrock: { reasoningConfig: { /* ... */ }, }, }, }); ``` ### Provider-Utils Changes Deprecated `CoreTool*` types have been removed. ```tsx filename="AI SDK 4.0" import { CoreToolCall, CoreToolResult, CoreToolResultUnion, CoreToolCallUnion, CoreToolChoice, } from '@ai-sdk/provider-utils'; ``` ```tsx filename="AI SDK 5.0" import { ToolCall, ToolResult, TypedToolResult, TypedToolCall, ToolChoice, } from '@ai-sdk/provider-utils'; ``` ## Codemod Table The following table lists available codemods for the AI SDK 5.0 upgrade process. For more information, see the [Codemods](#codemods) section. | Change | Codemod | | ------------------------------------------------ | ----------------------------------------------------- | | **AI SDK Core Changes** | | | Flatten streamText file properties | `v5/flatten-streamtext-file-properties` | | ID Generation Changes | `v5/require-createIdGenerator-size-argument` | | IDGenerator → IdGenerator | `v5/rename-IDGenerator-to-IdGenerator` | | Import LanguageModelV2 from provider package | `v5/import-LanguageModelV2-from-provider-package` | | Migrate to data stream protocol v2 | `v5/migrate-to-data-stream-protocol-v2` | | Move image model maxImagesPerCall | `v5/move-image-model-maxImagesPerCall` | | Move LangChain adapter | `v5/move-langchain-adapter` | | Move provider options | `v5/move-provider-options` | | Move React to AI SDK | `v5/move-react-to-ai-sdk` | | Move UI utils to AI | `v5/move-ui-utils-to-ai` | | Remove experimental wrap language model | `v5/remove-experimental-wrap-language-model` | | Remove experimental activeTools | `v5/remove-experimental-activetools` | | Remove experimental prepareStep | `v5/remove-experimental-preparestep` | | Remove experimental continueSteps | `v5/remove-experimental-continuesteps` | | Remove experimental temperature | `v5/remove-experimental-temperature` | | Remove experimental truncate | `v5/remove-experimental-truncate` | | Remove experimental OpenAI compatibility | `v5/remove-experimental-openai-compatibility` | | Remove experimental OpenAI legacy function calls | `v5/remove-experimental-openai-legacy-function-calls` | | Remove experimental OpenAI structured outputs | `v5/remove-experimental-openai-structured-outputs` | | Remove experimental OpenAI store | `v5/remove-experimental-openai-store` | | Remove experimental OpenAI user | `v5/remove-experimental-openai-user` | | Remove experimental OpenAI parallel tool calls | `v5/remove-experimental-openai-parallel-tool-calls` | | Remove experimental OpenAI response format | `v5/remove-experimental-openai-response-format` | | Remove experimental OpenAI logit bias | `v5/remove-experimental-openai-logit-bias` | | Remove experimental OpenAI logprobs | `v5/remove-experimental-openai-logprobs` | | Remove experimental OpenAI seed | `v5/remove-experimental-openai-seed` | | Remove experimental OpenAI service tier | `v5/remove-experimental-openai-service-tier` | | Remove experimental OpenAI top logprobs | `v5/remove-experimental-openai-top-logprobs` | | Remove experimental OpenAI transform | `v5/remove-experimental-openai-transform` | | Remove experimental OpenAI stream options | `v5/remove-experimental-openai-stream-options` | | Remove experimental OpenAI prediction | `v5/remove-experimental-openai-prediction` | | Remove experimental Anthropic caching | `v5/remove-experimental-anthropic-caching` | | Remove experimental Anthropic computer use | `v5/remove-experimental-anthropic-computer-use` | | Remove experimental Anthropic PDF support | `v5/remove-experimental-anthropic-pdf-support` | | Remove experimental Anthropic prompt caching | `v5/remove-experimental-anthropic-prompt-caching` | | Remove experimental Google search grounding | `v5/remove-experimental-google-search-grounding` | | Remove experimental Google code execution | `v5/remove-experimental-google-code-execution` | | Remove experimental Google cached content | `v5/remove-experimental-google-cached-content` | | Remove experimental Google custom headers | `v5/remove-experimental-google-custom-headers` | | Rename format stream part | `v5/rename-format-stream-part` | | Rename parse stream part | `v5/rename-parse-stream-part` | | Replace image type with file type | `v5/replace-image-type-with-file-type` | | Replace LlamaIndex adapter | `v5/replace-llamaindex-adapter` | | Replace onCompletion with onFinal | `v5/replace-oncompletion-with-onfinal` | | Replace provider metadata with provider options | `v5/replace-provider-metadata-with-provider-options` | | Replace rawResponse with response | `v5/replace-rawresponse-with-response` | | Replace redacted reasoning type | `v5/replace-redacted-reasoning-type` | | Replace simulate streaming | `v5/replace-simulate-streaming` | | Replace textDelta with text | `v5/replace-textdelta-with-text` | | Replace usage token properties | `v5/replace-usage-token-properties` | | Restructure file stream parts | `v5/restructure-file-stream-parts` | | Restructure source stream parts | `v5/restructure-source-stream-parts` | | RSC package | `v5/rsc-package` | ## Changes Between v5 Beta Versions This section documents breaking changes between different beta versions of AI SDK 5.0. If you're upgrading from an earlier v5 beta version to a later one, check this section for any changes that might affect your code. ### fullStream Type Rename: text/reasoning → text-delta/reasoning-delta The chunk types in `fullStream` have been renamed for consistency with UI streams and language model streams. ```tsx filename="AI SDK 5.0 (before beta.26)" for await (const chunk of result.fullStream) { switch (chunk.type) { case 'text': { process.stdout.write(chunk.text); break; } case 'reasoning': { console.log('Reasoning:', chunk.text); break; } } } ``` ```tsx filename="AI SDK 5.0 (beta.26 and later)" for await (const chunk of result.fullStream) { switch (chunk.type) { case 'text-delta': { process.stdout.write(chunk.text); break; } case 'reasoning-delta': { console.log('Reasoning:', chunk.text); break; } } } ``` --- File: /ai/content/docs/08-migration-guides/27-migration-guide-4-2.mdx --- --- title: Migrate AI SDK 4.1 to 4.2 description: Learn how to upgrade AI SDK 4.1 to 4.2. --- # Migrate AI SDK 4.1 to 4.2 <Note> Check out the [AI SDK 4.2 release blog post](https://vercel.com/blog/ai-sdk-4-2) for more information about the release. </Note> This guide will help you upgrade to AI SDK 4.2: ## Stable APIs The following APIs have been moved to stable and no longer have the `experimental_` prefix: - `customProvider` - `providerOptions` (renamed from `providerMetadata` for provider-specific inputs) - `providerMetadata` (for provider-specific outputs) - `toolCallStreaming` option for `streamText` ## Dependency Versions AI SDK requires a non-optional `zod` dependency with version `^3.23.8`. ## UI Message Parts In AI SDK 4.2, we've redesigned how `useChat` handles model outputs with message parts and multiple steps. This is a significant improvement that simplifies rendering complex, multi-modal AI responses in your UI. ### What's Changed Assistant messages with tool calling now get combined into a single message with multiple parts, rather than creating separate messages for each step. This change addresses two key developments in AI applications: 1. **Diverse Output Types**: Models now generate more than just text; they produce reasoning steps, sources, and tool calls. 2. **Interleaved Outputs**: In multi-step agent use-cases, these different output types are frequently interleaved. ### Benefits of the New Approach Previously, `useChat` stored different output types separately, which made it challenging to maintain the correct sequence in your UI when these elements were interleaved in a response, and led to multiple consecutive assistant messages when there were tool calls. For example: ```javascript message.content = "Final answer: 42"; message.reasoning = "First I'll calculate X, then Y..."; message.toolInvocations = [{toolName: "calculator", args: {...}}]; ``` This structure was limiting. The new message parts approach replaces separate properties with an ordered array that preserves the exact sequence: ```javascript message.parts = [ { type: "text", text: "Final answer: 42" }, { type: "reasoning", reasoning: "First I'll calculate X, then Y..." }, { type: "tool-invocation", toolInvocation: { toolName: "calculator", args: {...} } }, ]; ``` ### Migration Existing applications using the previous message format will need to update their UI components to handle the new `parts` array. The fields from the previous format are still available for backward compatibility, but we recommend migrating to the new format for better support of multi-modal and multi-step interactions. You can use the `useChat` hook with the new message parts as follows: ```javascript function Chat() { const { messages } = useChat(); return ( <div> {messages.map(message => message.parts.map((part, i) => { switch (part.type) { case 'text': return <p key={i}>{part.text}</p>; case 'source': return <p key={i}>{part.source.url}</p>; case 'reasoning': return <div key={i}>{part.reasoning}</div>; case 'tool-invocation': return <div key={i}>{part.toolInvocation.toolName}</div>; case 'file': return ( <img key={i} src={`data:${part.mediaType};base64,${part.data}`} /> ); } }), )} </div> ); } ``` --- File: /ai/content/docs/08-migration-guides/28-migration-guide-4-1.mdx --- --- title: Migrate AI SDK 4.0 to 4.1 description: Learn how to upgrade AI SDK 4.0 to 4.1. --- # Migrate AI SDK 4.0 to 4.1 <Note> Check out the [AI SDK 4.1 release blog post](https://vercel.com/blog/ai-sdk-4-1) for more information about the release. </Note> No breaking changes in this release. --- File: /ai/content/docs/08-migration-guides/29-migration-guide-4-0.mdx --- --- title: Migrate AI SDK 3.4 to 4.0 description: Learn how to upgrade AI SDK 3.4 to 4.0. --- # Migrate AI SDK 3.4 to 4.0 <Note> Check out the [AI SDK 4.0 release blog post](https://vercel.com/blog/ai-sdk-4-0) for more information about the release. </Note> ## Recommended Migration Process 1. Backup your project. If you use a versioning control system, make sure all previous versions are committed. 1. [Migrate to AI SDK 3.4](/docs/troubleshooting/migration-guide/migration-guide-3-4). 1. Upgrade to AI SDK 4.0. 1. Automatically migrate your code using [codemods](#codemods). > If you don't want to use codemods, we recommend resolving all deprecation warnings before upgrading to AI SDK 4.0. 1. Follow the breaking changes guide below. 1. Verify your project is working as expected. 1. Commit your changes. ## AI SDK 4.0 package versions You need to update the following packages to the following versions in your `package.json` file(s): - `ai` package: `4.0.*` - `ai-sdk@provider-utils` package: `2.0.*` - `ai-sdk/*` packages: `1.0.*` (other `@ai-sdk` packages) ## Codemods The AI SDK provides Codemod transformations to help upgrade your codebase when a feature is deprecated, removed, or otherwise changed. Codemods are transformations that run on your codebase programmatically. They allow you to easily apply many changes without having to manually go through every file. <Note> Codemods are intended as a tool to help you with the upgrade process. They may not cover all of the changes you need to make. You may need to make additional changes manually. </Note> You can run all codemods provided as part of the 4.0 upgrade process by running the following command from the root of your project: ```sh npx @ai-sdk/codemod upgrade ``` To run only the v4 codemods: ```sh npx @ai-sdk/codemod v4 ``` Individual codemods can be run by specifying the name of the codemod: ```sh npx @ai-sdk/codemod <codemod-name> <path> ``` For example, to run a specific v4 codemod: ```sh npx @ai-sdk/codemod v4/replace-baseurl src/ ``` See also the [table of codemods](#codemod-table). In addition, the latest set of codemods can be found in the [`@ai-sdk/codemod`](https://github.com/vercel/ai/tree/main/packages/codemod/src/codemods) repository. ## Provider Changes ### Removed `baseUrl` option The `baseUrl` option has been removed from all providers. Please use the `baseURL` option instead. ```ts filename="AI SDK 3.4" const perplexity = createOpenAI({ // ... baseUrl: 'https://api.perplexity.ai/', }); ``` ```ts filename="AI SDK 4.0" const perplexity = createOpenAI({ // ... baseURL: 'https://api.perplexity.ai/', }); ``` ### Anthropic Provider #### Removed `Anthropic` facade The `Anthropic` facade has been removed from the Anthropic provider. Please use the `anthropic` object or the `createAnthropic` function instead. ```ts filename="AI SDK 3.4" const anthropic = new Anthropic({ // ... }); ``` ```ts filename="AI SDK 4.0" const anthropic = createAnthropic({ // ... }); ``` #### Removed `topK` setting <Note type="warning"> There is no codemod available for this change. Please review and update your code manually. </Note> The model specific `topK` setting has been removed from the Anthropic provider. You can use the standard `topK` setting instead. ```ts filename="AI SDK 3.4" const result = await generateText({ model: anthropic('claude-3-5-sonnet-latest', { topK: 0.5, }), }); ``` ```ts filename="AI SDK 4.0" const result = await generateText({ model: anthropic('claude-3-5-sonnet-latest'), topK: 0.5, }); ``` ### Google Generative AI Provider #### Removed `Google` facade The `Google` facade has been removed from the Google Generative AI provider. Please use the `google` object or the `createGoogleGenerativeAI` function instead. ```ts filename="AI SDK 3.4" const google = new Google({ // ... }); ``` ```ts filename="AI SDK 4.0" const google = createGoogleGenerativeAI({ // ... }); ``` #### Removed `topK` setting <Note type="warning"> There is no codemod available for this change. Please review and update your code manually. </Note> The model-specific `topK` setting has been removed from the Google Generative AI provider. You can use the standard `topK` setting instead. ```ts filename="AI SDK 3.4" const result = await generateText({ model: google('gemini-1.5-flash', { topK: 0.5, }), }); ``` ```ts filename="AI SDK 4.0" const result = await generateText({ model: google('gemini-1.5-flash'), topK: 0.5, }); ``` ### Google Vertex Provider #### Removed `topK` setting <Note type="warning"> There is no codemod available for this change. Please review and update your code manually. </Note> The model-specific `topK` setting has been removed from the Google Vertex provider. You can use the standard `topK` setting instead. ```ts filename="AI SDK 3.4" const result = await generateText({ model: vertex('gemini-1.5-flash', { topK: 0.5, }), }); ``` ```ts filename="AI SDK 4.0" const result = await generateText({ model: vertex('gemini-1.5-flash'), topK: 0.5, }); ``` ### Mistral Provider #### Removed `Mistral` facade The `Mistral` facade has been removed from the Mistral provider. Please use the `mistral` object or the `createMistral` function instead. ```ts filename="AI SDK 3.4" const mistral = new Mistral({ // ... }); ``` ```ts filename="AI SDK 4.0" const mistral = createMistral({ // ... }); ``` ### OpenAI Provider #### Removed `OpenAI` facade The `OpenAI` facade has been removed from the OpenAI provider. Please use the `openai` object or the `createOpenAI` function instead. ```ts filename="AI SDK 3.4" const openai = new OpenAI({ // ... }); ``` ```ts filename="AI SDK 4.0" const openai = createOpenAI({ // ... }); ``` ### LangChain Adapter #### Removed `toAIStream` The `toAIStream` function has been removed from the LangChain adapter. Please use the `toDataStream` function instead. ```ts filename="AI SDK 3.4" LangChainAdapter.toAIStream(stream); ``` ```ts filename="AI SDK 4.0" LangChainAdapter.toDataStream(stream); ``` ## AI SDK Core Changes ### `streamText` returns immediately Instead of returning a Promise, the `streamText` function now returns immediately. It is not necessary to await the result of `streamText`. ```ts filename="AI SDK 3.4" const result = await streamText({ // ... }); ``` ```ts filename="AI SDK 4.0" const result = streamText({ // ... }); ``` ### `streamObject` returns immediately Instead of returning a Promise, the `streamObject` function now returns immediately. It is not necessary to await the result of `streamObject`. ```ts filename="AI SDK 3.4" const result = await streamObject({ // ... }); ``` ```ts filename="AI SDK 4.0" const result = streamObject({ // ... }); ``` ### Remove roundtrips The `maxToolRoundtrips` and `maxAutomaticRoundtrips` options have been removed from the `generateText` and `streamText` functions. Please use the `maxSteps` option instead. The `roundtrips` property has been removed from the `GenerateTextResult` type. Please use the `steps` property instead. ```ts filename="AI SDK 3.4" const { text, roundtrips } = await generateText({ maxToolRoundtrips: 1, // or maxAutomaticRoundtrips // ... }); ``` ```ts filename="AI SDK 4.0" const { text, steps } = await generateText({ maxSteps: 2, // ... }); ``` ### Removed `nanoid` export The `nanoid` export has been removed. Please use [`generateId`](/docs/reference/ai-sdk-core/generate-id) instead. ```ts filename="AI SDK 3.4" import { nanoid } from 'ai'; ``` ```ts filename="AI SDK 4.0" import { generateId } from 'ai'; ``` ### Increased default size of generated IDs <Note type="warning"> There is no codemod available for this change. Please review and update your code manually. </Note> The [`generateId`](/docs/reference/ai-sdk-core/generate-id) function now generates 16-character IDs. The previous default was 7 characters. This might e.g. require updating your database schema if you limit the length of IDs. ```ts filename="AI SDK 4.0" import { generateId } from 'ai'; const id = generateId(); // now 16 characters ``` ### Removed `ExperimentalMessage` types The following types have been removed: - `ExperimentalMessage` (use `ModelMessage` instead) - `ExperimentalUserMessage` (use `CoreUserMessage` instead) - `ExperimentalAssistantMessage` (use `CoreAssistantMessage` instead) - `ExperimentalToolMessage` (use `CoreToolMessage` instead) ```ts filename="AI SDK 3.4" import { ExperimentalMessage, ExperimentalUserMessage, ExperimentalAssistantMessage, ExperimentalToolMessage, } from 'ai'; ``` ```ts filename="AI SDK 4.0" import { ModelMessage, CoreUserMessage, CoreAssistantMessage, CoreToolMessage, } from 'ai'; ``` ### Removed `ExperimentalTool` type The `ExperimentalTool` type has been removed. Please use the `CoreTool` type instead. ```ts filename="AI SDK 3.4" import { ExperimentalTool } from 'ai'; ``` ```ts filename="AI SDK 4.0" import { CoreTool } from 'ai'; ``` ### Removed experimental AI function exports The following exports have been removed: - `experimental_generateText` (use `generateText` instead) - `experimental_streamText` (use `streamText` instead) - `experimental_generateObject` (use `generateObject` instead) - `experimental_streamObject` (use `streamObject` instead) ```ts filename="AI SDK 3.4" import { experimental_generateText, experimental_streamText, experimental_generateObject, experimental_streamObject, } from 'ai'; ``` ```ts filename="AI SDK 4.0" import { generateText, streamText, generateObject, streamObject } from 'ai'; ``` ### Removed AI-stream related methods from `streamText` The following methods have been removed from the `streamText` result: - `toAIStream` - `pipeAIStreamToResponse` - `toAIStreamResponse` Use the `toDataStream`, `pipeDataStreamToResponse`, and `toDataStreamResponse` functions instead. ```ts filename="AI SDK 3.4" const result = await streamText({ // ... }); result.toAIStream(); result.pipeAIStreamToResponse(response); result.toAIStreamResponse(); ``` ```ts filename="AI SDK 4.0" const result = streamText({ // ... }); result.toDataStream(); result.pipeDataStreamToResponse(response); result.toUIMessageStreamResponse(); ``` ### Renamed "formatStreamPart" to "formatDataStreamPart" The `formatStreamPart` function has been renamed to `formatDataStreamPart`. ```ts filename="AI SDK 3.4" formatStreamPart('text', 'Hello, world!'); ``` ```ts filename="AI SDK 4.0" formatDataStreamPart('text', 'Hello, world!'); ``` ### Renamed "parseStreamPart" to "parseDataStreamPart" The `parseStreamPart` function has been renamed to `parseDataStreamPart`. ```ts filename="AI SDK 3.4" const part = parseStreamPart(line); ``` ```ts filename="AI SDK 4.0" const part = parseDataStreamPart(line); ``` ### Renamed `TokenUsage`, `CompletionTokenUsage` and `EmbeddingTokenUsage` types The `TokenUsage`, `CompletionTokenUsage` and `EmbeddingTokenUsage` types have been renamed to `LanguageModelUsage` (for the first two) and `EmbeddingModelUsage` (for the last). ```ts filename="AI SDK 3.4" import { TokenUsage, CompletionTokenUsage, EmbeddingTokenUsage } from 'ai'; ``` ```ts filename="AI SDK 4.0" import { LanguageModelUsage, EmbeddingModelUsage } from 'ai'; ``` ### Removed deprecated telemetry data <Note type="warning"> There is no codemod available for this change. Please review and update your code manually. </Note> The following telemetry data values have been removed: - `ai.finishReason` (now in `ai.response.finishReason`) - `ai.result.object` (now in `ai.response.object`) - `ai.result.text` (now in `ai.response.text`) - `ai.result.toolCalls` (now in `ai.response.toolCalls`) - `ai.stream.msToFirstChunk` (now in `ai.response.msToFirstChunk`) This change will apply to observability providers and any scripts or automation that you use for processing telemetry data. ### Provider Registry #### Removed experimental_Provider, experimental_ProviderRegistry, and experimental_ModelRegistry The `experimental_Provider` interface, `experimental_ProviderRegistry` interface, and `experimental_ModelRegistry` interface have been removed. Please use the `Provider` interface instead. ```ts filename="AI SDK 3.4" import { experimental_Provider, experimental_ProviderRegistry } from 'ai'; ``` ```ts filename="AI SDK 4.0" import { Provider } from 'ai'; ``` <Note> The model registry is not available any more. Please [register providers](/docs/reference/ai-sdk-core/provider-registry#setup) instead. </Note> #### Removed `experimental_​createModelRegistry` function The `experimental_createModelRegistry` function has been removed. Please use the `experimental_createProviderRegistry` function instead. ```ts filename="AI SDK 3.4" import { experimental_createModelRegistry } from 'ai'; ``` ```ts filename="AI SDK 4.0" import { experimental_createProviderRegistry } from 'ai'; ``` <Note> The model registry is not available any more. Please [register providers](/docs/reference/ai-sdk-core/provider-registry#setup) instead. </Note> ### Removed `rawResponse` from results <Note type="warning"> There is no codemod available for this change. Please review and update your code manually. </Note> The `rawResponse` property has been removed from the `generateText`, `streamText`, `generateObject`, and `streamObject` results. You can use the `response` property instead. ```ts filename="AI SDK 3.4" const { text, rawResponse } = await generateText({ // ... }); ``` ```ts filename="AI SDK 4.0" const { text, response } = await generateText({ // ... }); ``` ### Removed `init` option from `pipeDataStreamToResponse` and `toDataStreamResponse` <Note type="warning"> There is no codemod available for this change. Please review and update your code manually. </Note> The `init` option has been removed from the `pipeDataStreamToResponse` and `toDataStreamResponse` functions. You can set the values from `init` directly into the `options` object. ```ts filename="AI SDK 3.4" const result = await streamText({ // ... }); result.toUIMessageStreamResponse(response, { init: { headers: { 'X-Custom-Header': 'value', }, }, // ... }); ``` ```ts filename="AI SDK 4.0" const result = streamText({ // ... }); result.toUIMessageStreamResponse(response, { headers: { 'X-Custom-Header': 'value', }, // ... }); ``` ### Removed `responseMessages` from `generateText` and `streamText` <Note type="warning"> There is no codemod available for this change. Please review and update your code manually. </Note> The `responseMessages` property has been removed from the `generateText` and `streamText` results. This includes the `onFinish` callback. Please use the `response.messages` property instead. ```ts filename="AI SDK 3.4" const { text, responseMessages } = await generateText({ // ... }); ``` ```ts filename="AI SDK 4.0" const { text, response } = await generateText({ // ... }); const responseMessages = response.messages; ``` ### Removed `experimental_​continuationSteps` option The `experimental_continuationSteps` option has been removed from the `generateText` function. Please use the `experimental_continueSteps` option instead. ```ts filename="AI SDK 3.4" const result = await generateText({ experimental_continuationSteps: true, // ... }); ``` ```ts filename="AI SDK 4.0" const result = await generateText({ experimental_continueSteps: true, // ... }); ``` ### Removed `LanguageModelResponseMetadataWithHeaders` type The `LanguageModelResponseMetadataWithHeaders` type has been removed. Please use the `LanguageModelResponseMetadata` type instead. ```ts filename="AI SDK 3.4" import { LanguageModelResponseMetadataWithHeaders } from 'ai'; ``` ```ts filename="AI SDK 4.0" import { LanguageModelResponseMetadata } from 'ai'; ``` #### Changed `streamText` warnings result to Promise <Note type="warning"> There is no codemod available for this change. Please review and update your code manually. </Note> The `warnings` property of the `StreamTextResult` type is now a Promise. ```ts filename="AI SDK 3.4" const result = await streamText({ // ... }); const warnings = result.warnings; ``` ```ts filename="AI SDK 4.0" const result = streamText({ // ... }); const warnings = await result.warnings; ``` #### Changed `streamObject` warnings result to Promise <Note type="warning"> There is no codemod available for this change. Please review and update your code manually. </Note> The `warnings` property of the `StreamObjectResult` type is now a Promise. ```ts filename="AI SDK 3.4" const result = await streamObject({ // ... }); const warnings = result.warnings; ``` ```ts filename="AI SDK 4.0" const result = streamObject({ // ... }); const warnings = await result.warnings; ``` #### Renamed `simulateReadableStream` `values` to `chunks` <Note type="warning"> There is no codemod available for this change. Please review and update your code manually. </Note> The `simulateReadableStream` function from `ai/test` has been renamed to `chunks`. ```ts filename="AI SDK 3.4" import { simulateReadableStream } from 'ai/test'; const stream = simulateReadableStream({ values: [1, 2, 3], chunkDelayInMs: 100, }); ``` ```ts filename="AI SDK 4.0" import { simulateReadableStream } from 'ai/test'; const stream = simulateReadableStream({ chunks: [1, 2, 3], chunkDelayInMs: 100, }); ``` ## AI SDK RSC Changes <Note type="warning"> There are no codemods available for the changes in this section. Please review and update your code manually. </Note> ### Removed `render` function The AI SDK RSC 3.0 `render` function has been removed. Please use the `streamUI` function instead or [switch to AI SDK UI](/docs/ai-sdk-rsc/migrating-to-ui). ```ts filename="AI SDK 3.0" import { render } from '@ai-sdk/rsc'; ``` ```ts filename="AI SDK 4.0" import { streamUI } from '@ai-sdk/rsc'; ``` ## AI SDK UI Changes ### Removed Svelte, Vue, and SolidJS exports <Note type="warning"> This codemod only operates on `.ts` and `.tsx` files. If you have code in files with other suffixes, please review and update your code manually. </Note> The `ai` package no longer exports Svelte, Vue, and SolidJS UI integrations. You need to install the `@ai-sdk/svelte`, `@ai-sdk/vue`, and `@ai-sdk/solid` packages directly. ```ts filename="AI SDK 3.4" import { useChat } from 'ai/svelte'; ``` ```ts filename="AI SDK 4.0" import { useChat } from '@ai-sdk/svelte'; ``` ### Removed `experimental_StreamData` The `experimental_StreamData` export has been removed. Please use the `StreamData` export instead. ```ts filename="AI SDK 3.4" import { experimental_StreamData } from 'ai'; ``` ```ts filename="AI SDK 4.0" import { StreamData } from 'ai'; ``` ### `useChat` hook <Note type="warning"> There are no codemods available for the changes in this section. Please review and update your code manually. </Note> #### Removed `streamMode` setting The `streamMode` options has been removed from the `useChat` hook. Please use the `streamProtocol` parameter instead. ```ts filename="AI SDK 3.4" const { messages } = useChat({ streamMode: 'text', // ... }); ``` ```ts filename="AI SDK 4.0" const { messages } = useChat({ streamProtocol: 'text', // ... }); ``` #### Replaced roundtrip setting with `maxSteps` The following options have been removed from the `useChat` hook: - `experimental_maxAutomaticRoundtrips` - `maxAutomaticRoundtrips` - `maxToolRoundtrips` Please use the [`maxSteps`](/docs/ai-sdk-core/tools-and-tool-calling#multi-step-calls) option instead. The value of `maxSteps` is equal to roundtrips + 1. ```ts filename="AI SDK 3.4" const { messages } = useChat({ experimental_maxAutomaticRoundtrips: 2, // or maxAutomaticRoundtrips // or maxToolRoundtrips // ... }); ``` ```ts filename="AI SDK 4.0" const { messages } = useChat({ maxSteps: 3, // 2 roundtrips + 1 // ... }); ``` #### Removed `options` setting The `options` parameter in the `useChat` hook has been removed. Please use the `headers` and `body` parameters instead. ```ts filename="AI SDK 3.4" const { messages } = useChat({ options: { headers: { 'X-Custom-Header': 'value', }, }, // ... }); ``` ```ts filename="AI SDK 4.0" const { messages } = useChat({ headers: { 'X-Custom-Header': 'value', }, // ... }); ``` #### Removed `experimental_addToolResult` method The `experimental_addToolResult` method has been removed from the `useChat` hook. Please use the `addToolResult` method instead. ```ts filename="AI SDK 3.4" const { messages, experimental_addToolResult } = useChat({ // ... }); ``` ```ts filename="AI SDK 4.0" const { messages, addToolResult } = useChat({ // ... }); ``` #### Changed default value of `keepLastMessageOnError` to true and deprecated the option The `keepLastMessageOnError` option has been changed to default to `true`. The option will be removed in the next major release. ```ts filename="AI SDK 3.4" const { messages } = useChat({ keepLastMessageOnError: true, // ... }); ``` ```ts filename="AI SDK 4.0" const { messages } = useChat({ // ... }); ``` ### `useCompletion` hook <Note type="warning"> There are no codemods available for the changes in this section. Please review and update your code manually. </Note> #### Removed `streamMode` setting The `streamMode` options has been removed from the `useCompletion` hook. Please use the `streamProtocol` parameter instead. ```ts filename="AI SDK 3.4" const { text } = useCompletion({ streamMode: 'text', // ... }); ``` ```ts filename="AI SDK 4.0" const { text } = useCompletion({ streamProtocol: 'text', // ... }); ``` ### `useAssistant` hook #### Removed `experimental_useAssistant` export The `experimental_useAssistant` export has been removed from the `useAssistant` hook. Please use the `useAssistant` hook directly instead. ```ts filename="AI SDK 3.4" import { experimental_useAssistant } from '@ai-sdk/react'; ``` ```ts filename="AI SDK 4.0" import { useAssistant } from '@ai-sdk/react'; ``` #### Removed `threadId` and `messageId` from `AssistantResponse` <Note type="warning"> There is no codemod available for this change. Please review and update your code manually. </Note> The `threadId` and `messageId` parameters have been removed from the `AssistantResponse` function. Please use the `threadId` and `messageId` variables from the outer scope instead. ```ts filename="AI SDK 3.4" return AssistantResponse( { threadId: myThreadId, messageId: myMessageId }, async ({ forwardStream, sendDataMessage, threadId, messageId }) => { // use threadId and messageId here }, ); ``` ```ts filename="AI SDK 4.0" return AssistantResponse( { threadId: myThreadId, messageId: myMessageId }, async ({ forwardStream, sendDataMessage }) => { // use myThreadId and myMessageId here }, ); ``` #### Removed `experimental_​AssistantResponse` export <Note type="warning"> There is no codemod available for this change. Please review and update your code manually. </Note> The `experimental_AssistantResponse` export has been removed. Please use the `AssistantResponse` function directly instead. ```ts filename="AI SDK 3.4" import { experimental_AssistantResponse } from 'ai'; ``` ```ts filename="AI SDK 4.0" import { AssistantResponse } from 'ai'; ``` ### `experimental_useObject` hook <Note type="warning"> There are no codemods available for the changes in this section. Please review and update your code manually. </Note> The `setInput` helper has been removed from the `experimental_useObject` hook. Please use the `submit` helper instead. ```ts filename="AI SDK 3.4" const { object, setInput } = useObject({ // ... }); ``` ```ts filename="AI SDK 4.0" const { object, submit } = useObject({ // ... }); ``` ## AI SDK Errors ### Removed `isXXXError` static methods The `isXXXError` static methods have been removed from AI SDK errors. Please use the `isInstance` method of the corresponding error class instead. ```ts filename="AI SDK 3.4" import { APICallError } from 'ai'; APICallError.isAPICallError(error); ``` ```ts filename="AI SDK 4.0" import { APICallError } from 'ai'; APICallError.isInstance(error); ``` ### Removed `toJSON` method <Note type="warning"> There is no codemod available for this change. Please review and update your code manually. </Note> The `toJSON` method has been removed from AI SDK errors. ## AI SDK 2.x Legacy Changes <Note type="warning"> There are no codemods available for the changes in this section. Please review and update your code manually. </Note> ### Removed 2.x legacy providers Legacy providers from AI SDK 2.x have been removed. Please use the new [AI SDK provider architecture](/docs/foundations/providers-and-models) instead. #### Removed 2.x legacy function and tool calling The legacy `function_call` and `tools` options have been removed from `useChat` and `Message`. The `name` property from the `Message` type has been removed. Please use the [AI SDK Core tool calling](/docs/ai-sdk-core/tools-and-tool-calling) instead. ### Removed 2.x prompt helpers Prompt helpers for constructing message prompts are no longer needed with the AI SDK provider architecture and have been removed. ### Removed 2.x `AIStream` The `AIStream` function and related exports have been removed. Please use the [`streamText`](/docs/reference/ai-sdk-core/stream-text) function and its `toDataStream()` method instead. ### Removed 2.x `StreamingTextResponse` The `StreamingTextResponse` function has been removed. Please use the [`streamText`](/docs/reference/ai-sdk-core/stream-text) function and its `toDataStreamResponse()` method instead. ### Removed 2.x `streamToResponse` The `streamToResponse` function has been removed. Please use the [`streamText`](/docs/reference/ai-sdk-core/stream-text) function and its `pipeDataStreamToResponse()` method instead. ### Removed 2.x RSC `Tokens` streaming The legacy `Tokens` RSC streaming from 2.x has been removed. `Tokens` were implemented prior to AI SDK RSC and are no longer needed. ## Codemod Table The following table lists codemod availability for the AI SDK 4.0 upgrade process. Note the codemod `upgrade` command will run all of them for you. This list is provided to give visibility into which migrations have some automation. It can also be helpful to find the codemod names if you'd like to run a subset of codemods. For more, see the [Codemods](#codemods) section. | Change | Codemod | | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | | **Provider Changes** | | | Removed baseUrl option | `v4/replace-baseurl` | | **Anthropic Provider** | | | Removed Anthropic facade | `v4/remove-anthropic-facade` | | Removed topK setting | _N/A_ | | **Google Generative AI Provider** | | | Removed Google facade | `v4/remove-google-facade` | | Removed topK setting | _N/A_ | | **Google Vertex Provider** | | | Removed topK setting | _N/A_ | | **Mistral Provider** | | | Removed Mistral facade | `v4/remove-mistral-facade` | | **OpenAI Provider** | | | Removed OpenAI facade | `v4/remove-openai-facade` | | **LangChain Adapter** | | | Removed toAIStream | `v4/replace-langchain-toaistream` | | **AI SDK Core Changes** | | | streamText returns immediately | `v4/remove-await-streamtext` | | streamObject returns immediately | `v4/remove-await-streamobject` | | Remove roundtrips | `v4/replace-roundtrips-with-maxsteps` | | Removed nanoid export | `v4/replace-nanoid` | | Increased default size of generated IDs | _N/A_ | | Removed ExperimentalMessage types | `v4/remove-experimental-message-types` | | Removed ExperimentalTool type | `v4/remove-experimental-tool` | | Removed experimental AI function exports | `v4/remove-experimental-ai-fn-exports` | | Removed AI-stream related methods from streamText | `v4/remove-ai-stream-methods-from-stream-text-result` | | Renamed "formatStreamPart" to "formatDataStreamPart" | `v4/rename-format-stream-part` | | Renamed "parseStreamPart" to "parseDataStreamPart" | `v4/rename-parse-stream-part` | | Renamed TokenUsage, CompletionTokenUsage and EmbeddingTokenUsage types | `v4/replace-token-usage-types` | | Removed deprecated telemetry data | _N/A_ | | **Provider Registry** | | | &rarr; Removed experimental_Provider, experimental_ProviderRegistry, and experimental_ModelRegistry | `v4/remove-deprecated-provider-registry-exports` | | &rarr; Removed experimental_createModelRegistry function | _N/A_ | | Removed rawResponse from results | _N/A_ | | Removed init option from pipeDataStreamToResponse and toDataStreamResponse | _N/A_ | | Removed responseMessages from generateText and streamText | _N/A_ | | Removed experimental_continuationSteps option | `v4/replace-continuation-steps` | | Removed LanguageModelResponseMetadataWithHeaders type | `v4/remove-metadata-with-headers` | | Changed streamText warnings result to Promise | _N/A_ | | Changed streamObject warnings result to Promise | _N/A_ | | Renamed simulateReadableStream values to chunks | _N/A_ | | **AI SDK RSC Changes** | | | Removed render function | _N/A_ | | **AI SDK UI Changes** | | | Removed Svelte, Vue, and SolidJS exports | `v4/rewrite-framework-imports` | | Removed experimental_StreamData | `v4/remove-experimental-streamdata` | | **useChat hook** | | | Removed streamMode setting | _N/A_ | | Replaced roundtrip setting with maxSteps | `v4/replace-roundtrips-with-maxsteps` | | Removed options setting | _N/A_ | | Removed experimental_addToolResult method | _N/A_ | | Changed default value of keepLastMessageOnError to true and deprecated the option | _N/A_ | | **useCompletion hook** | | | Removed streamMode setting | _N/A_ | | **useAssistant hook** | | | Removed experimental_useAssistant export | `v4/remove-experimental-useassistant` | | Removed threadId and messageId from AssistantResponse | _N/A_ | | Removed experimental_AssistantResponse export | _N/A_ | | **experimental_useObject hook** | | | Removed setInput helper | _N/A_ | | **AI SDK Errors** | | | Removed isXXXError static methods | `v4/remove-isxxxerror` | | Removed toJSON method | _N/A_ | | **AI SDK 2.x Legacy Changes** | | | Removed 2.x legacy providers | _N/A_ | | Removed 2.x legacy function and tool calling | _N/A_ | | Removed 2.x prompt helpers | _N/A_ | | Removed 2.x AIStream | _N/A_ | | Removed 2.x StreamingTextResponse | _N/A_ | | Removed 2.x streamToResponse | _N/A_ | | Removed 2.x RSC Tokens streaming | _N/A_ | --- File: /ai/content/docs/08-migration-guides/36-migration-guide-3-4.mdx --- --- title: Migrate AI SDK 3.3 to 3.4 description: Learn how to upgrade AI SDK 3.3 to 3.4. --- # Migrate AI SDK 3.3 to 3.4 <Note> Check out the [AI SDK 3.4 release blog post](https://vercel.com/blog/ai-sdk-3-4) for more information about the release. </Note> No breaking changes in this release. --- File: /ai/content/docs/08-migration-guides/37-migration-guide-3-3.mdx --- --- title: Migrate AI SDK 3.2 to 3.3 description: Learn how to upgrade AI SDK 3.2 to 3.3. --- # Migrate AI SDK 3.2 to 3.3 <Note> Check out the [AI SDK 3.3 release blog post](https://vercel.com/blog/vercel-ai-sdk-3-3) for more information about the release. </Note> No breaking changes in this release. The following changelog encompasses all changes made in the 3.2.x series, introducing significant improvements and new features across the AI SDK and its associated libraries: ## New Features ### Open Telemetry Support - Added experimental [OpenTelemetry support](/docs/ai-sdk-core/telemetry#telemetry) for all [AI SDK Core functions](/docs/ai-sdk-core/overview#ai-sdk-core-functions), enabling better observability and tracing capabilities. ### AI SDK UI Improvements - Introduced the experimental **`useObject`** hook (for React) that can be used in conjunction with **`streamObject`** on the backend to enable seamless streaming of structured data. - Enhanced **`useChat`** with experimental support for attachments and streaming tool calls, providing more versatile chat functionalities. - Patched **`useChat`** to prevent empty submissions, improving the quality of user interactions by ensuring that only intended inputs are processed. - Fix **`useChat`**'s **`reload`** function, now correctly sending data, body, and headers. - Implemented **`setThreadId`** helper for **`useAssistant`**, simplifying thread management. - Documented the stream data protocol for **`useChat`** and **`useCompletion`**, allowing developers to use these functions with any backend. The stream data protocol also enables the use of custom frontends with **`streamText`**. - Added support for custom fetch functions and request body customization, offering greater control over API interactions. - Added **`onFinish`** to **`useChat`** hook for access to token usage and finish reason. ### Core Enhancements - Implemented support for sending custom request headers, enabling more tailored API requests. - Added raw JSON schema support alongside existing Zod support, providing more options for schema and data validation. - Introduced usage information for **`embed`** and **`embedMany`** functions, offering insights into token usage. - Added support for additional settings including **`stopSequences`** and **`topK`**, allowing for finer control over text generation. - Provided access to information for all steps on **`generateText`**, providing access to intermediate tool calls and results. ### New Providers - [AWS Bedrock provider](/providers/ai-sdk-providers/amazon-bedrock). ### Provider Improvements - Enhanced existing providers including Anthropic, Google, Azure, and OpenAI with various improvements and bug fixes. - Upgraded the LangChain adapter with StreamEvent v2 support and introduced the **`toDataStreamResponse`** function, enabling conversion of LangChain output streams to data stream responses. - Added legacy function calling support to the OpenAI provider. - Updated Mistral AI provider with fixes and improvements for tool calling support. ### UI Framework Support Expansion - SolidJS: Updated **`useChat`** and **`useCompletion`** to achieve feature parity with React implementations. - Vue.js: Introduced **`useAssistant`** hook. - Vue.js / Nuxt: [Updated examples](https://github.com/vercel/ai/tree/main/examples/nuxt-openai) to showcase latest features and best practices. - Svelte: Added tool calling support to **`useChat`.** ## Fixes and Improvements - Resolved various issues across different components of the SDK, including race conditions, error handling, and state management. --- File: /ai/content/docs/08-migration-guides/38-migration-guide-3-2.mdx --- --- title: Migrate AI SDK 3.1 to 3.2 description: Learn how to upgrade AI SDK 3.1 to 3.2. --- # Migrate AI SDK 3.1 to 3.2 <Note> Check out the [AI SDK 3.2 release blog post](https://vercel.com/blog/introducing-vercel-ai-sdk-3-2) for more information about the release. </Note> This guide will help you upgrade to AI SDK 3.2: - Experimental `StreamingReactResponse` functionality has been removed - Several features have been deprecated - UI framework integrations have moved to their own Node modules ## Upgrading ### AI SDK To update to AI SDK version 3.2, run the following command using your preferred package manager: <Snippet text="pnpm add ai@latest" /> ## Removed Functionality The experimental `StreamingReactResponse` has been removed. You can use [AI SDK RSC](/docs/ai-sdk-rsc/overview) to build streaming UIs. ## Deprecated Functionality The `nanoid` export has been deprecated. Please use [`generateId`](/docs/reference/ai-sdk-core/generate-id) instead. ## UI Package Separation AI SDK UI supports several frameworks: [React](https://react.dev/), [Svelte](https://svelte.dev/), [Vue.js](https://vuejs.org/), and [SolidJS](https://www.solidjs.com/). The integrations (other than React and RSC) have moved to separate Node modules. You need to update the import and require statements as follows: - Change `ai/svelte` to `@ai-sdk/svelte` - Change `ai/vue` to `@ai-sdk/vue` - Change `ai/solid` to `@ai-sdk/solid` The old exports are still available but will be removed in a future release. --- File: /ai/content/docs/08-migration-guides/39-migration-guide-3-1.mdx --- --- title: Migrate AI SDK 3.0 to 3.1 description: Learn how to upgrade AI SDK 3.0 to 3.1. --- # Migrate AI SDK 3.0 to 3.1 <Note> Check out the [AI SDK 3.1 release blog post](https://vercel.com/blog/vercel-ai-sdk-3-1-modelfusion-joins-the-team) for more information about the release. </Note> This guide will help you: - Upgrade to AI SDK 3.1 - Migrate from Legacy Providers to AI SDK Core - Migrate from [`render`](/docs/reference/ai-sdk-rsc/render) to [`streamUI`](/docs/reference/ai-sdk-rsc/stream-ui) Upgrading to AI SDK 3.1 does not require using the newly released AI SDK Core API or [`streamUI`](/docs/reference/ai-sdk-rsc/stream-ui) function. ## Upgrading ### AI SDK To update to AI SDK version 3.1, run the following command using your preferred package manager: <Snippet text="pnpm add ai@3.1" /> ## Next Steps The release of AI SDK 3.1 introduces several new features that improve the way you build AI applications with the SDK: - AI SDK Core, a brand new unified API for interacting with large language models (LLMs). - [`streamUI`](/docs/reference/ai-sdk-rsc/stream-ui), a new abstraction, built upon AI SDK Core functions that simplifies building streaming UIs. ## Migrating from Legacy Providers to AI SDK Core Prior to AI SDK Core, you had to use a model provider's SDK to query their models. In the following Route Handler, you use the OpenAI SDK to query their model. You then pipe that response into the [`OpenAIStream`](/docs/reference/stream-helpers/openai-stream) function which returns a [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) that you can pass to the client using a new [`StreamingTextResponse`](/docs/reference/stream-helpers/streaming-text-response). ```tsx import OpenAI from 'openai'; import { OpenAIStream, StreamingTextResponse } from 'ai'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY!, }); export async function POST(req: Request) { const { messages } = await req.json(); const response = await openai.chat.completions.create({ model: 'gpt-4.1', stream: true, messages, }); const stream = OpenAIStream(response); return new StreamingTextResponse(stream); } ``` With AI SDK Core you have a unified API for any provider that implements the [AI SDK Language Model Specification](/providers/community-providers/custom-providers). Let’s take a look at the example above, but refactored to utilize the AI SDK Core API alongside the AI SDK OpenAI provider. In this example, you import the LLM function you want to use from the `ai` package, import the OpenAI provider from `@ai-sdk/openai`, and then you call the model and return the response using the `toDataStreamResponse()` helper function. ```tsx import { streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; export async function POST(req: Request) { const { messages } = await req.json(); const result = await streamText({ model: openai('gpt-4.1'), messages, }); return result.toUIMessageStreamResponse(); } ``` ## Migrating from `render` to `streamUI` The AI SDK RSC API was launched as part of version 3.0. This API introduced the [`render`](/docs/reference/ai-sdk-rsc/render) function, a helper function to create streamable UIs with OpenAI models. With the new AI SDK Core API, it became possible to make streamable UIs possible with any compatible provider. The following example Server Action uses the `render` function using the model provider directly from OpenAI. You first create an OpenAI provider instance with the OpenAI SDK. Then, you pass it to the provider key of the render function alongside a tool that returns a React Server Component, defined in the `render` key of the tool. ```tsx import { render } from '@ai-sdk/rsc'; import OpenAI from 'openai'; import { z } from 'zod'; import { Spinner, Weather } from '@/components'; import { getWeather } from '@/utils'; const openai = new OpenAI(); async function submitMessage(userInput = 'What is the weather in SF?') { 'use server'; return render({ provider: openai, model: 'gpt-4.1', messages: [ { role: 'system', content: 'You are a helpful assistant' }, { role: 'user', content: userInput }, ], text: ({ content }) => <p>{content}</p>, tools: { get_city_weather: { description: 'Get the current weather for a city', parameters: z .object({ city: z.string().describe('the city'), }) .required(), render: async function* ({ city }) { yield <Spinner />; const weather = await getWeather(city); return <Weather info={weather} />; }, }, }, }); } ``` With the new [`streamUI`](/docs/reference/ai-sdk-rsc/stream-ui) function, you can now use any compatible AI SDK provider. In this example, you import the AI SDK OpenAI provider. Then, you pass it to the [`model`](/docs/reference/ai-sdk-rsc/stream-ui#model) key of the new [`streamUI`](/docs/reference/ai-sdk-rsc/stream-ui) function. Finally, you declare a tool and return a React Server Component, defined in the [`generate`](/docs/reference/ai-sdk-rsc/stream-ui#tools-generate) key of the tool. ```tsx import { streamUI } from '@ai-sdk/rsc'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; import { Spinner, Weather } from '@/components'; import { getWeather } from '@/utils'; async function submitMessage(userInput = 'What is the weather in SF?') { 'use server'; const result = await streamUI({ model: openai('gpt-4.1'), system: 'You are a helpful assistant', messages: [{ role: 'user', content: userInput }], text: ({ content }) => <p>{content}</p>, tools: { get_city_weather: { description: 'Get the current weather for a city', parameters: z .object({ city: z.string().describe('Name of the city'), }) .required(), generate: async function* ({ city }) { yield <Spinner />; const weather = await getWeather(city); return <Weather info={weather} />; }, }, }, }); return result.value; } ``` --- File: /ai/content/docs/08-migration-guides/index.mdx --- --- title: Migration Guides description: Learn how to upgrade between Vercel AI versions. collapsed: true --- # Migration Guides - [ Migrate AI SDK 4.x to 5.0 ](/docs/migration-guides/migration-guide-5-0) - [ Migrate AI SDK 4.1 to 4.2 ](/docs/migration-guides/migration-guide-4-2) - [ Migrate AI SDK 4.0 to 4.1 ](/docs/migration-guides/migration-guide-4-1) - [ Migrate AI SDK 3.4 to 4.0 ](/docs/migration-guides/migration-guide-4-0) - [ Migrate AI SDK 3.3 to 3.4 ](/docs/migration-guides/migration-guide-3-4) - [ Migrate AI SDK 3.2 to 3.3 ](/docs/migration-guides/migration-guide-3-3) - [ Migrate AI SDK 3.1 to 3.2 ](/docs/migration-guides/migration-guide-3-2) - [ Migrate AI SDK 3.0 to 3.1 ](/docs/migration-guides/migration-guide-3-1) ## Versioning - [ Versioning ](/docs/migration-guides/versioning) --- File: /ai/content/docs/09-troubleshooting/01-azure-stream-slow.mdx --- --- title: Azure OpenAI Slow to Stream description: Learn to troubleshoot Azure OpenAI slow to stream issues. --- # Azure OpenAI Slow To Stream ## Issue When using OpenAI hosted on Azure, streaming is slow and in big chunks. ## Cause This is a Microsoft Azure issue. Some users have reported the following solutions: - **Update Content Filtering Settings**: Inside [Azure AI Studio](https://ai.azure.com/), within "Shared resources" > "Content filters", create a new content filter and set the "Streaming mode (Preview)" under "Output filter" from "Default" to "Asynchronous Filter". ## Solution You can use the [`smoothStream` transformation](/docs/ai-sdk-core/generating-text#smoothing-streams) to stream each word individually. ```tsx highlight="6" import { smoothStream, streamText } from 'ai'; const result = streamText({ model, prompt, experimental_transform: smoothStream(), }); ``` --- File: /ai/content/docs/09-troubleshooting/02-client-side-function-calls-not-invoked.mdx --- --- title: Client-Side Function Calls Not Invoked description: Troubleshooting client-side function calls not being invoked. --- # Client-Side Function Calls Not Invoked ## Issue I upgraded the AI SDK to v3.0.20 or newer. I am using [`OpenAIStream`](/docs/reference/stream-helpers/openai-stream). Client-side function calls are no longer invoked. ## Solution You will need to add a stub for `experimental_onFunctionCall` to [`OpenAIStream`](/docs/reference/stream-helpers/openai-stream) to enable the correct forwarding of the function calls to the client. ```tsx const stream = OpenAIStream(response, { async experimental_onFunctionCall() { return; }, }); ``` --- File: /ai/content/docs/09-troubleshooting/03-server-actions-in-client-components.mdx --- --- title: Server Actions in Client Components description: Troubleshooting errors related to server actions in client components. --- # Server Actions in Client Components You may use Server Actions in client components, but sometimes you may encounter the following issues. ## Issue It is not allowed to define inline `"use server"` annotated Server Actions in Client Components. ## Solution To use Server Actions in a Client Component, you can either: - Export them from a separate file with `"use server"` at the top. - Pass them down through props from a Server Component. - Implement a combination of [`createAI`](/docs/reference/ai-sdk-rsc/create-ai) and [`useActions`](/docs/reference/ai-sdk-rsc/use-actions) hooks to access them. Learn more about [Server Actions and Mutations](https://nextjs.org/docs/app/api-reference/functions/server-actions#with-client-components). ```ts file='actions.ts' 'use server'; import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; export async function getAnswer(question: string) { 'use server'; const { text } = await generateText({ model: openai.chat('gpt-3.5-turbo'), prompt: question, }); return { answer: text }; } ``` --- File: /ai/content/docs/09-troubleshooting/04-strange-stream-output.mdx --- --- title: useChat/useCompletion stream output contains 0:... instead of text description: How to fix strange stream output in the UI --- # useChat/useCompletion stream output contains 0:... instead of text ## Issue I am using custom client code to process a server response that is sent using [`StreamingTextResponse`](/docs/reference/stream-helpers/streaming-text-response). I am using version `3.0.20` or newer of the AI SDK. When I send a query, the UI streams text such as `0: "Je"`, `0: " suis"`, `0: "des"...` instead of the text that I’m looking for. ## Background The AI SDK has switched to the stream data protocol in version `3.0.20`. It sends different stream parts to support data, tool calls, etc. What you see is the raw stream data protocol response. ## Solution You have several options: 1. Use the AI Core [`streamText`](/docs/reference/ai-sdk-core/stream-text) function to send a raw text stream: ```tsx export async function POST(req: Request) { const { prompt } = await req.json(); const result = streamText({ model: openai.completion('gpt-3.5-turbo-instruct'), maxOutputTokens: 2000, prompt, }); return result.toTextStreamResponse(); } ``` 2. Pin the AI SDK version to `3.0.19` . This will keep the raw text stream. --- File: /ai/content/docs/09-troubleshooting/05-streamable-ui-errors.mdx --- --- title: Streamable UI Errors description: Troubleshooting errors related to streamable UI. --- # Streamable UI Component Error ## Issue - Variable Not Found - Cannot find `div` - `Component` refers to a value, but is being used as a type ## Solution If you encounter these errors when working with streamable UIs within server actions, it is likely because the file ends in `.ts` instead of `.tsx`. --- File: /ai/content/docs/09-troubleshooting/05-tool-invocation-missing-result.mdx --- --- title: Tool Invocation Missing Result Error description: How to fix the "ToolInvocation must have a result" error when using tools without execute functions --- # Tool Invocation Missing Result Error ## Issue When using `generateText()` or `streamText()`, you may encounter the error "ToolInvocation must have a result" when a tool without an `execute` function is called. ## Cause The error occurs when you define a tool without an `execute` function and don't provide the result through other means (like `useChat`'s `onToolCall` or `addToolResult` functions). Each time a tool is invoked, the model expects to receive a result before continuing the conversation. Without a result, the model cannot determine if the tool call succeeded or failed and the conversation state becomes invalid. ## Solution You have two options for handling tool results: 1. Server-side execution using tools with an `execute` function: ```tsx const tools = { weather: tool({ description: 'Get the weather in a location', parameters: z.object({ location: z .string() .describe('The city and state, e.g. "San Francisco, CA"'), }), execute: async ({ location }) => { // Fetch and return weather data return { temperature: 72, conditions: 'sunny', location }; }, }), }; ``` 2. Client-side execution with `useChat` (omitting the `execute` function), you must provide results using `addToolResult`: ```tsx import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls, } from 'ai'; const { messages, sendMessage, addToolResult } = useChat({ // Automatically submit when all tool results are available sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, // Handle tool calls in onToolCall onToolCall: async ({ toolCall }) => { if (toolCall.toolName === 'getLocation') { const result = await getLocationData(); // Important: Don't await inside onToolCall to avoid deadlocks addToolResult({ tool: 'getLocation', toolCallId: toolCall.toolCallId, output: result, }); } }, }); ``` ```tsx // For interactive UI elements: const { messages, sendMessage, addToolResult } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat' }), sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, }); // Inside your JSX, when rendering tool calls: <button onClick={() => addToolResult({ tool: 'myTool', toolCallId, // must provide tool call ID output: { /* your tool result */ }, }) } > Confirm </button>; ``` <Note type="warning"> Whether handling tools on the server or client, each tool call must have a corresponding result before the conversation can continue. </Note> --- File: /ai/content/docs/09-troubleshooting/06-streaming-not-working-when-deployed.mdx --- --- title: Streaming Not Working When Deployed description: Troubleshooting streaming issues in deployed apps. --- # Streaming Not Working When Deployed ## Issue Streaming with the AI SDK works in my local development environment. However, when deploying, streaming does not work in the deployed app. Instead of streaming, only the full response is returned after a while. ## Cause The causes of this issue are varied and depend on the deployment environment. ## Solution You can try the following: - add `'Transfer-Encoding': 'chunked'` and/or `Connection: 'keep-alive'` headers ```tsx return result.toUIMessageStreamResponse({ headers: { 'Transfer-Encoding': 'chunked', Connection: 'keep-alive', }, }); ``` --- File: /ai/content/docs/09-troubleshooting/06-streaming-not-working-when-proxied.mdx --- --- title: Streaming Not Working When Proxied description: Troubleshooting streaming issues in proxied apps. --- # Streaming Not Working When Proxied ## Issue Streaming with the AI SDK doesn't work in local development environment, or deployed in some proxy environments. Instead of streaming, only the full response is returned after a while. ## Cause The causes of this issue are caused by the proxy middleware. If the middleware is configured to compress the response, it will cause the streaming to fail. ## Solution You can try the following, the solution only affects the streaming API: - add `'Content-Encoding': 'none'` headers ```tsx return result.toUIMessageStreamResponse({ headers: { 'Content-Encoding': 'none', }, }); ``` --- File: /ai/content/docs/09-troubleshooting/06-timeout-on-vercel.mdx --- --- title: Getting Timeouts When Deploying on Vercel description: Learn how to fix timeouts and cut off responses when deploying to Vercel. --- # Getting Timeouts When Deploying on Vercel ## Issue Streaming with the AI SDK works in my local development environment. However, when I'm deploying to Vercel, longer responses get chopped off in the UI and I'm seeing timeouts in the Vercel logs or I'm seeing the error: `Uncaught (in promise) Error: Connection closed`. ## Solution If you are using Next.js with the App Router, you can add the following to your route file or the page you are calling your Server Action from: ```tsx export const maxDuration = 30; ``` This increases the maximum duration of the function to 30 seconds. For other frameworks such as Svelte, you can set timeouts in your `vercel.json` file: ```json { "functions": { "api/chat/route.ts": { "maxDuration": 30 } } } ``` ## Learn more - [Configuring Maximum Duration for Vercel Functions](https://vercel.com/docs/functions/configuring-functions/duration) - [Maximum Duration Limits](https://vercel.com/docs/functions/runtimes#max-duration) --- File: /ai/content/docs/09-troubleshooting/07-unclosed-streams.mdx --- --- title: Unclosed Streams description: Troubleshooting errors related to unclosed streams. --- # Unclosed Streams Sometimes streams are not closed properly, which can lead to unexpected behavior. The following are some common issues that can occur when streams are not closed properly. ## Issue The streamable UI has been slow to update. ## Solution This happens when you create a streamable UI using [`createStreamableUI`](/docs/reference/ai-sdk-rsc/create-streamable-ui) and fail to close the stream. In order to fix this, you must ensure you close the stream by calling the [`.done()`](/docs/reference/ai-sdk-rsc/create-streamable-ui#done) method. This will ensure the stream is closed. ```tsx file='app/actions.tsx' import { createStreamableUI } from '@ai-sdk/rsc'; const submitMessage = async () => { 'use server'; const stream = createStreamableUI('1'); stream.update('2'); stream.append('3'); stream.done('4'); // [!code ++] return stream.value; }; ``` --- File: /ai/content/docs/09-troubleshooting/08-use-chat-failed-to-parse-stream.mdx --- --- title: useChat Failed to Parse Stream description: Troubleshooting errors related to the Use Chat Failed to Parse Stream error. --- # `useChat` "Failed to Parse Stream String" Error ## Issue I am using [`useChat`](/docs/reference/ai-sdk-ui/use-chat) or [`useCompletion`](/docs/reference/ai-sdk-ui/use-completion), and I am getting a `"Failed to parse stream string. Invalid code"` error. I am using version `3.0.20` or newer of the AI SDK. ## Background The AI SDK has switched to the stream data protocol in version `3.0.20`. [`useChat`](/docs/reference/ai-sdk-ui/use-chat) and [`useCompletion`](/docs/reference/ai-sdk-ui/use-completion) expect stream parts that support data, tool calls, etc. What you see is a failure to parse the stream. This can be caused by using an older version of the AI SDK in the backend, by providing a text stream using a custom provider, or by using a raw LangChain stream result. ## Solution You can switch [`useChat`](/docs/reference/ai-sdk-ui/use-chat) and [`useCompletion`](/docs/reference/ai-sdk-ui/use-completion) to raw text stream processing with the [`streamProtocol`](/docs/reference/ai-sdk-ui/use-completion#stream-protocol) parameter. Set it to `text` as follows: ```tsx const { messages, append } = useChat({ streamProtocol: 'text' }); ``` --- File: /ai/content/docs/09-troubleshooting/09-client-stream-error.mdx --- --- title: Server Action Plain Objects Error description: Troubleshooting errors related to using AI SDK Core functions with Server Actions. --- # "Only plain objects can be passed from client components" Server Action Error ## Issue I am using [`streamText`](/docs/reference/ai-sdk-core/stream-text) or [`streamObject`](/docs/reference/ai-sdk-core/stream-object) with Server Actions, and I am getting a `"only plain objects and a few built ins can be passed from client components"` error. ## Background This error occurs when you're trying to return a non-serializable object from a Server Action to a Client Component. The streamText function likely returns an object with methods or complex structures that can't be directly serialized and passed to the client. ## Solution To fix this issue, you need to ensure that you're only returning serializable data from your Server Action. Here's how you can modify your approach: 1. Instead of returning the entire result object from streamText, extract only the necessary serializable data. 2. Use the [`createStreamableValue`](/docs/reference/ai-sdk-rsc/create-streamable-value) function to create a streamable value that can be safely passed to the client. Here's an example that demonstrates how to implement this solution: [Streaming Text Generation](/examples/next-app/basics/streaming-text-generation). This approach ensures that only serializable data (the text) is passed to the client, avoiding the "only plain objects" error. --- File: /ai/content/docs/09-troubleshooting/10-use-chat-tools-no-response.mdx --- --- title: useChat No Response description: Troubleshooting errors related to the Use Chat Failed to Parse Stream error. --- # `useChat` No Response ## Issue I am using [`useChat`](/docs/reference/ai-sdk-ui/use-chat). When I log the incoming messages on the server, I can see the tool call and the tool result, but the model does not respond with anything. ## Solution To resolve this issue, convert the incoming messages to the `ModelMessage` format using the [`convertToModelMessages`](/docs/reference/ai-sdk-ui/convert-to-model-messages) function. ```tsx highlight="9" import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText } from 'ai'; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } ``` --- File: /ai/content/docs/09-troubleshooting/11-use-chat-custom-request-options.mdx --- --- title: Custom headers, body, and credentials not working with useChat description: Troubleshooting errors related to custom request configuration in useChat hook --- # Custom headers, body, and credentials not working with useChat ## Issue When using the `useChat` hook, custom request options like headers, body fields, and credentials configured directly on the hook are not being sent with the request: ```tsx // These options are not sent with the request const { messages, sendMessage } = useChat({ headers: { Authorization: 'Bearer token123', }, body: { user_id: '123', }, credentials: 'include', }); ``` ## Background The `useChat` hook has changed its API for configuring request options. Direct options like `headers`, `body`, and `credentials` on the hook itself are no longer supported. Instead, you need to use the `transport` configuration with `DefaultChatTransport` or pass options at the request level. ## Solution There are three ways to properly configure request options with `useChat`: ### Option 1: Request-Level Configuration (Recommended for Dynamic Values) For dynamic values that change over time, the recommended approach is to pass options when calling `sendMessage`: ```tsx const { messages, sendMessage } = useChat(); // Send options with each message sendMessage( { text: input }, { headers: { Authorization: `Bearer ${getAuthToken()}`, // Dynamic auth token 'X-Request-ID': generateRequestId(), }, body: { temperature: 0.7, max_tokens: 100, user_id: getCurrentUserId(), // Dynamic user ID sessionId: getCurrentSessionId(), // Dynamic session }, }, ); ``` This approach ensures that the most up-to-date values are always sent with each request. ### Option 2: Hook-Level Configuration with Static Values For static values that don't change during the component lifecycle, use the `DefaultChatTransport`: ```tsx import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', headers: { 'X-API-Version': 'v1', // Static API version 'X-App-ID': 'my-app', // Static app identifier }, body: { model: 'gpt-4o', // Default model stream: true, // Static configuration }, credentials: 'include', // Static credentials policy }), }); ``` ### Option 3: Hook-Level Configuration with Resolvable Functions If you need dynamic values at the hook level, you can use functions that return configuration values. However, request-level configuration is generally preferred for better reliability: ```tsx import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', headers: () => ({ Authorization: `Bearer ${getAuthToken()}`, 'X-User-ID': getCurrentUserId(), }), body: () => ({ sessionId: getCurrentSessionId(), preferences: getUserPreferences(), }), credentials: () => (isAuthenticated() ? 'include' : 'same-origin'), }), }); ``` <Note> For component state that changes over time, request-level configuration (Option 1) is recommended. If using hook-level functions, consider using `useRef` to store current values and reference `ref.current` in your configuration function. </Note> ### Combining Hook and Request Level Options Request-level options take precedence over hook-level options: ```tsx // Hook-level default configuration const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', headers: { 'X-API-Version': 'v1', }, body: { model: 'gpt-4o', }, }), }); // Override or add options per request sendMessage( { text: input }, { headers: { 'X-API-Version': 'v2', // This overrides the hook-level header 'X-Request-ID': '123', // This is added }, body: { model: 'gpt-4o-mini', // This overrides the hook-level body field temperature: 0.5, // This is added }, }, ); ``` For more details on request configuration, see the [Request Configuration](/docs/ai-sdk-ui/chatbot#request-configuration) documentation. --- File: /ai/content/docs/09-troubleshooting/12-use-chat-an-error-occurred.mdx --- --- title: useChat "An error occurred" description: Troubleshooting errors related to the "An error occurred" error in useChat. --- # `useChat` "An error occurred" ## Issue I am using [`useChat`](/docs/reference/ai-sdk-ui/use-chat) and I get the error "An error occurred". ## Background Error messages from `streamText` are masked by default when using `toDataStreamResponse` for security reasons (secure-by-default). This prevents leaking sensitive information to the client. ## Solution To forward error details to the client or to log errors, use the `getErrorMessage` function when calling `toDataStreamResponse`. ```tsx export function errorHandler(error: unknown) { if (error == null) { return 'unknown error'; } if (typeof error === 'string') { return error; } if (error instanceof Error) { return error.message; } return JSON.stringify(error); } ``` ```tsx const result = streamText({ // ... }); return result.toUIMessageStreamResponse({ getErrorMessage: errorHandler, }); ``` In case you are using `createDataStreamResponse`, you can use the `onError` function when calling `toDataStreamResponse`: ```tsx const response = createDataStreamResponse({ // ... async execute(dataStream) { // ... }, onError: errorHandler, }); ``` --- File: /ai/content/docs/09-troubleshooting/13-repeated-assistant-messages.mdx --- --- title: Repeated assistant messages in useChat description: Troubleshooting duplicate assistant messages when using useChat with streamText --- # Repeated assistant messages in useChat ## Issue When using `useChat` with `streamText` on the server, the assistant's messages appear duplicated in the UI - showing both the previous message and the new message, or showing the same message multiple times. This can occur when using tool calls or complex message flows. ```tsx // Server-side code that may experience assistant message duplication on the client export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4o-mini'), messages: convertToModelMessages(messages), tools: { weather: { description: 'Get the weather for a location', parameters: z.object({ location: z.string(), }), execute: async ({ location }) => { return { temperature: 72, condition: 'sunny' }; }, }, }, }); return result.toUIMessageStreamResponse(); } ``` ## Background The duplication occurs because `toUIMessageStreamResponse` generates new message IDs for each new message. ## Solution Pass the original messages array to `toUIMessageStreamResponse` using the `originalMessages` option. By passing `originalMessages`, the method can reuse existing message IDs instead of generating new ones, ensuring the client properly updates existing messages rather than creating duplicates. ```tsx export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4o-mini'), messages: convertToModelMessages(messages), tools: { weather: { description: 'Get the weather for a location', parameters: z.object({ location: z.string(), }), execute: async ({ location }) => { return { temperature: 72, condition: 'sunny' }; }, }, }, }); return result.toUIMessageStreamResponse({ originalMessages: messages, // Pass the original messages here generateMessageId: generateId, onFinish: ({ messages }) => { saveChat({ id, messages }); }, }); } ``` --- File: /ai/content/docs/09-troubleshooting/14-stream-abort-handling.mdx --- --- title: onFinish not called when stream is aborted description: Troubleshooting onFinish callback not executing when streams are aborted with toUIMessageStreamResponse --- # onFinish not called when stream is aborted ## Issue When using `toUIMessageStreamResponse` with an `onFinish` callback, the callback may not execute when the stream is aborted. This happens because the abort handler immediately terminates the response, preventing the `onFinish` callback from being triggered. ```tsx // Server-side code where onFinish isn't called on abort export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), abortSignal: req.signal, }); return result.toUIMessageStreamResponse({ onFinish: async ({ isAborted }) => { // This isn't called when the stream is aborted! if (isAborted) { console.log('Stream was aborted'); // Handle abort-specific cleanup } else { console.log('Stream completed normally'); // Handle normal completion } }, }); } ``` ## Background When a stream is aborted, the response is immediately terminated. Without proper handling, the `onFinish` callback has no chance to execute, preventing important cleanup operations like saving partial results or logging abort events. ## Solution Add `consumeStream` to the `toUIMessageStreamResponse` configuration. This ensures that abort events are properly captured and forwarded to the `onFinish` callback, allowing it to execute even when the stream is aborted. ```tsx // other imports... import { consumeStream } from 'ai'; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), abortSignal: req.signal, }); return result.toUIMessageStreamResponse({ onFinish: async ({ isAborted }) => { // Now this WILL be called even when aborted! if (isAborted) { console.log('Stream was aborted'); // Handle abort-specific cleanup } else { console.log('Stream completed normally'); // Handle normal completion } }, consumeSseStream: consumeStream, // This enables onFinish to be called on abort }); } ``` --- File: /ai/content/docs/09-troubleshooting/15-stream-text-not-working.mdx --- --- title: streamText fails silently description: Troubleshooting errors related to the streamText function not working. --- # `streamText` is not working ## Issue I am using [`streamText`](/docs/reference/ai-sdk-core/stream-text) function, and it does not work. It does not throw any errors and the stream is only containing error parts. ## Background `streamText` immediately starts streaming to enable sending data without waiting for the model. Errors become part of the stream and are not thrown to prevent e.g. servers from crashing. ## Solution To log errors, you can provide an `onError` callback that is triggered when an error occurs. ```tsx highlight="6-8" import { streamText } from 'ai'; const result = streamText({ model: 'openai/gpt-4.1', prompt: 'Invent a new holiday and describe its traditions.', onError({ error }) { console.error(error); // your error logging logic here }, }); ``` --- File: /ai/content/docs/09-troubleshooting/16-streaming-status-delay.mdx --- --- title: Streaming Status Shows But No Text Appears description: Why useChat shows "streaming" status without any visible content --- # Streaming Status Shows But No Text Appears ## Issue When using `useChat`, the status changes to "streaming" immediately, but no text appears for several seconds. ## Background The status changes to "streaming" as soon as the connection to the server is established and streaming begins - this includes metadata streaming, not just the LLM's generated tokens. ## Solution Create a custom loading state that checks if the last assistant message actually contains content: ```tsx 'use client'; import { useChat } from '@ai-sdk/react'; export default function Page() { const { messages, status } = useChat(); const lastMessage = messages.at(-1); const showLoader = status === 'streaming' && lastMessage?.role === 'assistant' && lastMessage?.parts?.length === 0; return ( <> {messages.map(message => ( <div key={message.id}> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, index) => part.type === 'text' ? <span key={index}>{part.text}</span> : null, )} </div> ))} {showLoader && <div>Loading...</div>} </> ); } ``` You can also check for specific part types if you're waiting for something specific: ```tsx const showLoader = status === 'streaming' && lastMessage?.role === 'assistant' && !lastMessage?.parts?.some(part => part.type === 'text'); ``` ## Related Issues - [GitHub Issue #7586](https://github.com/vercel/ai/issues/7586) --- File: /ai/content/docs/09-troubleshooting/30-model-is-not-assignable-to-type.mdx --- --- title: Model is not assignable to type "LanguageModelV1" description: Troubleshooting errors related to incompatible models. --- # Model is not assignable to type "LanguageModelV1" ## Issue I have updated the AI SDK and now I get the following error: `Type 'SomeModel' is not assignable to type 'LanguageModelV1'.` <Note>Similar errors can occur with `EmbeddingModelV2` as well.</Note> ## Background Sometimes new features are being added to the model specification. This can cause incompatibilities with older provider versions. ## Solution Update your provider packages and the AI SDK to the latest version. --- File: /ai/content/docs/09-troubleshooting/40-typescript-cannot-find-namespace-jsx.mdx --- --- title: TypeScript error "Cannot find namespace 'JSX'" description: Troubleshooting errors related to TypeScript and JSX. --- # TypeScript error "Cannot find namespace 'JSX'" ## Issue I am using the AI SDK in a project without React, e.g. an Hono server, and I get the following error: `error TS2503: Cannot find namespace 'JSX'.` ## Background The AI SDK has a dependency on `@types/react` which defines the `JSX` namespace. It will be removed in the next major version of the AI SDK. ## Solution You can install the `@types/react` package as a dependency to fix the error. ```bash npm install @types/react ``` --- File: /ai/content/docs/09-troubleshooting/50-react-maximum-update-depth-exceeded.mdx --- --- title: React error "Maximum update depth exceeded" description: Troubleshooting errors related to the "Maximum update depth exceeded" error. --- # React error "Maximum update depth exceeded" ## Issue I am using the AI SDK in a React project with the `useChat` or `useCompletion` hooks and I get the following error when AI responses stream in: `Maximum update depth exceeded`. ## Background By default, the UI is re-rendered on every chunk that arrives. This can overload the rendering, especially on slower devices or when complex components need updating (e.g. Markdown). Throttling can mitigate this. ## Solution Use the `experimental_throttle` option to throttle the UI updates: ### `useChat` ```tsx filename="page.tsx" highlight="2-3" const { messages, ... } = useChat({ // Throttle the messages and data updates to 50ms: experimental_throttle: 50 }) ``` ### `useCompletion` ```tsx filename="page.tsx" highlight="2-3" const { completion, ... } = useCompletion({ // Throttle the completion and data updates to 50ms: experimental_throttle: 50 }) ``` --- File: /ai/content/docs/09-troubleshooting/60-jest-cannot-find-module-ai-rsc.mdx --- --- title: "Jest: cannot find module '@ai-sdk/rsc'" description: "Troubleshooting AI SDK errors related to the Jest: cannot find module '@ai-sdk/rsc' error" --- # Jest: cannot find module '@ai-sdk/rsc' ## Issue I am using AI SDK RSC and am writing tests for my RSC components with Jest. I am getting the following error: `Cannot find module '@ai-sdk/rsc'`. ## Solution Configure the module resolution via `jest config update` in `moduleNameMapper`: ```json filename="jest.config.js" "moduleNameMapper": { "^@ai-sdk/rsc$": "<rootDir>/node_modules/@ai-sdk/rsc/dist" } ``` --- File: /ai/content/docs/09-troubleshooting/index.mdx --- --- title: Troubleshooting description: Troubleshooting information for common issues encountered with the AI SDK. collapsed: true --- # Troubleshooting This section is designed to help you quickly identify and resolve common issues encountered with the AI SDK, ensuring a smoother and more efficient development experience. <Support /> --- File: /ai/content/providers/01-ai-sdk-providers/00-ai-gateway.mdx --- --- title: AI Gateway description: Learn how to use the AI Gateway provider with the AI SDK. --- # AI Gateway Provider The [AI Gateway](https://vercel.com/docs/ai-gateway) provider connects you to models from multiple AI providers through a single interface. Instead of integrating with each provider separately, you can access OpenAI, Anthropic, Google, Meta, xAI, and other providers and their models. ## Features - Access models from multiple providers without having to install additional provider modules/dependencies - Use the same code structure across different AI providers - Switch between models and providers easily - Automatic authentication when deployed on Vercel - View pricing information across providers - Observability for AI model usage through the Vercel dashboard ## Setup The AI Gateway provider is available in the `@ai-sdk/gateway` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/gateway" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/gateway" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/gateway" dark /> </Tab> </Tabs> ## Basic Usage For most use cases, you can use the AI Gateway directly with a model string: ```ts // use plain model string with global provider import { generateText } from 'ai'; const { text } = await generateText({ model: 'openai/gpt-4o', prompt: 'Hello world', }); ``` ```ts // use provider instance import { generateText } from 'ai'; import { gateway } from '@ai-sdk/gateway'; const { text } = await generateText({ model: gateway('openai/gpt-4o'), prompt: 'Hello world', }); ``` The AI SDK automatically uses the AI Gateway when you pass a model string in the `creator/model-name` format. ## Provider Instance You can also import the default provider instance `gateway` from `@ai-sdk/gateway`: ```ts import { gateway } from '@ai-sdk/gateway'; ``` You may want to create a custom provider instance when you need to: - Set custom configuration options (API key, base URL, headers) - Use the provider in a [provider registry](/docs/ai-sdk-core/provider-registry) - Wrap the provider with [middleware](/docs/ai-sdk-core/middleware) - Use different settings for different parts of your application To create a custom provider instance, import `createGateway` from `@ai-sdk/gateway`: ```ts import { createGateway } from '@ai-sdk/gateway'; const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '', }); ``` You can use the following optional settings to customize the AI Gateway provider instance: - **baseURL** _string_ Use a different URL prefix for API calls. The default prefix is `https://ai-gateway.vercel.sh/v1/ai`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `AI_GATEWAY_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. - **metadataCacheRefreshMillis** _number_ How frequently to refresh the metadata cache in milliseconds. Defaults to 5 minutes (300,000ms). ## Authentication The Gateway provider supports two authentication methods: ### API Key Authentication Set your API key via environment variable: ```bash AI_GATEWAY_API_KEY=your_api_key_here ``` Or pass it directly to the provider: ```ts import { createGateway } from '@ai-sdk/gateway'; const gateway = createGateway({ apiKey: 'your_api_key_here', }); ``` ### OIDC Authentication (Vercel Deployments) When deployed to Vercel, the AI Gateway provider supports authenticating using [OIDC (OpenID Connect) tokens](https://vercel.com/docs/oidc) without API Keys. #### How OIDC Authentication Works 1. **In Production/Preview Deployments**: - OIDC authentication is automatically handled - No manual configuration needed - Tokens are automatically obtained and refreshed 2. **In Local Development**: - First, install and authenticate with the [Vercel CLI](https://vercel.com/docs/cli) - Run `vercel env pull` to download your project's OIDC token locally - For automatic token management: - Use `vercel dev` to start your development server - this will handle token refreshing automatically - For manual token management: - If not using `vercel dev`, note that OIDC tokens expire after 12 hours - You'll need to run `vercel env pull` again to refresh the token before it expires <Note> If an API Key is present (either passed directly or via environment), it will always be used, even if invalid. </Note> Read more about using OIDC tokens in the [Vercel AI Gateway docs](https://vercel.com/docs/ai-gateway#using-the-ai-gateway-with-a-vercel-oidc-token). ## Language Models You can create language models using a provider instance. The first argument is the model ID in the format `creator/model-name`: ```ts import { gateway } from '@ai-sdk/gateway'; import { generateText } from 'ai'; const { text } = await generateText({ model: gateway('openai/gpt-4o'), prompt: 'Explain quantum computing in simple terms', }); ``` AI Gateway language models can also be used in the `streamText`, `generateObject`, and `streamObject` functions (see [AI SDK Core](/docs/ai-sdk-core)). ## Available Models The AI Gateway supports models from OpenAI, Anthropic, Google, Meta, xAI, Mistral, DeepSeek, Amazon Bedrock, Cohere, Perplexity, Alibaba, and other providers. For the complete list of available models, see the [AI Gateway documentation](https://vercel.com/docs/ai-gateway). ## Dynamic Model Discovery You can discover available models programmatically: ```ts import { gateway } from '@ai-sdk/gateway'; import { generateText } from 'ai'; const availableModels = await gateway.getAvailableModels(); // List all available models availableModels.models.forEach(model => { console.log(`${model.id}: ${model.name}`); if (model.description) { console.log(` Description: ${model.description}`); } if (model.pricing) { console.log(` Input: $${model.pricing.input}/token`); console.log(` Output: $${model.pricing.output}/token`); } }); // Use any discovered model with plain string const { text } = await generateText({ model: availableModels.models[0].id, // e.g., 'openai/gpt-4o' prompt: 'Hello world', }); ``` ## Examples ### Basic Text Generation ```ts import { gateway } from '@ai-sdk/gateway'; import { generateText } from 'ai'; const { text } = await generateText({ model: gateway('anthropic/claude-3.5-sonnet'), prompt: 'Write a haiku about programming', }); console.log(text); ``` ### Streaming ```ts import { gateway } from '@ai-sdk/gateway'; import { streamText } from 'ai'; const { textStream } = await streamText({ model: gateway('openai/gpt-4o'), prompt: 'Explain the benefits of serverless architecture', }); for await (const textPart of textStream) { process.stdout.write(textPart); } ``` ### Tool Usage ```ts import { gateway } from '@ai-sdk/gateway'; import { generateText, tool } from 'ai'; import { z } from 'zod'; const { text } = await generateText({ model: gateway('meta/llama-3.3-70b'), prompt: 'What is the weather like in San Francisco?', tools: { getWeather: tool({ description: 'Get the current weather for a location', parameters: z.object({ location: z.string().describe('The location to get weather for'), }), execute: async ({ location }) => { // Your weather API call here return `It's sunny in ${location}`; }, }), }, }); ``` ## Provider Options When using provider-specific options, use the actual provider name (e.g. `anthropic` not `gateway`) as the key: ```ts // with model string import { generateText } from 'ai'; const { text } = await generateText({ model: 'anthropic/claude-3-5-sonnet', prompt: 'Explain quantum computing', providerOptions: { anthropic: { thinking: { type: 'enabled', budgetTokens: 12000 }, }, }, }); ``` ```ts // with provider instance import { generateText } from 'ai'; import { gateway } from '@ai-sdk/gateway'; const { text } = await generateText({ model: gateway('anthropic/claude-3-5-sonnet'), prompt: 'Explain quantum computing', providerOptions: { anthropic: { thinking: { type: 'enabled', budgetTokens: 12000 }, }, }, }); ``` The AI Gateway provider also accepts its own set of options. Refer to the [AI Gateway provider options documentation](https://vercel.com/docs/ai-gateway/provider-options). ## Model Capabilities Model capabilities depend on the specific provider and model you're using. For detailed capability information, see: - [AI Gateway provider options](https://vercel.com/docs/ai-gateway/provider-options#available-providers) for an overview of available providers - Individual [AI SDK provider pages](/providers/ai-sdk-providers) for specific model capabilities and features --- File: /ai/content/providers/01-ai-sdk-providers/01-xai.mdx --- --- title: xAI Grok description: Learn how to use xAI Grok. --- # xAI Grok Provider The [xAI Grok](https://x.ai) provider contains language model support for the [xAI API](https://x.ai/api). ## Setup The xAI Grok provider is available via the `@ai-sdk/xai` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/xai" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/xai" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/xai" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `xai` from `@ai-sdk/xai`: ```ts import { xai } from '@ai-sdk/xai'; ``` If you need a customized setup, you can import `createXai` from `@ai-sdk/xai` and create a provider instance with your settings: ```ts import { createXai } from '@ai-sdk/xai'; const xai = createXai({ apiKey: 'your-api-key', }); ``` You can use the following optional settings to customize the xAI provider instance: - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.x.ai/v1`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `XAI_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Language Models You can create [xAI models](https://console.x.ai) using a provider instance. The first argument is the model id, e.g. `grok-3`. ```ts const model = xai('grok-3'); ``` ### Example You can use xAI language models to generate text with the `generateText` function: ```ts import { xai } from '@ai-sdk/xai'; import { generateText } from 'ai'; const { text } = await generateText({ model: xai('grok-3'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` xAI language models can also be used in the `streamText`, `generateObject`, and `streamObject` functions (see [AI SDK Core](/docs/ai-sdk-core)). ### Provider Options xAI chat models support additional provider options that are not part of the [standard call settings](/docs/ai-sdk-core/settings). You can pass them in the `providerOptions` argument: ```ts const model = xai('grok-3-mini'); await generateText({ model, providerOptions: { xai: { reasoningEffort: 'high', }, }, }); ``` The following optional provider options are available for xAI chat models: - **reasoningEffort** _'low' | 'high'_ Reasoning effort for reasoning models. Only supported by `grok-3-mini` and `grok-3-mini-fast` models. ## Live Search xAI models support Live Search functionality, allowing them to query real-time data from various sources and include it in responses with citations. ### Basic Search To enable search, specify `searchParameters` with a search mode: ```ts import { xai } from '@ai-sdk/xai'; import { generateText } from 'ai'; const { text, sources } = await generateText({ model: xai('grok-3-latest'), prompt: 'What are the latest developments in AI?', providerOptions: { xai: { searchParameters: { mode: 'auto', // 'auto', 'on', or 'off' returnCitations: true, maxSearchResults: 5, }, }, }, }); console.log(text); console.log('Sources:', sources); ``` ### Search Parameters The following search parameters are available: - **mode** _'auto' | 'on' | 'off'_ Search mode preference: - `'auto'` (default): Model decides whether to search - `'on'`: Always enables search - `'off'`: Disables search completely - **returnCitations** _boolean_ Whether to return citations in the response. Defaults to `true`. - **fromDate** _string_ Start date for search data in ISO8601 format (`YYYY-MM-DD`). - **toDate** _string_ End date for search data in ISO8601 format (`YYYY-MM-DD`). - **maxSearchResults** _number_ Maximum number of search results to consider. Defaults to 20, max 50. - **sources** _Array&lt;SearchSource&gt;_ Data sources to search from. Defaults to `["web", "x"]` if not specified. ### Search Sources You can specify different types of data sources for search: #### Web Search ```ts const result = await generateText({ model: xai('grok-3-latest'), prompt: 'Best ski resorts in Switzerland', providerOptions: { xai: { searchParameters: { mode: 'on', sources: [ { type: 'web', country: 'CH', // ISO alpha-2 country code allowedWebsites: ['ski.com', 'snow-forecast.com'], safeSearch: true, }, ], }, }, }, }); ``` #### Web source parameters - **country** _string_: ISO alpha-2 country code - **allowedWebsites** _string[]_: Max 5 allowed websites - **excludedWebsites** _string[]_: Max 5 excluded websites - **safeSearch** _boolean_: Enable safe search (default: true) #### X (Twitter) Search ```ts const result = await generateText({ model: xai('grok-3-latest'), prompt: 'Latest updates on Grok AI', providerOptions: { xai: { searchParameters: { mode: 'on', sources: [ { type: 'x', xHandles: ['grok', 'xai'], }, ], }, }, }, }); ``` #### X source parameters - **xHandles** _string[]_: Array of X handles to search (without @ symbol) #### News Search ```ts const result = await generateText({ model: xai('grok-3-latest'), prompt: 'Recent tech industry news', providerOptions: { xai: { searchParameters: { mode: 'on', sources: [ { type: 'news', country: 'US', excludedWebsites: ['tabloid.com'], safeSearch: true, }, ], }, }, }, }); ``` #### News source parameters - **country** _string_: ISO alpha-2 country code - **excludedWebsites** _string[]_: Max 5 excluded websites - **safeSearch** _boolean_: Enable safe search (default: true) #### RSS Feed Search ```ts const result = await generateText({ model: xai('grok-3-latest'), prompt: 'Latest status updates', providerOptions: { xai: { searchParameters: { mode: 'on', sources: [ { type: 'rss', links: ['https://status.x.ai/feed.xml'], }, ], }, }, }, }); ``` #### RSS source parameters - **links** _string[]_: Array of RSS feed URLs (max 1 currently supported) ### Multiple Sources You can combine multiple data sources in a single search: ```ts const result = await generateText({ model: xai('grok-3-latest'), prompt: 'Comprehensive overview of recent AI breakthroughs', providerOptions: { xai: { searchParameters: { mode: 'on', returnCitations: true, maxSearchResults: 15, sources: [ { type: 'web', allowedWebsites: ['arxiv.org', 'openai.com'], }, { type: 'news', country: 'US', }, { type: 'x', xHandles: ['openai', 'deepmind'], }, ], }, }, }, }); ``` ### Sources and Citations When search is enabled with `returnCitations: true`, the response includes sources that were used to generate the answer: ```ts const { text, sources } = await generateText({ model: xai('grok-3-latest'), prompt: 'What are the latest developments in AI?', providerOptions: { xai: { searchParameters: { mode: 'auto', returnCitations: true, }, }, }, }); // Access the sources used for (const source of sources) { if (source.sourceType === 'url') { console.log('Source:', source.url); } } ``` ### Streaming with Search Live Search works with streaming responses. Citations are included when the stream completes: ```ts import { streamText } from 'ai'; const result = streamText({ model: xai('grok-3-latest'), prompt: 'What has happened in tech recently?', providerOptions: { xai: { searchParameters: { mode: 'auto', returnCitations: true, }, }, }, }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log('Sources:', await result.sources); ``` ## Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | Reasoning | | ------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `grok-4` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `grok-3` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `grok-3-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `grok-3-fast` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `grok-3-fast-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `grok-3-mini` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `grok-3-mini-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `grok-3-mini-fast` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `grok-3-mini-fast-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `grok-2` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `grok-2-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `grok-2-1212` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `grok-2-vision` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `grok-2-vision-latest` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `grok-2-vision-1212` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `grok-beta` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `grok-vision-beta` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Note> The table above lists popular models. Please see the [xAI docs](https://docs.x.ai/docs#models) for a full list of available models. The table above lists popular models. You can also pass any available provider model ID as a string if needed. </Note> ## Image Models You can create xAI image models using the `.image()` factory method. For more on image generation with the AI SDK see [generateImage()](/docs/reference/ai-sdk-core/generate-image). ```ts import { xai } from '@ai-sdk/xai'; import { experimental_generateImage as generateImage } from 'ai'; const { image } = await generateImage({ model: xai.image('grok-2-image'), prompt: 'A futuristic cityscape at sunset', }); ``` <Note> The xAI image model does not currently support the `aspectRatio` or `size` parameters. Image size defaults to 1024x768. </Note> ### Model-specific options You can customize the image generation behavior with model-specific settings: ```ts import { xai } from '@ai-sdk/xai'; import { experimental_generateImage as generateImage } from 'ai'; const { images } = await generateImage({ model: xai.image('grok-2-image'), prompt: 'A futuristic cityscape at sunset', maxImagesPerCall: 5, // Default is 10 n: 2, // Generate 2 images }); ``` ### Model Capabilities | Model | Sizes | Notes | | -------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `grok-2-image` | 1024x768 (default) | xAI's text-to-image generation model, designed to create high-quality images from text prompts. It's trained on a diverse dataset and can generate images across various styles, subjects, and settings. | --- File: /ai/content/providers/01-ai-sdk-providers/02-vercel.mdx --- --- title: Vercel description: Learn how to use Vercel's v0 models with the AI SDK. --- # Vercel Provider The [Vercel](https://vercel.com) provider gives you access to the [v0 API](https://vercel.com/docs/v0/api), designed for building modern web applications. The v0 models support text and image inputs and provide fast streaming responses. You can create your Vercel API key at [v0.dev](https://v0.dev/chat/settings/keys). <Note> The v0 API is currently in beta and requires a Premium or Team plan with usage-based billing enabled. For details, visit the [pricing page](https://v0.dev/pricing). To request a higher limit, contact Vercel at support@v0.dev. </Note> ## Features - **Framework aware completions**: Evaluated on modern stacks like Next.js and Vercel - **Auto-fix**: Identifies and corrects common coding issues during generation - **Quick edit**: Streams inline edits as they're available - **Multimodal**: Supports both text and image inputs ## Setup The Vercel provider is available via the `@ai-sdk/vercel` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/vercel" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/vercel" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/vercel" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `vercel` from `@ai-sdk/vercel`: ```ts import { vercel } from '@ai-sdk/vercel'; ``` If you need a customized setup, you can import `createVercel` from `@ai-sdk/vercel` and create a provider instance with your settings: ```ts import { createVercel } from '@ai-sdk/vercel'; const vercel = createVercel({ apiKey: process.env.VERCEL_API_KEY ?? '', }); ``` You can use the following optional settings to customize the Vercel provider instance: - **baseURL** _string_ Use a different URL prefix for API calls. The default prefix is `https://api.v0.dev/v1`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `VERCEL_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Language Models You can create language models using a provider instance. The first argument is the model ID, for example: ```ts import { vercel } from '@ai-sdk/vercel'; import { generateText } from 'ai'; const { text } = await generateText({ model: vercel('v0-1.0-md'), prompt: 'Create a Next.js AI chatbot', }); ``` Vercel language models can also be used in the `streamText` function (see [AI SDK Core](/docs/ai-sdk-core)). ## Models ### v0-1.5-md The `v0-1.5-md` model is for everyday tasks and UI generation. ### v0-1.5-lg The `v0-1.5-lg` model is for advanced thinking or reasoning. ### v0-1.0-md (legacy) The `v0-1.0-md` model is the legacy model served by the v0 API. All v0 models have the following capabilities: - Supports text and image inputs (multimodal) - Supports function/tool calls - Streaming responses with low latency - Optimized for frontend and full-stack web development ## Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | ----------- | ------------------- | ------------------- | ------------------- | ------------------- | | `v0-1.5-md` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `v0-1.5-lg` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `v0-1.0-md` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | --- File: /ai/content/providers/01-ai-sdk-providers/03-openai.mdx --- --- title: OpenAI description: Learn how to use the OpenAI provider for the AI SDK. --- # OpenAI Provider The [OpenAI](https://openai.com/) provider contains language model support for the OpenAI responses, chat, and completion APIs, as well as embedding model support for the OpenAI embeddings API. ## Setup The OpenAI provider is available in the `@ai-sdk/openai` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/openai" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/openai" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/openai" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `openai` from `@ai-sdk/openai`: ```ts import { openai } from '@ai-sdk/openai'; ``` If you need a customized setup, you can import `createOpenAI` from `@ai-sdk/openai` and create a provider instance with your settings: ```ts import { createOpenAI } from '@ai-sdk/openai'; const openai = createOpenAI({ // custom settings, e.g. headers: { 'header-name': 'header-value', }, }); ``` You can use the following optional settings to customize the OpenAI provider instance: - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.openai.com/v1`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `OPENAI_API_KEY` environment variable. - **name** _string_ The provider name. You can set this when using OpenAI compatible providers to change the model provider property. Defaults to `openai`. - **organization** _string_ OpenAI Organization. - **project** _string_ OpenAI project. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Language Models The OpenAI provider instance is a function that you can invoke to create a language model: ```ts const model = openai('gpt-4.1'); ``` It automatically selects the correct API based on the model id. You can also pass additional settings in the second argument: ```ts const model = openai('gpt-4.1', { // additional settings }); ``` The available options depend on the API that's automatically chosen for the model (see below). If you want to explicitly select a specific model API, you can use `.chat` or `.completion`. ### Example You can use OpenAI language models to generate text with the `generateText` function: ```ts import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; const { text } = await generateText({ model: openai('gpt-4.1'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` OpenAI language models can also be used in the `streamText`, `generateObject`, and `streamObject` functions (see [AI SDK Core](/docs/ai-sdk-core)). ### Chat Models You can create models that call the [OpenAI chat API](https://platform.openai.com/docs/api-reference/chat) using the `.chat()` factory method. The first argument is the model id, e.g. `gpt-4`. The OpenAI chat models support tool calls and some have multi-modal capabilities. ```ts const model = openai.chat('gpt-3.5-turbo'); ``` OpenAI chat models support also some model specific provider options that are not part of the [standard call settings](/docs/ai-sdk-core/settings). You can pass them in the `providerOptions` argument: ```ts const model = openai.chat('gpt-3.5-turbo'); await generateText({ model, providerOptions: { openai: { logitBias: { // optional likelihood for specific tokens '50256': -100, }, user: 'test-user', // optional unique user identifier }, }, }); ``` The following optional provider options are available for OpenAI chat models: - **logitBias** _Record&lt;number, number&gt;_ Modifies the likelihood of specified tokens appearing in the completion. Accepts a JSON object that maps tokens (specified by their token ID in the GPT tokenizer) to an associated bias value from -100 to 100. You can use this tokenizer tool to convert text to token IDs. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token. As an example, you can pass `{"50256": -100}` to prevent the token from being generated. - **logprobs** _boolean | number_ Return the log probabilities of the tokens. Including logprobs will increase the response size and can slow down response times. However, it can be useful to better understand how the model is behaving. Setting to true will return the log probabilities of the tokens that were generated. Setting to a number will return the log probabilities of the top n tokens that were generated. - **parallelToolCalls** _boolean_ Whether to enable parallel function calling during tool use. Defaults to `true`. - **user** _string_ A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). - **reasoningEffort** _'low' | 'medium' | 'high'_ Reasoning effort for reasoning models. Defaults to `medium`. If you use `providerOptions` to set the `reasoningEffort` option, this model setting will be ignored. - **structuredOutputs** _boolean_ Whether to use [structured outputs](#structured-outputs). Defaults to `true`. When enabled, tool calls and object generation will be strict and follow the provided schema. - **maxCompletionTokens** _number_ Maximum number of completion tokens to generate. Useful for reasoning models. - **store** _boolean_ Whether to enable persistence in Responses API. - **metadata** _Record&lt;string, string&gt;_ Metadata to associate with the request. - **prediction** _Record&lt;string, any&gt;_ Parameters for prediction mode. - **serviceTier** _'auto' | 'flex'_ Service tier for the request. Set to 'flex' for 50% cheaper processing at the cost of increased latency. Only available for o3 and o4-mini models. Defaults to 'auto'. - **strictJsonSchema** _boolean_ Whether to use strict JSON schema validation. Defaults to `false`. #### Reasoning OpenAI has introduced the `o1`,`o3`, and `o4` series of [reasoning models](https://platform.openai.com/docs/guides/reasoning). Currently, `o4-mini`, `o3`, `o3-mini`, `o1`, `o1-mini`, and `o1-preview` are available via both the chat and responses APIs. The models `codex-mini-latest` and `computer-use-preview` are available only via the [responses API](#responses-models). Reasoning models currently only generate text, have several limitations, and are only supported using `generateText` and `streamText`. They support additional settings and response metadata: - You can use `providerOptions` to set - the `reasoningEffort` option (or alternatively the `reasoningEffort` model setting), which determines the amount of reasoning the model performs. - You can use response `providerMetadata` to access the number of reasoning tokens that the model generated. ```ts highlight="4,7-11,17" import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; const { text, usage, providerMetadata } = await generateText({ model: openai('o3-mini'), prompt: 'Invent a new holiday and describe its traditions.', providerOptions: { openai: { reasoningEffort: 'low', }, }, }); console.log(text); console.log('Usage:', { ...usage, reasoningTokens: providerMetadata?.openai?.reasoningTokens, }); ``` <Note> System messages are automatically converted to OpenAI developer messages for reasoning models when supported. For models that do not support developer messages, such as `o1-preview`, system messages are removed and a warning is added. </Note> <Note> Reasoning models like `o1-mini` and `o1-preview` require additional runtime inference to complete their reasoning phase before generating a response. This introduces longer latency compared to other models, with `o1-preview` exhibiting significantly more inference time than `o1-mini`. </Note> <Note> `maxOutputTokens` is automatically mapped to `max_completion_tokens` for reasoning models. </Note> #### Structured Outputs Structured outputs are enabled by default. You can disable them by setting the `structuredOutputs` option to `false`. ```ts highlight="7" import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import { z } from 'zod'; const result = await generateObject({ model: openai('gpt-4o-2024-08-06'), providerOptions: { openai: { structuredOutputs: false, }, }, schemaName: 'recipe', schemaDescription: 'A recipe for lasagna.', schema: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object, null, 2)); ``` <Note type="warning"> OpenAI structured outputs have several [limitations](https://openai.com/index/introducing-structured-outputs-in-the-api), in particular around the [supported schemas](https://platform.openai.com/docs/guides/structured-outputs/supported-schemas), and are therefore opt-in. For example, optional schema properties are not supported. You need to change Zod `.nullish()` and `.optional()` to `.nullable()`. </Note> #### Logprobs OpenAI provides logprobs information for completion/chat models. You can access it in the `providerMetadata` object. ```ts highlight="11" import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; const result = await generateText({ model: openai('gpt-4o'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', providerOptions: { openai: { // this can also be a number, // refer to logprobs provider options section for more logprobs: true, }, }, }); const openaiMetadata = (await result.providerMetadata)?.openai; const logprobs = openaiMetadata?.logprobs; ``` #### Image Support The OpenAI Chat API supports Image inputs for appropriate models. You can pass Image files as part of the message content using the 'image' type: ```ts const result = await generateText({ model: openai('gpt-4o'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Please describe the image.', }, { type: 'image', image: fs.readFileSync('./data/image.png'), }, ], }, ], }); ``` Multimodal models will have access to the image and can analyze and conversate about it. The image should be passed using the `image` field. You can also pass the URL of an image. ```ts { type: 'image', image: 'https://sample.edu/image.png', } ``` #### PDF support The OpenAI Chat API supports reading PDF files. You can pass PDF files as part of the message content using the `file` type: ```ts const result = await generateText({ model: openai('gpt-4o'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', filename: 'ai.pdf', // optional }, ], }, ], }); ``` The model will have access to the contents of the PDF file and respond to questions about it. The PDF file should be passed using the `data` field, and the `mediaType` should be set to `'application/pdf'`. You can also pass a file-id from the OpenAI Files API. ```ts { type: 'file', data: 'file-8EFBcWHsQxZV7YGezBC1fq', mediaType: 'application/pdf', } ``` You can also pass the URL of a PDF. ```ts { type: 'file', data: 'https://sample.edu/example.pdf', mediaType: 'application/pdf', filename: 'ai.pdf', // optional } ``` #### Predicted Outputs OpenAI supports [predicted outputs](https://platform.openai.com/docs/guides/latency-optimization#use-predicted-outputs) for `gpt-4o` and `gpt-4o-mini`. Predicted outputs help you reduce latency by allowing you to specify a base text that the model should modify. You can enable predicted outputs by adding the `prediction` option to the `providerOptions.openai` object: ```ts highlight="15-18" const result = streamText({ model: openai('gpt-4o'), messages: [ { role: 'user', content: 'Replace the Username property with an Email property.', }, { role: 'user', content: existingCode, }, ], providerOptions: { openai: { prediction: { type: 'content', content: existingCode, }, }, }, }); ``` OpenAI provides usage information for predicted outputs (`acceptedPredictionTokens` and `rejectedPredictionTokens`). You can access it in the `providerMetadata` object. ```ts highlight="11" const openaiMetadata = (await result.providerMetadata)?.openai; const acceptedPredictionTokens = openaiMetadata?.acceptedPredictionTokens; const rejectedPredictionTokens = openaiMetadata?.rejectedPredictionTokens; ``` <Note type="warning"> OpenAI Predicted Outputs have several [limitations](https://platform.openai.com/docs/guides/predicted-outputs#limitations), e.g. unsupported API parameters and no tool calling support. </Note> #### Image Detail You can use the `openai` provider option to set the [image input detail](https://platform.openai.com/docs/guides/images-vision?api-mode=responses#specify-image-input-detail-level) to `high`, `low`, or `auto`: ```ts highlight="13-16" const result = await generateText({ model: openai('gpt-4o'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/comic-cat.png?raw=true', // OpenAI specific options - image detail: providerOptions: { openai: { imageDetail: 'low' }, }, }, ], }, ], }); ``` <Note type="warning"> Because the `UIMessage` type (used by AI SDK UI hooks like `useChat`) does not support the `providerOptions` property, you can use `convertToModelMessages` first before passing the messages to functions like `generateText` or `streamText`. For more details on `providerOptions` usage, see [here](/docs/foundations/prompts#provider-options). </Note> #### Distillation OpenAI supports model distillation for some models. If you want to store a generation for use in the distillation process, you can add the `store` option to the `providerOptions.openai` object. This will save the generation to the OpenAI platform for later use in distillation. ```typescript highlight="9-16" import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const { text, usage } = await generateText({ model: openai('gpt-4o-mini'), prompt: 'Who worked on the original macintosh?', providerOptions: { openai: { store: true, metadata: { custom: 'value', }, }, }, }); console.log(text); console.log(); console.log('Usage:', usage); } main().catch(console.error); ``` #### Prompt Caching OpenAI has introduced [Prompt Caching](https://platform.openai.com/docs/guides/prompt-caching) for supported models including `gpt-4o`, `gpt-4o-mini`, `o1-preview`, and `o1-mini`. - Prompt caching is automatically enabled for these models, when the prompt is 1024 tokens or longer. It does not need to be explicitly enabled. - You can use response `providerMetadata` to access the number of prompt tokens that were a cache hit. - Note that caching behavior is dependent on load on OpenAI's infrastructure. Prompt prefixes generally remain in the cache following 5-10 minutes of inactivity before they are evicted, but during off-peak periods they may persist for up to an hour. ```ts highlight="11" import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; const { text, usage, providerMetadata } = await generateText({ model: openai('gpt-4o-mini'), prompt: `A 1024-token or longer prompt...`, }); console.log(`usage:`, { ...usage, cachedPromptTokens: providerMetadata?.openai?.cachedPromptTokens, }); ``` #### Audio Input With the `gpt-4o-audio-preview` model, you can pass audio files to the model. <Note type="warning"> The `gpt-4o-audio-preview` model is currently in preview and requires at least some audio inputs. It will not work with non-audio data. </Note> ```ts highlight="12-14" import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; const result = await generateText({ model: openai('gpt-4o-audio-preview'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is the audio saying?' }, { type: 'file', mediaType: 'audio/mpeg', data: fs.readFileSync('./data/galileo.mp3'), }, ], }, ], }); ``` ### Responses Models You can use the OpenAI responses API with the `openai.responses(modelId)` factory method. ```ts const model = openai.responses('gpt-4o-mini'); ``` Further configuration can be done using OpenAI provider options. You can validate the provider options using the `OpenAIResponsesProviderOptions` type. ```ts import { openai, OpenAIResponsesProviderOptions } from '@ai-sdk/openai'; import { generateText } from 'ai'; const result = await generateText({ model: openai.responses('gpt-4o-mini'), providerOptions: { openai: { parallelToolCalls: false, store: false, user: 'user_123', // ... } satisfies OpenAIResponsesProviderOptions, }, // ... }); ``` The following provider options are available: - **parallelToolCalls** _boolean_ Whether to use parallel tool calls. Defaults to `true`. - **store** _boolean_ Whether to store the generation. Defaults to `true`. When using reasoning models (o1, o3, o4-mini) with multi-step tool calls and `store: false`, include `['reasoning.encrypted_content']` in the `include` option to ensure reasoning content is available across conversation steps. - **metadata** _Record&lt;string, string&gt;_ Additional metadata to store with the generation. - **previousResponseId** _string_ The ID of the previous response. You can use it to continue a conversation. Defaults to `undefined`. - **instructions** _string_ Instructions for the model. They can be used to change the system or developer message when continuing a conversation using the `previousResponseId` option. Defaults to `undefined`. - **user** _string_ A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. Defaults to `undefined`. - **reasoningEffort** _'low' | 'medium' | 'high'_ Reasoning effort for reasoning models. Defaults to `medium`. If you use `providerOptions` to set the `reasoningEffort` option, this model setting will be ignored. - **reasoningSummary** _'auto' | 'detailed'_ Controls whether the model returns its reasoning process. Set to `'auto'` for a condensed summary, `'detailed'` for more comprehensive reasoning. Defaults to `undefined` (no reasoning summaries). When enabled, reasoning summaries appear in the stream as events with type `'reasoning'` and in non-streaming responses within the `reasoning` field. - **strictJsonSchema** _boolean_ Whether to use strict JSON schema validation. Defaults to `false`. - **serviceTier** _'auto' | 'flex'_ Service tier for the request. Set to 'flex' for 50% cheaper processing at the cost of increased latency. Only available for o3 and o4-mini models. Defaults to 'auto'. - **include** _Array&lt;string&gt;_ Specifies additional content to include in the response. Supported values: `['reasoning.encrypted_content']` for accessing reasoning content across conversation steps, and `['file_search_call.results']` for including file search results in responses. Defaults to `undefined`. The OpenAI responses provider also returns provider-specific metadata: ```ts const { providerMetadata } = await generateText({ model: openai.responses('gpt-4o-mini'), }); const openaiMetadata = providerMetadata?.openai; ``` The following OpenAI-specific metadata is returned: - **responseId** _string_ The ID of the response. Can be used to continue a conversation. - **cachedPromptTokens** _number_ The number of prompt tokens that were a cache hit. - **reasoningTokens** _number_ The number of reasoning tokens that the model generated. #### Web Search The OpenAI responses provider supports web search through the `openai.tools.webSearchPreview` tool. You can force the use of the web search tool by setting the `toolChoice` parameter to `{ type: 'tool', toolName: 'web_search_preview' }`. ```ts const result = await generateText({ model: openai.responses('gpt-4o-mini'), prompt: 'What happened in San Francisco last week?', tools: { web_search_preview: openai.tools.webSearchPreview({ // optional configuration: searchContextSize: 'high', userLocation: { type: 'approximate', city: 'San Francisco', region: 'California', }, }), }, // Force web search tool: toolChoice: { type: 'tool', toolName: 'web_search_preview' }, }); // URL sources const sources = result.sources; ``` #### File Search The OpenAI responses provider supports file search through the `openai.tools.fileSearch` tool. You can force the use of the file search tool by setting the `toolChoice` parameter to `{ type: 'tool', toolName: 'file_search' }`. ```ts const result = await generateText({ model: openai.responses('gpt-4o-mini'), prompt: 'What does the document say about user authentication?', tools: { file_search: openai.tools.fileSearch({ // optional configuration: vectorStoreIds: ['vs_123', 'vs_456'], maxNumResults: 10, ranking: { ranker: 'auto', }, filters: { type: 'and', filters: [ { key: 'author', type: 'eq', value: 'John Doe' }, { key: 'date', type: 'gte', value: '2023-01-01' }, ], }, }), }, // Force file search tool: toolChoice: { type: 'tool', toolName: 'file_search' }, }); ``` <Note> The tool must be named `file_search` when using OpenAI's file search functionality. This name is required by OpenAI's API specification and cannot be customized. </Note> #### Reasoning Summaries For reasoning models like `o3-mini`, `o3`, and `o4-mini`, you can enable reasoning summaries to see the model's thought process. Different models support different summarizers—for example, `o4-mini` supports detailed summaries. Set `reasoningSummary: "auto"` to automatically receive the richest level available. ```ts highlight="8-9,16" import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; const result = streamText({ model: openai.responses('o4-mini'), prompt: 'Tell me about the Mission burrito debate in San Francisco.', providerOptions: { openai: { reasoningSummary: 'detailed', // 'auto' for condensed or 'detailed' for comprehensive }, }, }); for await (const part of result.fullStream) { if (part.type === 'reasoning') { console.log(`Reasoning: ${part.textDelta}`); } else if (part.type === 'text-delta') { process.stdout.write(part.textDelta); } } ``` For non-streaming calls with `generateText`, the reasoning summaries are available in the `reasoning` field of the response: ```ts highlight="8-9,13" import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; const result = await generateText({ model: openai.responses('o3-mini'), prompt: 'Tell me about the Mission burrito debate in San Francisco.', providerOptions: { openai: { reasoningSummary: 'auto', }, }, }); console.log('Reasoning:', result.reasoning); ``` Learn more about reasoning summaries in the [OpenAI documentation](https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries). #### Image Support The OpenAI Responses API supports Image inputs for appropriate models. You can pass Image files as part of the message content using the 'image' type: ```ts const result = await generateText({ model: openai('gpt-4o'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Please describe the image.', }, { type: 'image', image: fs.readFileSync('./data/image.png'), }, ], }, ], }); ``` Multimodal models will have access to the image and can analyze and conversate about it. The image should be passed using the `image` field. You can also pass a file-id from the OpenAI Files API. ```ts { type: 'image', image: 'file-8EFBcWHsQxZV7YGezBC1fq' } ``` You can also pass the URL of an image. ```ts { type: 'image', image: 'https://sample.edu/image.png', } ``` #### PDF support The OpenAI Responses API supports reading PDF files. You can pass PDF files as part of the message content using the `file` type: ```ts const result = await generateText({ model: openai.responses('gpt-4o'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', filename: 'ai.pdf', // optional }, ], }, ], }); ``` You can also pass a file-id from the OpenAI Files API. ```ts { type: 'file', data: 'file-8EFBcWHsQxZV7YGezBC1fq', mediaType: 'application/pdf', } ``` You can also pass the URL of a pdf. ```ts { type: 'file', data: 'https://sample.edu/example.pdf', mediaType: 'application/pdf', filename: 'ai.pdf', // optional } ``` The model will have access to the contents of the PDF file and respond to questions about it. The PDF file should be passed using the `data` field, and the `mediaType` should be set to `'application/pdf'`. #### Structured Outputs The OpenAI Responses API supports structured outputs. You can enforce structured outputs using `generateObject` or `streamObject`, which expose a `schema` option. Additionally, you can pass a Zod or JSON Schema object to the `experimental_output` option when using `generateText` or `streamText`. ```ts // Using generateObject const result = await generateObject({ model: openai.responses('gpt-4.1'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); // Using generateText const result = await generateText({ model: openai.responses('gpt-4.1'), prompt: 'How do I make a pizza?', experimental_output: Output.object({ schema: z.object({ ingredients: z.array(z.string()), steps: z.array(z.string()), }), }), }); ``` ### Completion Models You can create models that call the [OpenAI completions API](https://platform.openai.com/docs/api-reference/completions) using the `.completion()` factory method. The first argument is the model id. Currently only `gpt-3.5-turbo-instruct` is supported. ```ts const model = openai.completion('gpt-3.5-turbo-instruct'); ``` OpenAI completion models support also some model specific settings that are not part of the [standard call settings](/docs/ai-sdk-core/settings). You can pass them as an options argument: ```ts const model = openai.completion('gpt-3.5-turbo-instruct'); await model.doGenerate({ providerOptions: { openai: { echo: true, // optional, echo the prompt in addition to the completion logitBias: { // optional likelihood for specific tokens '50256': -100, }, suffix: 'some text', // optional suffix that comes after a completion of inserted text user: 'test-user', // optional unique user identifier }, }, }); ``` The following optional provider options are available for OpenAI completion models: - **echo**: _boolean_ Echo back the prompt in addition to the completion. - **logitBias** _Record&lt;number, number&gt;_ Modifies the likelihood of specified tokens appearing in the completion. Accepts a JSON object that maps tokens (specified by their token ID in the GPT tokenizer) to an associated bias value from -100 to 100. You can use this tokenizer tool to convert text to token IDs. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token. As an example, you can pass `{"50256": -100}` to prevent the &lt;|endoftext|&gt; token from being generated. - **logprobs** _boolean | number_ Return the log probabilities of the tokens. Including logprobs will increase the response size and can slow down response times. However, it can be useful to better understand how the model is behaving. Setting to true will return the log probabilities of the tokens that were generated. Setting to a number will return the log probabilities of the top n tokens that were generated. - **suffix** _string_ The suffix that comes after a completion of inserted text. - **user** _string_ A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). ### Model Capabilities | Model | Image Input | Audio Input | Object Generation | Tool Usage | | ---------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `gpt-4.1` | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gpt-4.1-mini` | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gpt-4.1-nano` | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gpt-4o` | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gpt-4o-mini` | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gpt-4o-audio-preview` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gpt-4-turbo` | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gpt-4` | <Cross size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gpt-3.5-turbo` | <Cross size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | `o1` | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | `o1-mini` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `o1-preview` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `o3-mini` | <Cross size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | `o3` | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | `o4-mini` | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | `chatgpt-4o-latest` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Note> The table above lists popular models. Please see the [OpenAI docs](https://platform.openai.com/docs/models) for a full list of available models. The table above lists popular models. You can also pass any available provider model ID as a string if needed. </Note> ## Embedding Models You can create models that call the [OpenAI embeddings API](https://platform.openai.com/docs/api-reference/embeddings) using the `.textEmbedding()` factory method. ```ts const model = openai.textEmbedding('text-embedding-3-large'); ``` OpenAI embedding models support several additional provider options. You can pass them as an options argument: ```ts import { openai } from '@ai-sdk/openai'; import { embed } from 'ai'; const { embedding } = await embed({ model: openai.textEmbedding('text-embedding-3-large'), value: 'sunny day at the beach', providerOptions: { openai: { dimensions: 512, // optional, number of dimensions for the embedding user: 'test-user', // optional unique user identifier }, }, }); ``` The following optional provider options are available for OpenAI embedding models: - **dimensions**: _number_ The number of dimensions the resulting output embeddings should have. Only supported in text-embedding-3 and later models. - **user** _string_ A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids). ### Model Capabilities | Model | Default Dimensions | Custom Dimensions | | ------------------------ | ------------------ | ------------------- | | `text-embedding-3-large` | 3072 | <Check size={18} /> | | `text-embedding-3-small` | 1536 | <Check size={18} /> | | `text-embedding-ada-002` | 1536 | <Cross size={18} /> | ## Image Models You can create models that call the [OpenAI image generation API](https://platform.openai.com/docs/api-reference/images) using the `.image()` factory method. ```ts const model = openai.image('dall-e-3'); ``` <Note> Dall-E models do not support the `aspectRatio` parameter. Use the `size` parameter instead. </Note> ### Model Capabilities | Model | Sizes | | ------------- | ------------------------------- | | `gpt-image-1` | 1024x1024, 1536x1024, 1024x1536 | | `dall-e-3` | 1024x1024, 1792x1024, 1024x1792 | | `dall-e-2` | 256x256, 512x512, 1024x1024 | You can pass optional `providerOptions` to the image model. These are prone to change by OpenAI and are model dependent. For example, the `gpt-image-1` model supports the `quality` option: ```ts const { image, providerMetadata } = await generateImage({ model: openai.image('gpt-image-1'), prompt: 'A salamander at sunrise in a forest pond in the Seychelles.', providerOptions: { openai: { quality: 'high' }, }, }); ``` For more on `generateImage()` see [Image Generation](/docs/ai-sdk-core/image-generation). OpenAI's image models may return a revised prompt for each image. It can be access at `providerMetadata.openai.images[0]?.revisedPrompt`. For more information on the available OpenAI image model options, see the [OpenAI API reference](https://platform.openai.com/docs/api-reference/images/create). ## Transcription Models You can create models that call the [OpenAI transcription API](https://platform.openai.com/docs/api-reference/audio/transcribe) using the `.transcription()` factory method. The first argument is the model id e.g. `whisper-1`. ```ts const model = openai.transcription('whisper-1'); ``` You can also pass additional provider-specific options using the `providerOptions` argument. For example, supplying the input language in ISO-639-1 (e.g. `en`) format will improve accuracy and latency. ```ts highlight="6" import { experimental_transcribe as transcribe } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = await transcribe({ model: openai.transcription('whisper-1'), audio: new Uint8Array([1, 2, 3, 4]), providerOptions: { openai: { language: 'en' } }, }); ``` The following provider options are available: - **timestampGranularities** _string[]_ The granularity of the timestamps in the transcription. Defaults to `['segment']`. Possible values are `['word']`, `['segment']`, and `['word', 'segment']`. Note: There is no additional latency for segment timestamps, but generating word timestamps incurs additional latency. - **language** _string_ The language of the input audio. Supplying the input language in ISO-639-1 format (e.g. 'en') will improve accuracy and latency. Optional. - **prompt** _string_ An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language. Optional. - **temperature** _number_ The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit. Defaults to 0. Optional. - **include** _string[]_ Additional information to include in the transcription response. ### Model Capabilities | Model | Transcription | Duration | Segments | Language | | ------------------------ | ------------------- | ------------------- | ------------------- | ------------------- | | `whisper-1` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gpt-4o-mini-transcribe` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `gpt-4o-transcribe` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | ## Speech Models You can create models that call the [OpenAI speech API](https://platform.openai.com/docs/api-reference/audio/speech) using the `.speech()` factory method. The first argument is the model id e.g. `tts-1`. ```ts const model = openai.speech('tts-1'); ``` You can also pass additional provider-specific options using the `providerOptions` argument. For example, supplying a voice to use for the generated audio. ```ts highlight="6" import { experimental_generateSpeech as generateSpeech } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = await generateSpeech({ model: openai.speech('tts-1'), text: 'Hello, world!', providerOptions: { openai: {} }, }); ``` - **instructions** _string_ Control the voice of your generated audio with additional instructions e.g. "Speak in a slow and steady tone". Does not work with `tts-1` or `tts-1-hd`. Optional. - **response_format** _string_ The format to audio in. Supported formats are `mp3`, `opus`, `aac`, `flac`, `wav`, and `pcm`. Defaults to `mp3`. Optional. - **speed** _number_ The speed of the generated audio. Select a value from 0.25 to 4.0. Defaults to 1.0. Optional. ### Model Capabilities | Model | Instructions | | ----------------- | ------------------- | | `tts-1` | <Check size={18} /> | | `tts-1-hd` | <Check size={18} /> | | `gpt-4o-mini-tts` | <Check size={18} /> | --- File: /ai/content/providers/01-ai-sdk-providers/04-azure.mdx --- --- title: Azure OpenAI description: Learn how to use the Azure OpenAI provider for the AI SDK. --- # Azure OpenAI Provider The [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) provider contains language model support for the Azure OpenAI chat API. ## Setup The Azure OpenAI provider is available in the `@ai-sdk/azure` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/azure" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/azure" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/azure" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `azure` from `@ai-sdk/azure`: ```ts import { azure } from '@ai-sdk/azure'; ``` If you need a customized setup, you can import `createAzure` from `@ai-sdk/azure` and create a provider instance with your settings: ```ts import { createAzure } from '@ai-sdk/azure'; const azure = createAzure({ resourceName: 'your-resource-name', // Azure resource name apiKey: 'your-api-key', }); ``` You can use the following optional settings to customize the OpenAI provider instance: - **resourceName** _string_ Azure resource name. It defaults to the `AZURE_RESOURCE_NAME` environment variable. The resource name is used in the assembled URL: `https://{resourceName}.openai.azure.com/openai/v1{path}`. You can use `baseURL` instead to specify the URL prefix. - **apiKey** _string_ API key that is being sent using the `api-key` header. It defaults to the `AZURE_API_KEY` environment variable. - **apiVersion** _string_ Sets a custom [api version](https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation). Defaults to `preview`. - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. Either this or `resourceName` can be used. When a baseURL is provided, the resourceName is ignored. With a baseURL, the resolved URL is `{baseURL}/v1{path}`. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Language Models The Azure OpenAI provider instance is a function that you can invoke to create a language model: ```ts const model = azure('your-deployment-name'); ``` You need to pass your deployment name as the first argument. ### Reasoning Models Azure exposes the thinking of `DeepSeek-R1` in the generated text using the `<think>` tag. You can use the `extractReasoningMiddleware` to extract this reasoning and expose it as a `reasoning` property on the result: ```ts import { azure } from '@ai-sdk/azure'; import { wrapLanguageModel, extractReasoningMiddleware } from 'ai'; const enhancedModel = wrapLanguageModel({ model: azure('your-deepseek-r1-deployment-name'), middleware: extractReasoningMiddleware({ tagName: 'think' }), }); ``` You can then use that enhanced model in functions like `generateText` and `streamText`. ### Example You can use OpenAI language models to generate text with the `generateText` function: ```ts import { azure } from '@ai-sdk/azure'; import { generateText } from 'ai'; const { text } = await generateText({ model: azure('your-deployment-name'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` OpenAI language models can also be used in the `streamText`, `generateObject`, and `streamObject` functions (see [AI SDK Core](/docs/ai-sdk-core)). <Note> Azure OpenAI sends larger chunks than OpenAI. This can lead to the perception that the response is slower. See [Troubleshooting: Azure OpenAI Slow To Stream](/docs/troubleshooting/common-issues/azure-stream-slow) </Note> ### Provider Options When using OpenAI language models on Azure, you can configure provider-specific options using `providerOptions.openai`. More information on available configuration options are on [the OpenAI provider page](/providers/ai-sdk-providers/openai#language-models). ```ts highlight="12-14,22-24" const messages = [ { role: 'user', content: [ { type: 'text', text: 'What is the capital of the moon?', }, { type: 'image', image: 'https://example.com/image.png', providerOptions: { openai: { imageDetail: 'low' }, }, }, ], }, ]; const { text } = await generateText({ model: azure('your-deployment-name'), providerOptions: { openai: { reasoningEffort: 'low', }, }, }); ``` ### Chat Models <Note> The URL for calling Azure chat models will be constructed as follows: `https://RESOURCE_NAME.openai.azure.com/openai/v1/chat/completions?api-version=preview` </Note> Azure OpenAI chat models support also some model specific settings that are not part of the [standard call settings](/docs/ai-sdk-core/settings). You can pass them as an options argument: ```ts import { azure } from '@ai-sdk/azure'; import { generateText } from 'ai'; const result = await generateText({ model: azure('your-deployment-name'), prompt: 'Write a short story about a robot.', providerOptions: { azure: { logitBias: { // optional likelihood for specific tokens '50256': -100, }, user: 'test-user', // optional unique user identifier }, }, }); ``` The following optional provider options are available for OpenAI chat models: - **logitBias** _Record&lt;number, number&gt;_ Modifies the likelihood of specified tokens appearing in the completion. Accepts a JSON object that maps tokens (specified by their token ID in the GPT tokenizer) to an associated bias value from -100 to 100. You can use this tokenizer tool to convert text to token IDs. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token. As an example, you can pass `{"50256": -100}` to prevent the token from being generated. - **logprobs** _boolean | number_ Return the log probabilities of the tokens. Including logprobs will increase the response size and can slow down response times. However, it can be useful to better understand how the model is behaving. Setting to true will return the log probabilities of the tokens that were generated. Setting to a number will return the log probabilities of the top n tokens that were generated. - **parallelToolCalls** _boolean_ Whether to enable parallel function calling during tool use. Default to true. - **user** _string_ A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. Learn more. ### Responses Models You can use the Azure OpenAI responses API with the `azure.responses(deploymentName)` factory method. ```ts const model = azure.responses('your-deployment-name'); ``` Further configuration can be done using OpenAI provider options. You can validate the provider options using the `OpenAIResponsesProviderOptions` type. ```ts import { azure, OpenAIResponsesProviderOptions } from '@ai-sdk/azure'; import { generateText } from 'ai'; const result = await generateText({ model: azure.responses('your-deployment-name'), providerOptions: { openai: { parallelToolCalls: false, store: false, user: 'user_123', // ... } satisfies OpenAIResponsesProviderOptions, }, // ... }); ``` The following provider options are available: - **parallelToolCalls** _boolean_ Whether to use parallel tool calls. Defaults to `true`. - **store** _boolean_ Whether to store the generation. Defaults to `true`. - **metadata** _Record&lt;string, string&gt;_ Additional metadata to store with the generation. - **previousResponseId** _string_ The ID of the previous response. You can use it to continue a conversation. Defaults to `undefined`. - **instructions** _string_ Instructions for the model. They can be used to change the system or developer message when continuing a conversation using the `previousResponseId` option. Defaults to `undefined`. - **user** _string_ A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. Defaults to `undefined`. - **reasoningEffort** _'low' | 'medium' | 'high'_ Reasoning effort for reasoning models. Defaults to `medium`. If you use `providerOptions` to set the `reasoningEffort` option, this model setting will be ignored. - **strictJsonSchema** _boolean_ Whether to use strict JSON schema validation. Defaults to `false`. The Azure OpenAI responses provider also returns provider-specific metadata: ```ts const { providerMetadata } = await generateText({ model: azure.responses('your-deployment-name'), }); const openaiMetadata = providerMetadata?.openai; ``` The following OpenAI-specific metadata is returned: - **responseId** _string_ The ID of the response. Can be used to continue a conversation. - **cachedPromptTokens** _number_ The number of prompt tokens that were a cache hit. - **reasoningTokens** _number_ The number of reasoning tokens that the model generated. #### PDF support The Azure OpenAI Responses API supports reading PDF files. You can pass PDF files as part of the message content using the `file` type: ```ts const result = await generateText({ model: azure.responses('your-deployment-name'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', filename: 'ai.pdf', // optional }, ], }, ], }); ``` The model will have access to the contents of the PDF file and respond to questions about it. The PDF file should be passed using the `data` field, and the `mediaType` should be set to `'application/pdf'`. ### Completion Models You can create models that call the completions API using the `.completion()` factory method. The first argument is the model id. Currently only `gpt-35-turbo-instruct` is supported. ```ts const model = azure.completion('your-gpt-35-turbo-instruct-deployment'); ``` OpenAI completion models support also some model specific settings that are not part of the [standard call settings](/docs/ai-sdk-core/settings). You can pass them as an options argument: ```ts import { azure } from '@ai-sdk/azure'; import { generateText } from 'ai'; const result = await generateText({ model: azure.completion('your-gpt-35-turbo-instruct-deployment'), prompt: 'Write a haiku about coding.', providerOptions: { azure: { echo: true, // optional, echo the prompt in addition to the completion logitBias: { // optional likelihood for specific tokens '50256': -100, }, suffix: 'some text', // optional suffix that comes after a completion of inserted text user: 'test-user', // optional unique user identifier }, }, }); ``` The following optional provider options are available for Azure OpenAI completion models: - **echo**: _boolean_ Echo back the prompt in addition to the completion. - **logitBias** _Record&lt;number, number&gt;_ Modifies the likelihood of specified tokens appearing in the completion. Accepts a JSON object that maps tokens (specified by their token ID in the GPT tokenizer) to an associated bias value from -100 to 100. You can use this tokenizer tool to convert text to token IDs. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token. As an example, you can pass `{"50256": -100}` to prevent the &lt;|endoftext|&gt; token from being generated. - **logprobs** _boolean | number_ Return the log probabilities of the tokens. Including logprobs will increase the response size and can slow down response times. However, it can be useful to better understand how the model is behaving. Setting to true will return the log probabilities of the tokens that were generated. Setting to a number will return the log probabilities of the top n tokens that were generated. - **suffix** _string_ The suffix that comes after a completion of inserted text. - **user** _string_ A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. Learn more. ## Embedding Models You can create models that call the Azure OpenAI embeddings API using the `.textEmbedding()` factory method. ```ts const model = azure.textEmbedding('your-embedding-deployment'); ``` Azure OpenAI embedding models support several additional settings. You can pass them as an options argument: ```ts import { azure } from '@ai-sdk/azure'; import { embed } from 'ai'; const { embedding } = await embed({ model: azure.textEmbedding('your-embedding-deployment'), value: 'sunny day at the beach', providerOptions: { azure: { dimensions: 512, // optional, number of dimensions for the embedding user: 'test-user', // optional unique user identifier }, }, }); ``` The following optional provider options are available for Azure OpenAI embedding models: - **dimensions**: _number_ The number of dimensions the resulting output embeddings should have. Only supported in text-embedding-3 and later models. - **user** _string_ A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. Learn more. ## Image Models You can create models that call the Azure OpenAI image generation API (DALL-E) using the `.image()` factory method. The first argument is your deployment name for the DALL-E model. ```ts const model = azure.image('your-dalle-deployment-name'); ``` Azure OpenAI image models support several additional settings. You can pass them as `providerOptions.azure` when generating the image: ```ts await generateImage({ model: azure.image('your-dalle-deployment-name'), prompt: 'A photorealistic image of a cat astronaut floating in space', size: '1024x1024', // '1024x1024', '1792x1024', or '1024x1792' for DALL-E 3 providerOptions: { azure: { user: 'test-user', // optional unique user identifier responseFormat: 'url', // 'url' or 'b64_json', defaults to 'url' }, }, }); ``` ### Example You can use Azure OpenAI image models to generate images with the `generateImage` function: ```ts import { azure } from '@ai-sdk/azure'; import { experimental_generateImage as generateImage } from 'ai'; const { image } = await generateImage({ model: azure.image('your-dalle-deployment-name'), prompt: 'A photorealistic image of a cat astronaut floating in space', size: '1024x1024', // '1024x1024', '1792x1024', or '1024x1792' for DALL-E 3 }); // image contains the URL or base64 data of the generated image console.log(image); ``` ### Model Capabilities Azure OpenAI supports DALL-E 2 and DALL-E 3 models through deployments. The capabilities depend on which model version your deployment is using: | Model Version | Sizes | | ------------- | ------------------------------- | | DALL-E 3 | 1024x1024, 1792x1024, 1024x1792 | | DALL-E 2 | 256x256, 512x512, 1024x1024 | <Note> DALL-E models do not support the `aspectRatio` parameter. Use the `size` parameter instead. </Note> <Note> When creating your Azure OpenAI deployment, make sure to set the DALL-E model version you want to use. </Note> ## Transcription Models You can create models that call the Azure OpenAI transcription API using the `.transcription()` factory method. The first argument is the model id e.g. `whisper-1`. ```ts const model = azure.transcription('whisper-1'); ``` You can also pass additional provider-specific options using the `providerOptions` argument. For example, supplying the input language in ISO-639-1 (e.g. `en`) format will improve accuracy and latency. ```ts highlight="6" import { experimental_transcribe as transcribe } from 'ai'; import { azure } from '@ai-sdk/azure'; import { readFile } from 'fs/promises'; const result = await transcribe({ model: azure.transcription('whisper-1'), audio: await readFile('audio.mp3'), providerOptions: { azure: { language: 'en' } }, }); ``` The following provider options are available: - **timestampGranularities** _string[]_ The granularity of the timestamps in the transcription. Defaults to `['segment']`. Possible values are `['word']`, `['segment']`, and `['word', 'segment']`. Note: There is no additional latency for segment timestamps, but generating word timestamps incurs additional latency. - **language** _string_ The language of the input audio. Supplying the input language in ISO-639-1 format (e.g. 'en') will improve accuracy and latency. Optional. - **prompt** _string_ An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language. Optional. - **temperature** _number_ The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit. Defaults to 0. Optional. - **include** _string[]_ Additional information to include in the transcription response. ### Model Capabilities | Model | Transcription | Duration | Segments | Language | | ------------------------ | ------------------- | ------------------- | ------------------- | ------------------- | | `whisper-1` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gpt-4o-mini-transcribe` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `gpt-4o-transcribe` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | --- File: /ai/content/providers/01-ai-sdk-providers/05-anthropic.mdx --- --- title: Anthropic description: Learn how to use the Anthropic provider for the AI SDK. --- # Anthropic Provider The [Anthropic](https://www.anthropic.com/) provider contains language model support for the [Anthropic Messages API](https://docs.anthropic.com/claude/reference/messages_post). ## Setup The Anthropic provider is available in the `@ai-sdk/anthropic` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/anthropic" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/anthropic" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/anthropic" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `anthropic` from `@ai-sdk/anthropic`: ```ts import { anthropic } from '@ai-sdk/anthropic'; ``` If you need a customized setup, you can import `createAnthropic` from `@ai-sdk/anthropic` and create a provider instance with your settings: ```ts import { createAnthropic } from '@ai-sdk/anthropic'; const anthropic = createAnthropic({ // custom settings }); ``` You can use the following optional settings to customize the Anthropic provider instance: - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.anthropic.com/v1`. - **apiKey** _string_ API key that is being sent using the `x-api-key` header. It defaults to the `ANTHROPIC_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Language Models You can create models that call the [Anthropic Messages API](https://docs.anthropic.com/claude/reference/messages_post) using the provider instance. The first argument is the model id, e.g. `claude-3-haiku-20240307`. Some models have multi-modal capabilities. ```ts const model = anthropic('claude-3-haiku-20240307'); ``` You can use Anthropic language models to generate text with the `generateText` function: ```ts import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; const { text } = await generateText({ model: anthropic('claude-3-haiku-20240307'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` Anthropic language models can also be used in the `streamText`, `generateObject`, and `streamObject` functions (see [AI SDK Core](/docs/ai-sdk-core)). <Note> The Anthropic API returns streaming tool calls all at once after a delay. This causes the `streamObject` function to generate the object fully after a delay instead of streaming it incrementally. </Note> The following optional provider options are available for Anthropic models: - `sendReasoning` _boolean_ Optional. Include reasoning content in requests sent to the model. Defaults to `true`. If you are experiencing issues with the model handling requests involving reasoning content, you can set this to `false` to omit them from the request. - `thinking` _object_ Optional. See [Reasoning section](#reasoning) for more details. ### Reasoning Anthropic has reasoning support for `claude-opus-4-20250514`, `claude-sonnet-4-20250514`, and `claude-3-7-sonnet-20250219` models. You can enable it using the `thinking` provider option and specifying a thinking budget in tokens. ```ts import { anthropic, AnthropicProviderOptions } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; const { text, reasoning, reasoningDetails } = await generateText({ model: anthropic('claude-opus-4-20250514'), prompt: 'How many people will live in the world in 2040?', providerOptions: { anthropic: { thinking: { type: 'enabled', budgetTokens: 12000 }, } satisfies AnthropicProviderOptions, }, }); console.log(reasoning); // reasoning text console.log(reasoningDetails); // reasoning details including redacted reasoning console.log(text); // text response ``` See [AI SDK UI: Chatbot](/docs/ai-sdk-ui/chatbot#reasoning) for more details on how to integrate reasoning into your chatbot. ### Cache Control In the messages and message parts, you can use the `providerOptions` property to set cache control breakpoints. You need to set the `anthropic` property in the `providerOptions` object to `{ cacheControl: { type: 'ephemeral' } }` to set a cache control breakpoint. The cache creation input tokens are then returned in the `providerMetadata` object for `generateText` and `generateObject`, again under the `anthropic` property. When you use `streamText` or `streamObject`, the response contains a promise that resolves to the metadata. Alternatively you can receive it in the `onFinish` callback. ```ts highlight="8,18-20,29-30" import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; const errorMessage = '... long error message ...'; const result = await generateText({ model: anthropic('claude-3-5-sonnet-20240620'), messages: [ { role: 'user', content: [ { type: 'text', text: 'You are a JavaScript expert.' }, { type: 'text', text: `Error message: ${errorMessage}`, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } }, }, }, { type: 'text', text: 'Explain the error message.' }, ], }, ], }); console.log(result.text); console.log(result.providerMetadata?.anthropic); // e.g. { cacheCreationInputTokens: 2118 } ``` You can also use cache control on system messages by providing multiple system messages at the head of your messages array: ```ts highlight="3,7-9" const result = await generateText({ model: anthropic('claude-3-5-sonnet-20240620'), messages: [ { role: 'system', content: 'Cached system message part', providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } }, }, }, { role: 'system', content: 'Uncached system message part', }, { role: 'user', content: 'User prompt', }, ], }); ``` Cache control for tools: ```ts const result = await generateText({ model: anthropic('claude-3-5-haiku-latest'), tools: { cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }), }, messages: [ { role: 'user', content: 'User prompt', }, ], }); ``` #### Longer cache TTL Anthropic also supports a longer 1-hour cache duration. At time of writing, [this is currently in beta](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#1-hour-cache-duration), so you must pass a `'anthropic-beta'` header set to `'extended-cache-ttl-2025-04-11'`. Here's an example: ```ts const result = await generateText({ model: anthropic('claude-3-5-haiku-latest'), headers: { 'anthropic-beta': 'extended-cache-ttl-2025-04-11', }, messages: [ { role: 'user', content: [ { type: 'text', text: 'Long cached message', providerOptions: { anthropic: { cacheControl: { type: 'ephemeral', ttl: '1h' }, }, }, }, ], }, ], }); ``` #### Limitations The minimum cacheable prompt length is: - 1024 tokens for Claude 3.7 Sonnet, Claude 3.5 Sonnet and Claude 3 Opus - 2048 tokens for Claude 3.5 Haiku and Claude 3 Haiku Shorter prompts cannot be cached, even if marked with `cacheControl`. Any requests to cache fewer than this number of tokens will be processed without caching. For more on prompt caching with Anthropic, see [Anthropic's Cache Control documentation](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching). <Note type="warning"> Because the `UIMessage` type (used by AI SDK UI hooks like `useChat`) does not support the `providerOptions` property, you can use `convertToModelMessages` first before passing the messages to functions like `generateText` or `streamText`. For more details on `providerOptions` usage, see [here](/docs/foundations/prompts#provider-options). </Note> ### Computer Use Anthropic provides three provider-defined tools that can be used to interact with external systems: 1. **Bash Tool**: Allows running bash commands. 2. **Text Editor Tool**: Provides functionality for viewing and editing text files. 3. **Computer Tool**: Enables control of keyboard and mouse actions on a computer. They are available via the `tools` property of the provider instance. #### Bash Tool The Bash Tool allows running bash commands. Here's how to create and use it: ```ts const bashTool = anthropic.tools.bash_20241022({ execute: async ({ command, restart }) => { // Implement your bash command execution logic here // Return the result of the command execution }, }); ``` Parameters: - `command` (string): The bash command to run. Required unless the tool is being restarted. - `restart` (boolean, optional): Specifying true will restart this tool. #### Text Editor Tool The Text Editor Tool provides functionality for viewing and editing text files. **For Claude 4 models (Opus & Sonnet):** ```ts const textEditorTool = anthropic.tools.textEditor_20250429({ execute: async ({ command, path, file_text, insert_line, new_str, old_str, view_range, }) => { // Implement your text editing logic here // Return the result of the text editing operation }, }); ``` **For Claude 3.5 Sonnet and earlier models:** ```ts const textEditorTool = anthropic.tools.textEditor_20241022({ execute: async ({ command, path, file_text, insert_line, new_str, old_str, view_range, }) => { // Implement your text editing logic here // Return the result of the text editing operation }, }); ``` Parameters: - `command` ('view' | 'create' | 'str_replace' | 'insert' | 'undo_edit'): The command to run. Note: `undo_edit` is only available in Claude 3.5 Sonnet and earlier models. - `path` (string): Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. - `file_text` (string, optional): Required for `create` command, with the content of the file to be created. - `insert_line` (number, optional): Required for `insert` command. The line number after which to insert the new string. - `new_str` (string, optional): New string for `str_replace` or `insert` commands. - `old_str` (string, optional): Required for `str_replace` command, containing the string to replace. - `view_range` (number[], optional): Optional for `view` command to specify line range to show. When using the Text Editor Tool, make sure to name the key in the tools object correctly: - **Claude 4 models**: Use `str_replace_based_edit_tool` - **Claude 3.5 Sonnet and earlier**: Use `str_replace_editor` ```ts // For Claude 4 models const response = await generateText({ model: anthropic('claude-opus-4-20250514'), prompt: "Create a new file called example.txt, write 'Hello World' to it, and run 'cat example.txt' in the terminal", tools: { str_replace_based_edit_tool: textEditorTool, // Claude 4 tool name }, }); // For Claude 3.5 Sonnet and earlier const response = await generateText({ model: anthropic('claude-3-5-sonnet-20241022'), prompt: "Create a new file called example.txt, write 'Hello World' to it, and run 'cat example.txt' in the terminal", tools: { str_replace_editor: textEditorTool, // Earlier models tool name }, }); ``` #### Computer Tool The Computer Tool enables control of keyboard and mouse actions on a computer: ```ts const computerTool = anthropic.tools.computer_20241022({ displayWidthPx: 1920, displayHeightPx: 1080, displayNumber: 0, // Optional, for X11 environments execute: async ({ action, coordinate, text }) => { // Implement your computer control logic here // Return the result of the action // Example code: switch (action) { case 'screenshot': { // multipart result: return { type: 'image', data: fs .readFileSync('./data/screenshot-editor.png') .toString('base64'), }; } default: { console.log('Action:', action); console.log('Coordinate:', coordinate); console.log('Text:', text); return `executed ${action}`; } } }, // map to tool result content for LLM consumption: toModelOutput(result) { return typeof result === 'string' ? [{ type: 'text', text: result }] : [{ type: 'image', data: result.data, mediaType: 'image/png' }]; }, }); ``` Parameters: - `action` ('key' | 'type' | 'mouse_move' | 'left_click' | 'left_click_drag' | 'right_click' | 'middle_click' | 'double_click' | 'screenshot' | 'cursor_position'): The action to perform. - `coordinate` (number[], optional): Required for `mouse_move` and `left_click_drag` actions. Specifies the (x, y) coordinates. - `text` (string, optional): Required for `type` and `key` actions. These tools can be used in conjunction with the `sonnet-3-5-sonnet-20240620` model to enable more complex interactions and tasks. ### Web Search Anthropic provides a provider-defined web search tool that gives Claude direct access to real-time web content, allowing it to answer questions with up-to-date information beyond its knowledge cutoff. <Note> Web search must be enabled in your organization's [Console settings](https://console.anthropic.com/settings/privacy). </Note> You can enable web search using the provider-defined web search tool: ```ts import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; const webSearchTool = anthropic.tools.webSearch_20250305({ maxUses: 5, }); const result = await generateText({ model: anthropic('claude-opus-4-20250514'), prompt: 'What are the latest developments in AI?', tools: { web_search: webSearchTool, }, }); ``` #### Configuration Options The web search tool supports several configuration options: - **maxUses** _number_ Maximum number of web searches Claude can perform during the conversation. - **allowedDomains** _string[]_ Optional list of domains that Claude is allowed to search. If provided, searches will be restricted to these domains. - **blockedDomains** _string[]_ Optional list of domains that Claude should avoid when searching. - **userLocation** _object_ Optional user location information to provide geographically relevant search results. ```ts const webSearchTool = anthropic.tools.webSearch_20250305({ maxUses: 3, allowedDomains: ['techcrunch.com', 'wired.com'], blockedDomains: ['example-spam-site.com'], userLocation: { type: 'approximate', country: 'US', region: 'California', city: 'San Francisco', timezone: 'America/Los_Angeles', }, }); const result = await generateText({ model: anthropic('claude-opus-4-20250514'), prompt: 'Find local news about technology', tools: { web_search: webSearchTool, }, }); ``` #### Error Handling Web search errors are handled differently depending on whether you're using streaming or non-streaming: **Non-streaming (`generateText`, `generateObject`):** Web search errors throw exceptions that you can catch: ```ts try { const result = await generateText({ model: anthropic('claude-opus-4-20250514'), prompt: 'Search for something', tools: { web_search: webSearchTool, }, }); } catch (error) { if (error.message.includes('Web search failed')) { console.log('Search error:', error.message); // Handle search error appropriately } } ``` **Streaming (`streamText`, `streamObject`):** Web search errors are delivered as error parts in the stream: ```ts const result = await streamText({ model: anthropic('claude-opus-4-20250514'), prompt: 'Search for something', tools: { web_search: webSearchTool, }, }); for await (const part of result.textStream) { if (part.type === 'error') { console.log('Search error:', part.error); // Handle search error appropriately } } ``` ### PDF support Anthropic Sonnet `claude-3-5-sonnet-20241022` supports reading PDF files. You can pass PDF files as part of the message content using the `file` type: Option 1: URL-based PDF document ```ts const result = await generateText({ model: anthropic('claude-3-5-sonnet-20241022'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: new URL( 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/ai.pdf?raw=true', ), mimeType: 'application/pdf', }, ], }, ], }); ``` Option 2: Base64-encoded PDF document ```ts const result = await generateText({ model: anthropic('claude-3-5-sonnet-20241022'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', }, ], }, ], }); ``` The model will have access to the contents of the PDF file and respond to questions about it. The PDF file should be passed using the `data` field, and the `mediaType` should be set to `'application/pdf'`. ### Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Computer Use | Web Search | | ---------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `claude-opus-4-20250514` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `claude-sonnet-4-20250514` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `claude-3-7-sonnet-20250219` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `claude-3-5-sonnet-20241022` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `claude-3-5-sonnet-20240620` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | | `claude-3-5-haiku-20241022` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | | `claude-3-opus-20240229` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `claude-3-sonnet-20240229` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `claude-3-haiku-20240307` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Note> The table above lists popular models. Please see the [Anthropic docs](https://docs.anthropic.com/en/docs/about-claude/models) for a full list of available models. The table above lists popular models. You can also pass any available provider model ID as a string if needed. </Note> --- File: /ai/content/providers/01-ai-sdk-providers/08-amazon-bedrock.mdx --- --- title: Amazon Bedrock description: Learn how to use the Amazon Bedrock provider. --- # Amazon Bedrock Provider The Amazon Bedrock provider for the [AI SDK](/docs) contains language model support for the [Amazon Bedrock](https://aws.amazon.com/bedrock) APIs. ## Setup The Bedrock provider is available in the `@ai-sdk/amazon-bedrock` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/amazon-bedrock" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/amazon-bedrock" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/amazon-bedrock" dark /> </Tab> </Tabs> ### Prerequisites Access to Amazon Bedrock foundation models isn't granted by default. In order to gain access to a foundation model, an IAM user with sufficient permissions needs to request access to it through the console. Once access is provided to a model, it is available for all users in the account. See the [Model Access Docs](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) for more information. ### Authentication #### Using IAM Access Key and Secret Key **Step 1: Creating AWS Access Key and Secret Key** To get started, you'll need to create an AWS access key and secret key. Here's how: **Login to AWS Management Console** - Go to the [AWS Management Console](https://console.aws.amazon.com/) and log in with your AWS account credentials. **Create an IAM User** - Navigate to the [IAM dashboard](https://console.aws.amazon.com/iam/home) and click on "Users" in the left-hand navigation menu. - Click on "Create user" and fill in the required details to create a new IAM user. - Make sure to select "Programmatic access" as the access type. - The user account needs the `AmazonBedrockFullAccess` policy attached to it. **Create Access Key** - Click on the "Security credentials" tab and then click on "Create access key". - Click "Create access key" to generate a new access key pair. - Download the `.csv` file containing the access key ID and secret access key. **Step 2: Configuring the Access Key and Secret Key** Within your project add a `.env` file if you don't already have one. This file will be used to set the access key and secret key as environment variables. Add the following lines to the `.env` file: ```makefile AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY=YOUR_SECRET_ACCESS_KEY AWS_REGION=YOUR_REGION ``` <Note> Many frameworks such as [Next.js](https://nextjs.org/) load the `.env` file automatically. If you're using a different framework, you may need to load the `.env` file manually using a package like [`dotenv`](https://github.com/motdotla/dotenv). </Note> Remember to replace `YOUR_ACCESS_KEY_ID`, `YOUR_SECRET_ACCESS_KEY`, and `YOUR_REGION` with the actual values from your AWS account. #### Using AWS SDK Credentials Chain (instance profiles, instance roles, ECS roles, EKS Service Accounts, etc.) When using AWS SDK, the SDK will automatically use the credentials chain to determine the credentials to use. This includes instance profiles, instance roles, ECS roles, EKS Service Accounts, etc. A similar behavior is possible using the AI SDK by not specifying the `accessKeyId` and `secretAccessKey`, `sessionToken` properties in the provider settings and instead passing a `credentialProvider` property. _Usage:_ `@aws-sdk/credential-providers` package provides a set of credential providers that can be used to create a credential provider chain. <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @aws-sdk/credential-providers" dark /> </Tab> <Tab> <Snippet text="npm install @aws-sdk/credential-providers" dark /> </Tab> <Tab> <Snippet text="yarn add @aws-sdk/credential-providers" dark /> </Tab> </Tabs> ```ts import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'; import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; const bedrock = createAmazonBedrock({ region: 'us-east-1', credentialProvider: fromNodeProviderChain(), }); ``` ## Provider Instance You can import the default provider instance `bedrock` from `@ai-sdk/amazon-bedrock`: ```ts import { bedrock } from '@ai-sdk/amazon-bedrock'; ``` If you need a customized setup, you can import `createAmazonBedrock` from `@ai-sdk/amazon-bedrock` and create a provider instance with your settings: ```ts import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'; const bedrock = createAmazonBedrock({ region: 'us-east-1', accessKeyId: 'xxxxxxxxx', secretAccessKey: 'xxxxxxxxx', sessionToken: 'xxxxxxxxx', }); ``` <Note> The credentials settings fall back to environment variable defaults described below. These may be set by your serverless environment without your awareness, which can lead to merged/conflicting credential values and provider errors around failed authentication. If you're experiencing issues be sure you are explicitly specifying all settings (even if `undefined`) to avoid any defaults. </Note> You can use the following optional settings to customize the Amazon Bedrock provider instance: - **region** _string_ The AWS region that you want to use for the API calls. It uses the `AWS_REGION` environment variable by default. - **accessKeyId** _string_ The AWS access key ID that you want to use for the API calls. It uses the `AWS_ACCESS_KEY_ID` environment variable by default. - **secretAccessKey** _string_ The AWS secret access key that you want to use for the API calls. It uses the `AWS_SECRET_ACCESS_KEY` environment variable by default. - **sessionToken** _string_ Optional. The AWS session token that you want to use for the API calls. It uses the `AWS_SESSION_TOKEN` environment variable by default. - **credentialProvider** _() =&gt; Promise&lt;&#123; accessKeyId: string; secretAccessKey: string; sessionToken?: string; &#125;&gt;_ Optional. The AWS credential provider chain that you want to use for the API calls. It uses the specified credentials by default. ## Language Models You can create models that call the Bedrock API using the provider instance. The first argument is the model id, e.g. `meta.llama3-70b-instruct-v1:0`. ```ts const model = bedrock('meta.llama3-70b-instruct-v1:0'); ``` Amazon Bedrock models also support some model specific provider options that are not part of the [standard call settings](/docs/ai-sdk-core/settings). You can pass them in the `providerOptions` argument: ```ts const model = bedrock('anthropic.claude-3-sonnet-20240229-v1:0'); await generateText({ model, providerOptions: { anthropic: { additionalModelRequestFields: { top_k: 350 }, }, }, }); ``` Documentation for additional settings based on the selected model can be found within the [Amazon Bedrock Inference Parameter Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters.html). You can use Amazon Bedrock language models to generate text with the `generateText` function: ```ts import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; const { text } = await generateText({ model: bedrock('meta.llama3-70b-instruct-v1:0'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` Amazon Bedrock language models can also be used in the `streamText` function (see [AI SDK Core](/docs/ai-sdk-core)). ### File Inputs <Note type="warning"> Amazon Bedrock supports file inputs on in combination with specific models, e.g. `anthropic.claude-3-haiku-20240307-v1:0`. </Note> The Amazon Bedrock provider supports file inputs, e.g. PDF files. ```ts import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; const result = await generateText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the pdf in detail.' }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', }, ], }, ], }); ``` ### Guardrails You can use the `bedrock` provider options to utilize [Amazon Bedrock Guardrails](https://aws.amazon.com/bedrock/guardrails/): ```ts const result = await generateText({ model: bedrock('anthropic.claude-3-sonnet-20240229-v1:0'), prompt: 'Write a story about space exploration.', providerOptions: { bedrock: { guardrailConfig: { guardrailIdentifier: '1abcd2ef34gh', guardrailVersion: '1', trace: 'enabled' as const, streamProcessingMode: 'async', }, }, }, }); ``` Tracing information will be returned in the provider metadata if you have tracing enabled. ```ts if (result.providerMetadata?.bedrock.trace) { // ... } ``` See the [Amazon Bedrock Guardrails documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html) for more information. ### Cache Points <Note> Amazon Bedrock prompt caching is currently in preview release. To request access, visit the [Amazon Bedrock prompt caching page](https://aws.amazon.com/bedrock/prompt-caching/). </Note> In messages, you can use the `providerOptions` property to set cache points. Set the `bedrock` property in the `providerOptions` object to `{ cachePoint: { type: 'default' } }` to create a cache point. Cache usage information is returned in the `providerMetadata` object`. See examples below. <Note> Cache points have model-specific token minimums and limits. For example, Claude 3.5 Sonnet v2 requires at least 1,024 tokens for a cache point and allows up to 4 cache points. See the [Amazon Bedrock prompt caching documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html) for details on supported models, regions, and limits. </Note> ```ts import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; const cyberpunkAnalysis = '... literary analysis of cyberpunk themes and concepts ...'; const result = await generateText({ model: bedrock('anthropic.claude-3-5-sonnet-20241022-v2:0'), messages: [ { role: 'system', content: `You are an expert on William Gibson's cyberpunk literature and themes. You have access to the following academic analysis: ${cyberpunkAnalysis}`, providerOptions: { bedrock: { cachePoint: { type: 'default' } }, }, }, { role: 'user', content: 'What are the key cyberpunk themes that Gibson explores in Neuromancer?', }, ], }); console.log(result.text); console.log(result.providerMetadata?.bedrock?.usage); // Shows cache read/write token usage, e.g.: // { // cacheReadInputTokens: 1337, // cacheWriteInputTokens: 42, // } ``` Cache points also work with streaming responses: ```ts import { bedrock } from '@ai-sdk/amazon-bedrock'; import { streamText } from 'ai'; const cyberpunkAnalysis = '... literary analysis of cyberpunk themes and concepts ...'; const result = streamText({ model: bedrock('anthropic.claude-3-5-sonnet-20241022-v2:0'), messages: [ { role: 'assistant', content: [ { type: 'text', text: 'You are an expert on cyberpunk literature.' }, { type: 'text', text: `Academic analysis: ${cyberpunkAnalysis}` }, ], providerOptions: { bedrock: { cachePoint: { type: 'default' } } }, }, { role: 'user', content: 'How does Gibson explore the relationship between humanity and technology?', }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log( 'Cache token usage:', (await result.providerMetadata)?.bedrock?.usage, ); // Shows cache read/write token usage, e.g.: // { // cacheReadInputTokens: 1337, // cacheWriteInputTokens: 42, // } ``` ## Reasoning Amazon Bedrock has reasoning support for the `claude-3-7-sonnet-20250219` model. You can enable it using the `reasoningConfig` provider option and specifying a thinking budget in tokens (minimum: `1024`, maximum: `64000`). ```ts import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; const { text, reasoning, reasoningDetails } = await generateText({ model: bedrock('us.anthropic.claude-3-7-sonnet-20250219-v1:0'), prompt: 'How many people will live in the world in 2040?', providerOptions: { bedrock: { reasoningConfig: { type: 'enabled', budgetTokens: 1024 }, }, }, }); console.log(reasoning); // reasoning text console.log(reasoningDetails); // reasoning details including redacted reasoning console.log(text); // text response ``` See [AI SDK UI: Chatbot](/docs/ai-sdk-ui/chatbot#reasoning) for more details on how to integrate reasoning into your chatbot. ## Computer Use Via Anthropic, Amazon Bedrock provides three provider-defined tools that can be used to interact with external systems: 1. **Bash Tool**: Allows running bash commands. 2. **Text Editor Tool**: Provides functionality for viewing and editing text files. 3. **Computer Tool**: Enables control of keyboard and mouse actions on a computer. They are available via the `tools` property of the provider instance. ### Bash Tool The Bash Tool allows running bash commands. Here's how to create and use it: ```ts const bashTool = anthropic.tools.bash_20241022({ execute: async ({ command, restart }) => { // Implement your bash command execution logic here // Return the result of the command execution }, }); ``` Parameters: - `command` (string): The bash command to run. Required unless the tool is being restarted. - `restart` (boolean, optional): Specifying true will restart this tool. ### Text Editor Tool The Text Editor Tool provides functionality for viewing and editing text files. **For Claude 4 models (Opus & Sonnet):** ```ts const textEditorTool = anthropic.tools.textEditor_20250429({ execute: async ({ command, path, file_text, insert_line, new_str, old_str, view_range, }) => { // Implement your text editing logic here // Return the result of the text editing operation }, }); ``` **For Claude 3.5 Sonnet and earlier models:** ```ts const textEditorTool = anthropic.tools.textEditor_20241022({ execute: async ({ command, path, file_text, insert_line, new_str, old_str, view_range, }) => { // Implement your text editing logic here // Return the result of the text editing operation }, }); ``` Parameters: - `command` ('view' | 'create' | 'str_replace' | 'insert' | 'undo_edit'): The command to run. Note: `undo_edit` is only available in Claude 3.5 Sonnet and earlier models. - `path` (string): Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. - `file_text` (string, optional): Required for `create` command, with the content of the file to be created. - `insert_line` (number, optional): Required for `insert` command. The line number after which to insert the new string. - `new_str` (string, optional): New string for `str_replace` or `insert` commands. - `old_str` (string, optional): Required for `str_replace` command, containing the string to replace. - `view_range` (number[], optional): Optional for `view` command to specify line range to show. When using the Text Editor Tool, make sure to name the key in the tools object correctly: - **Claude 4 models**: Use `str_replace_based_edit_tool` - **Claude 3.5 Sonnet and earlier**: Use `str_replace_editor` ```ts // For Claude 4 models const response = await generateText({ model: bedrock('us.anthropic.claude-sonnet-4-20250514-v1:0'), prompt: "Create a new file called example.txt, write 'Hello World' to it, and run 'cat example.txt' in the terminal", tools: { str_replace_based_edit_tool: textEditorTool, // Claude 4 tool name }, }); // For Claude 3.5 Sonnet and earlier const response = await generateText({ model: bedrock('anthropic.claude-3-5-sonnet-20241022-v2:0'), prompt: "Create a new file called example.txt, write 'Hello World' to it, and run 'cat example.txt' in the terminal", tools: { str_replace_editor: textEditorTool, // Earlier models tool name }, }); ``` ### Computer Tool The Computer Tool enables control of keyboard and mouse actions on a computer: ```ts const computerTool = anthropic.tools.computer_20241022({ displayWidthPx: 1920, displayHeightPx: 1080, displayNumber: 0, // Optional, for X11 environments execute: async ({ action, coordinate, text }) => { // Implement your computer control logic here // Return the result of the action // Example code: switch (action) { case 'screenshot': { // multipart result: return { type: 'image', data: fs .readFileSync('./data/screenshot-editor.png') .toString('base64'), }; } default: { console.log('Action:', action); console.log('Coordinate:', coordinate); console.log('Text:', text); return `executed ${action}`; } } }, // map to tool result content for LLM consumption: toModelOutput(result) { return typeof result === 'string' ? [{ type: 'text', text: result }] : [{ type: 'image', data: result.data, mediaType: 'image/png' }]; }, }); ``` Parameters: - `action` ('key' | 'type' | 'mouse_move' | 'left_click' | 'left_click_drag' | 'right_click' | 'middle_click' | 'double_click' | 'screenshot' | 'cursor_position'): The action to perform. - `coordinate` (number[], optional): Required for `mouse_move` and `left_click_drag` actions. Specifies the (x, y) coordinates. - `text` (string, optional): Required for `type` and `key` actions. These tools can be used in conjunction with the `anthropic.claude-3-5-sonnet-20240620-v1:0` model to enable more complex interactions and tasks. ### Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | ---------------------------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `amazon.titan-tg1-large` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `amazon.titan-text-express-v1` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `amazon.titan-text-lite-v1` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `us.amazon.nova-premier-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.amazon.nova-pro-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.amazon.nova-lite-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.amazon.nova-micro-v1:0` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `anthropic.claude-sonnet-4-20250514-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `anthropic.claude-opus-4-20250514-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `anthropic.claude-opus-4-1-20250805-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `anthropic.claude-3-7-sonnet-20250219-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `anthropic.claude-3-5-sonnet-20241022-v2:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `anthropic.claude-3-5-sonnet-20240620-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `anthropic.claude-3-5-haiku-20241022-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `anthropic.claude-3-opus-20240229-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `anthropic.claude-3-sonnet-20240229-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `anthropic.claude-3-haiku-20240307-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.anthropic.claude-sonnet-4-20250514-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.anthropic.claude-opus-4-20250514-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.anthropic.claude-opus-4-1-20250805-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.anthropic.claude-3-7-sonnet-20250219-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.anthropic.claude-3-5-sonnet-20241022-v2:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.anthropic.claude-3-5-sonnet-20240620-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.anthropic.claude-3-5-haiku-20241022-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `us.anthropic.claude-3-sonnet-20240229-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.anthropic.claude-3-opus-20240229-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.anthropic.claude-3-haiku-20240307-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `anthropic.claude-v2` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `anthropic.claude-v2:1` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `anthropic.claude-instant-v1` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `cohere.command-text-v14` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `cohere.command-light-text-v14` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `cohere.command-r-v1:0` | <Cross size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `cohere.command-r-plus-v1:0` | <Cross size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `us.deepseek.r1-v1:0` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `meta.llama3-8b-instruct-v1:0` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `meta.llama3-70b-instruct-v1:0` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `meta.llama3-1-8b-instruct-v1:0` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `meta.llama3-1-70b-instruct-v1:0` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `meta.llama3-1-405b-instruct-v1:0` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `meta.llama3-2-1b-instruct-v1:0` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `meta.llama3-2-3b-instruct-v1:0` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `meta.llama3-2-11b-instruct-v1:0` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `meta.llama3-2-90b-instruct-v1:0` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `us.meta.llama3-2-1b-instruct-v1:0` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.meta.llama3-2-3b-instruct-v1:0` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.meta.llama3-2-11b-instruct-v1:0` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.meta.llama3-2-90b-instruct-v1:0` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.meta.llama3-1-8b-instruct-v1:0` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.meta.llama3-1-70b-instruct-v1:0` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.meta.llama3-3-70b-instruct-v1:0` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.meta.llama4-scout-17b-instruct-v1:0` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `us.meta.llama4-maverick-17b-instruct-v1:0` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `mistral.mistral-7b-instruct-v0:2` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `mistral.mixtral-8x7b-instruct-v0:1` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `mistral.mistral-large-2402-v1:0` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `mistral.mistral-small-2402-v1:0` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `us.mistral.pixtral-large-2502-v1:0` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `openai.gpt-oss-120b-1:0` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `openai.gpt-oss-20b-1:0` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Note> The table above lists popular models. Please see the [Amazon Bedrock docs](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html) for a full list of available models. The table above lists popular models. You can also pass any available provider model ID as a string if needed. </Note> ## Embedding Models You can create models that call the Bedrock API [Bedrock API](https://docs.aws.amazon.com/bedrock/latest/userguide/titan-embedding-models.html) using the `.textEmbedding()` factory method. ```ts const model = bedrock.textEmbedding('amazon.titan-embed-text-v1'); ``` Bedrock Titan embedding model amazon.titan-embed-text-v2:0 supports several additional settings. You can pass them as an options argument: ```ts import { bedrock } from '@ai-sdk/amazon-bedrock'; import { embed } from 'ai'; const model = bedrock.textEmbedding('amazon.titan-embed-text-v2:0'); const { embedding } = await embed({ model, value: 'sunny day at the beach', providerOptions: { bedrock: { dimensions: 512, // optional, number of dimensions for the embedding normalize: true, // optional, normalize the output embeddings }, }, }); ``` The following optional provider options are available for Bedrock Titan embedding models: - **dimensions**: _number_ The number of dimensions the output embeddings should have. The following values are accepted: 1024 (default), 512, 256. - **normalize** _boolean_ Flag indicating whether or not to normalize the output embeddings. Defaults to true. ### Model Capabilities | Model | Default Dimensions | Custom Dimensions | | ------------------------------ | ------------------ | ------------------- | | `amazon.titan-embed-text-v1` | 1536 | <Cross size={18} /> | | `amazon.titan-embed-text-v2:0` | 1024 | <Check size={18} /> | | `cohere.embed-english-v3` | 1024 | <Cross size={18} /> | | `cohere.embed-multilingual-v3` | 1024 | <Cross size={18} /> | ## Image Models You can create models that call the Bedrock API [Bedrock API](https://docs.aws.amazon.com/nova/latest/userguide/image-generation.html) using the `.image()` factory method. For more on the Amazon Nova Canvas image model, see the [Nova Canvas Overview](https://docs.aws.amazon.com/ai/responsible-ai/nova-canvas/overview.html). <Note> The `amazon.nova-canvas-v1:0` model is available in the `us-east-1`, `eu-west-1`, and `ap-northeast-1` regions. </Note> ```ts const model = bedrock.image('amazon.nova-canvas-v1:0'); ``` You can then generate images with the `experimental_generateImage` function: ```ts import { bedrock } from '@ai-sdk/amazon-bedrock'; import { experimental_generateImage as generateImage } from 'ai'; const { image } = await generateImage({ model: bedrock.image('amazon.nova-canvas-v1:0'), prompt: 'A beautiful sunset over a calm ocean', size: '512x512', seed: 42, }); ``` You can also pass the `providerOptions` object to the `generateImage` function to customize the generation behavior: ```ts import { bedrock } from '@ai-sdk/amazon-bedrock'; import { experimental_generateImage as generateImage } from 'ai'; const { image } = await generateImage({ model: bedrock.image('amazon.nova-canvas-v1:0'), prompt: 'A beautiful sunset over a calm ocean', size: '512x512', seed: 42, providerOptions: { bedrock: { quality: 'premium', negativeText: 'blurry, low quality', cfgScale: 7.5, style: 'PHOTOREALISM', }, }, }); ``` The following optional provider options are available for Amazon Nova Canvas: - **quality** _string_ The quality level for image generation. Accepts `'standard'` or `'premium'`. - **negativeText** _string_ Text describing what you don't want in the generated image. - **cfgScale** _number_ Controls how closely the generated image adheres to the prompt. Higher values result in images that are more closely aligned to the prompt. - **style** _string_ Predefined visual style for image generation. Accepts one of: `3D_ANIMATED_FAMILY_FILM` · `DESIGN_SKETCH` · `FLAT_VECTOR_ILLUSTRATION` · `GRAPHIC_NOVEL_ILLUSTRATION` · `MAXIMALISM` · `MIDCENTURY_RETRO` · `PHOTOREALISM` · `SOFT_DIGITAL_PAINTING`. Documentation for additional settings can be found within the [Amazon Bedrock User Guide for Amazon Nova Documentation](https://docs.aws.amazon.com/nova/latest/userguide/image-gen-req-resp-structure.html). ### Image Model Settings You can customize the generation behavior with optional options: ```ts await generateImage({ model: bedrock.image('amazon.nova-canvas-v1:0'), prompt: 'A beautiful sunset over a calm ocean', size: '512x512', seed: 42, maxImagesPerCall: 1, // Maximum number of images to generate per API call }); ``` - **maxImagesPerCall** _number_ Override the maximum number of images generated per API call. Default can vary by model, with 5 as a common default. ### Model Capabilities The Amazon Nova Canvas model supports custom sizes with constraints as follows: - Each side must be between 320-4096 pixels, inclusive. - Each side must be evenly divisible by 16. - The aspect ratio must be between 1:4 and 4:1. That is, one side can't be more than 4 times longer than the other side. - The total pixel count must be less than 4,194,304. For more, see [Image generation access and usage](https://docs.aws.amazon.com/nova/latest/userguide/image-gen-access.html). | Model | Sizes | | ------------------------- | ----------------------------------------------------------------------------------------------------- | | `amazon.nova-canvas-v1:0` | Custom sizes: 320-4096px per side (must be divisible by 16), aspect ratio 1:4 to 4:1, max 4.2M pixels | ## Response Headers The Amazon Bedrock provider will return the response headers associated with network requests made of the Bedrock servers. ```ts import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; const { text } = await generateText({ model: bedrock('meta.llama3-70b-instruct-v1:0'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); console.log(result.response.headers); ``` Below is sample output where you can see the `x-amzn-requestid` header. This can be useful for correlating Bedrock API calls with requests made by the AI SDK: ```js highlight="6" { connection: 'keep-alive', 'content-length': '2399', 'content-type': 'application/json', date: 'Fri, 07 Feb 2025 04:28:30 GMT', 'x-amzn-requestid': 'c9f3ace4-dd5d-49e5-9807-39aedfa47c8e' } ``` This information is also available with `streamText`: ```ts import { bedrock } from '@ai-sdk/amazon-bedrock'; import { streamText } from 'ai'; const result = streamText({ model: bedrock('meta.llama3-70b-instruct-v1:0'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log('Response headers:', (await result.response).headers); ``` With sample output as: ```js highlight="6" { connection: 'keep-alive', 'content-type': 'application/vnd.amazon.eventstream', date: 'Fri, 07 Feb 2025 04:33:37 GMT', 'transfer-encoding': 'chunked', 'x-amzn-requestid': 'a976e3fc-0e45-4241-9954-b9bdd80ab407' } ``` ## Migrating to `@ai-sdk/amazon-bedrock` 2.x The Amazon Bedrock provider was rewritten in version 2.x to remove the dependency on the `@aws-sdk/client-bedrock-runtime` package. The `bedrockOptions` provider setting previously available has been removed. If you were using the `bedrockOptions` object, you should now use the `region`, `accessKeyId`, `secretAccessKey`, and `sessionToken` settings directly instead. Note that you may need to set all of these explicitly, e.g. even if you're not using `sessionToken`, set it to `undefined`. If you're running in a serverless environment, there may be default environment variables set by your containing environment that the Amazon Bedrock provider will then pick up and could conflict with the ones you're intending to use. --- File: /ai/content/providers/01-ai-sdk-providers/09-groq.mdx --- --- title: Groq description: Learn how to use Groq. --- # Groq Provider The [Groq](https://groq.com/) provider contains language model support for the Groq API. ## Setup The Groq provider is available via the `@ai-sdk/groq` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/groq" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/groq" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/groq" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `groq` from `@ai-sdk/groq`: ```ts import { groq } from '@ai-sdk/groq'; ``` If you need a customized setup, you can import `createGroq` from `@ai-sdk/groq` and create a provider instance with your settings: ```ts import { createGroq } from '@ai-sdk/groq'; const groq = createGroq({ // custom settings }); ``` You can use the following optional settings to customize the Groq provider instance: - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.groq.com/openai/v1`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `GROQ_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Language Models You can create [Groq models](https://console.groq.com/docs/models) using a provider instance. The first argument is the model id, e.g. `gemma2-9b-it`. ```ts const model = groq('gemma2-9b-it'); ``` ### Reasoning Models Groq offers several reasoning models such as `qwen-qwq-32b` and `deepseek-r1-distill-llama-70b`. You can configure how the reasoning is exposed in the generated text by using the `reasoningFormat` option. It supports the options `parsed`, `hidden`, and `raw`. ```ts import { groq } from '@ai-sdk/groq'; import { generateText } from 'ai'; const result = await generateText({ model: groq('qwen/qwen3-32b'), providerOptions: { groq: { reasoningFormat: 'parsed', reasoningEffort: 'default', parallelToolCalls: true, // Enable parallel function calling (default: true) user: 'user-123', // Unique identifier for end-user (optional) }, }, prompt: 'How many "r"s are in the word "strawberry"?', }); ``` The following optional provider options are available for Groq language models: - **reasoningFormat** _'parsed' | 'raw' | 'hidden'_ Controls how reasoning is exposed in the generated text. Only supported by reasoning models like `qwen-qwq-32b` and `deepseek-r1-distill-*` models. For a complete list of reasoning models and their capabilities, see [Groq's reasoning models documentation](https://console.groq.com/docs/reasoning). - **reasoningEffort** _'none' | 'default'_ Controls the level of effort the model will put into reasoning. Only supported by the `qwen/qwen3-32b` model. - `none`: Disable reasoning. The model will not use any reasoning tokens. - `default`: Enable reasoning. Defaults to `default`. - **structuredOutputs** _boolean_ Whether to use structured outputs. Defaults to `true`. When enabled, object generation will use the `json_schema` format instead of `json_object` format, providing more reliable structured outputs. - **parallelToolCalls** _boolean_ Whether to enable parallel function calling during tool use. Defaults to `true`. - **user** _string_ A unique identifier representing your end-user, which can help with monitoring and abuse detection. <Note>Only Groq reasoning models support the `reasoningFormat` option.</Note> #### Structured Outputs Structured outputs are enabled by default for Groq models. You can disable them by setting the `structuredOutputs` option to `false`. ```ts import { groq } from '@ai-sdk/groq'; import { generateObject } from 'ai'; import { z } from 'zod'; const result = await generateObject({ model: groq('moonshotai/kimi-k2-instruct'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.string()), instructions: z.array(z.string()), }), }), prompt: 'Generate a simple pasta recipe.', }); console.log(JSON.stringify(result.object, null, 2)); ``` You can disable structured outputs for models that don't support them: ```ts highlight="9" import { groq } from '@ai-sdk/groq'; import { generateObject } from 'ai'; import { z } from 'zod'; const result = await generateObject({ model: groq('gemma2-9b-it'), providerOptions: { groq: { structuredOutputs: false, }, }, schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.string()), instructions: z.array(z.string()), }), }), prompt: 'Generate a simple pasta recipe in JSON format.', }); console.log(JSON.stringify(result.object, null, 2)); ``` <Note type="warning"> Structured outputs are only supported by newer Groq models like `moonshotai/kimi-k2-instruct`. For unsupported models, you can disable structured outputs by setting `structuredOutputs: false`. When disabled, Groq uses the `json_object` format which requires the word "JSON" to be included in your messages. </Note> ### Example You can use Groq language models to generate text with the `generateText` function: ```ts import { groq } from '@ai-sdk/groq'; import { generateText } from 'ai'; const { text } = await generateText({ model: groq('gemma2-9b-it'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` ### Image Input Groq's multi-modal models like `meta-llama/llama-4-scout-17b-16e-instruct` support image inputs. You can include images in your messages using either URLs or base64-encoded data: ```ts import { groq } from '@ai-sdk/groq'; import { generateText } from 'ai'; const { text } = await generateText({ model: groq('meta-llama/llama-4-scout-17b-16e-instruct'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What do you see in this image?' }, { type: 'image', image: 'https://example.com/image.jpg', }, ], }, ], }); ``` You can also use base64-encoded images: ```ts import { groq } from '@ai-sdk/groq'; import { generateText } from 'ai'; import { readFileSync } from 'fs'; const imageData = readFileSync('path/to/image.jpg', 'base64'); const { text } = await generateText({ model: groq('meta-llama/llama-4-scout-17b-16e-instruct'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe this image in detail.' }, { type: 'image', image: `data:image/jpeg;base64,${imageData}`, }, ], }, ], }); ``` ## Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | ----------------------------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `gemma2-9b-it` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `llama-3.1-8b-instant` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `llama-3.3-70b-versatile` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `meta-llama/llama-guard-4-12b` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `deepseek-r1-distill-llama-70b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `meta-llama/llama-4-maverick-17b-128e-instruct` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `meta-llama/llama-4-scout-17b-16e-instruct` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `meta-llama/llama-prompt-guard-2-22m` | <Cross size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `meta-llama/llama-prompt-guard-2-86m` | <Cross size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `mistral-saba-24b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `moonshotai/kimi-k2-instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `qwen/qwen3-32b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `llama-guard-3-8b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `llama3-70b-8192` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `llama3-8b-8192` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `mixtral-8x7b-32768` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `qwen-qwq-32b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `qwen-2.5-32b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `deepseek-r1-distill-qwen-32b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `openai/gpt-oss-20b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `openai/gpt-oss-120b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Note> The tables above list the most commonly used models. Please see the [Groq docs](https://console.groq.com/docs/models) for a complete list of available models. You can also pass any available provider model ID as a string if needed. </Note> ## Transcription Models You can create models that call the [Groq transcription API](https://console.groq.com/docs/speech-to-text) using the `.transcription()` factory method. The first argument is the model id e.g. `whisper-large-v3`. ```ts const model = groq.transcription('whisper-large-v3'); ``` You can also pass additional provider-specific options using the `providerOptions` argument. For example, supplying the input language in ISO-639-1 (e.g. `en`) format will improve accuracy and latency. ```ts highlight="6" import { experimental_transcribe as transcribe } from 'ai'; import { groq } from '@ai-sdk/groq'; import { readFile } from 'fs/promises'; const result = await transcribe({ model: groq.transcription('whisper-large-v3'), audio: await readFile('audio.mp3'), providerOptions: { groq: { language: 'en' } }, }); ``` The following provider options are available: - **timestampGranularities** _string[]_ The granularity of the timestamps in the transcription. Defaults to `['segment']`. Possible values are `['word']`, `['segment']`, and `['word', 'segment']`. Note: There is no additional latency for segment timestamps, but generating word timestamps incurs additional latency. - **language** _string_ The language of the input audio. Supplying the input language in ISO-639-1 format (e.g. 'en') will improve accuracy and latency. Optional. - **prompt** _string_ An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language. Optional. - **temperature** _number_ The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit. Defaults to 0. Optional. ### Model Capabilities | Model | Transcription | Duration | Segments | Language | | ---------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `whisper-large-v3` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `whisper-large-v3-turbo` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `distil-whisper-large-v3-en` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | --- File: /ai/content/providers/01-ai-sdk-providers/10-fal.mdx --- --- title: Fal description: Learn how to use Fal AI models with the AI SDK. --- # Fal Provider [Fal AI](https://fal.ai/) provides a generative media platform for developers with lightning-fast inference capabilities. Their platform offers optimized performance for running diffusion models, with speeds up to 4x faster than alternatives. ## Setup The Fal provider is available via the `@ai-sdk/fal` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/fal" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/fal" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/fal" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `fal` from `@ai-sdk/fal`: ```ts import { fal } from '@ai-sdk/fal'; ``` If you need a customized setup, you can import `createFal` and create a provider instance with your settings: ```ts import { createFal } from '@ai-sdk/fal'; const fal = createFal({ apiKey: 'your-api-key', // optional, defaults to FAL_API_KEY environment variable, falling back to FAL_KEY baseURL: 'custom-url', // optional headers: { /* custom headers */ }, // optional }); ``` You can use the following optional settings to customize the Fal provider instance: - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://fal.run`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `FAL_API_KEY` environment variable, falling back to `FAL_KEY`. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Image Models You can create Fal image models using the `.image()` factory method. For more on image generation with the AI SDK see [generateImage()](/docs/reference/ai-sdk-core/generate-image). ### Basic Usage ```ts import { fal } from '@ai-sdk/fal'; import { experimental_generateImage as generateImage } from 'ai'; import fs from 'fs'; const { image, providerMetadata } = await generateImage({ model: fal.image('fal-ai/fast-sdxl'), prompt: 'A serene mountain landscape at sunset', }); const filename = `image-${Date.now()}.png`; fs.writeFileSync(filename, image.uint8Array); console.log(`Image saved to ${filename}`); ``` Fal image models may return additional information for the images and the request. Here are some examples of properties that may be set for each image ```js providerMetadata.fal.images[0].nsfw; // boolean, image is not safe for work providerMetadata.fal.images[0].width; // number, image width providerMetadata.fal.images[0].height; // number, image height providerMetadata.fal.images[0].content_type; // string, mime type of the image ``` ### Model Capabilities Fal offers many models optimized for different use cases. Here are a few popular examples. For a full list of models, see the [Fal AI documentation](https://fal.ai/models). | Model | Description | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `fal-ai/fast-sdxl` | High-speed SDXL model optimized for quick inference with up to 4x faster speeds | | `fal-ai/flux-pro/kontext` | FLUX.1 Kontext [pro] handles both text and reference images as inputs, seamlessly enabling targeted, local edits and complex transformations of entire scenes | | `fal-ai/flux-pro/kontext/max` | FLUX.1 Kontext [max] with greatly improved prompt adherence and typography generation, meeting premium consistency for editing without compromise on speed | | `fal-ai/flux-lora` | Super fast endpoint for the FLUX.1 [dev] model with LoRA support, enabling rapid and high-quality image generation using pre-trained LoRA adaptations. | | `fal-ai/flux-pro/v1.1-ultra` | Professional-grade image generation with up to 2K resolution and enhanced photorealism | | `fal-ai/ideogram/v2` | Specialized for high-quality posters and logos with exceptional typography handling | | `fal-ai/recraft-v3` | SOTA in image generation with vector art and brand style capabilities | | `fal-ai/stable-diffusion-3.5-large` | Advanced MMDiT model with improved typography and complex prompt understanding | | `fal-ai/hyper-sdxl` | Performance-optimized SDXL variant with enhanced creative capabilities | | `fal-ai/flux/dev` | FLUX.1 [dev] model for high-quality image generation | | `fal-ai/flux/schnell` | FLUX.1 [schnell] model optimized for speed | | `fal-ai/stable-diffusion-3.5-medium` | Medium-sized Stable Diffusion 3.5 model with good balance of quality and speed | Fal models support the following aspect ratios: - 1:1 (square HD) - 16:9 (landscape) - 9:16 (portrait) - 4:3 (landscape) - 3:4 (portrait) - 16:10 (1280x800) - 10:16 (800x1280) - 21:9 (2560x1080) - 9:21 (1080x2560) Key features of Fal models include: - Up to 4x faster inference speeds compared to alternatives - Optimized by the Fal Inference Engine™ - Support for real-time infrastructure - Cost-effective scaling with pay-per-use pricing - LoRA training capabilities for model personalization #### Modify Image Transform existing images using text prompts. ```ts // Example: Modify existing image await generateImage({ model: fal.image('fal-ai/flux-pro/kontext'), prompt: 'Put a donut next to the flour.', providerOptions: { fal: { image_url: 'https://v3.fal.media/files/rabbit/rmgBxhwGYb2d3pl3x9sKf_output.png', }, }, }); ``` ### Provider Options Fal image models support flexible provider options through the `providerOptions.fal` object. You can pass any parameters supported by the specific Fal model's API. Common options include: - **image_url** - Reference image URL for image-to-image generation - **strength** - Controls how much the output differs from the input image - **guidance_scale** - Controls adherence to the prompt - **num_inference_steps** - Number of denoising steps - **safety_checker** - Enable/disable safety filtering Refer to the [Fal AI model documentation](https://fal.ai/models) for model-specific parameters. ### Advanced Features Fal's platform offers several advanced capabilities: - **Private Model Inference**: Run your own diffusion transformer models with up to 50% faster inference - **LoRA Training**: Train and personalize models in under 5 minutes - **Real-time Infrastructure**: Enable new user experiences with fast inference times - **Scalable Architecture**: Scale to thousands of GPUs when needed For more details about Fal's capabilities and features, visit the [Fal AI documentation](https://fal.ai/docs). ## Transcription Models You can create models that call the [Fal transcription API](https://docs.fal.ai/guides/convert-speech-to-text) using the `.transcription()` factory method. The first argument is the model id without the `fal-ai/` prefix e.g. `wizper`. ```ts const model = fal.transcription('wizper'); ``` You can also pass additional provider-specific options using the `providerOptions` argument. For example, supplying the `batchSize` option will increase the number of audio chunks processed in parallel. ```ts highlight="6" import { experimental_transcribe as transcribe } from 'ai'; import { fal } from '@ai-sdk/fal'; import { readFile } from 'fs/promises'; const result = await transcribe({ model: fal.transcription('wizper'), audio: await readFile('audio.mp3'), providerOptions: { fal: { batchSize: 10 } }, }); ``` The following provider options are available: - **language** _string_ Language of the audio file. If set to null, the language will be automatically detected. Accepts ISO language codes like 'en', 'fr', 'zh', etc. Optional. - **diarize** _boolean_ Whether to diarize the audio file (identify different speakers). Defaults to true. Optional. - **chunkLevel** _string_ Level of the chunks to return. Either 'segment' or 'word'. Default value: "segment" Optional. - **version** _string_ Version of the model to use. All models are Whisper large variants. Default value: "3" Optional. - **batchSize** _number_ Batch size for processing. Default value: 64 Optional. - **numSpeakers** _number_ Number of speakers in the audio file. If not provided, the number of speakers will be automatically detected. Optional. ### Model Capabilities | Model | Transcription | Duration | Segments | Language | | --------- | ------------------- | ------------------- | ------------------- | ------------------- | | `whisper` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `wizper` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | --- File: /ai/content/providers/01-ai-sdk-providers/100-assemblyai.mdx --- --- title: AssemblyAI description: Learn how to use the AssemblyAI provider for the AI SDK. --- # AssemblyAI Provider The [AssemblyAI](https://assemblyai.com/) provider contains language model support for the AssemblyAI transcription API. ## Setup The AssemblyAI provider is available in the `@ai-sdk/assemblyai` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/assemblyai" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/assemblyai" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/assemblyai" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `assemblyai` from `@ai-sdk/assemblyai`: ```ts import { assemblyai } from '@ai-sdk/assemblyai'; ``` If you need a customized setup, you can import `createAssemblyAI` from `@ai-sdk/assemblyai` and create a provider instance with your settings: ```ts import { createAssemblyAI } from '@ai-sdk/assemblyai'; const assemblyai = createAssemblyAI({ // custom settings, e.g. fetch: customFetch, }); ``` You can use the following optional settings to customize the AssemblyAI provider instance: - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `ASSEMBLYAI_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Transcription Models You can create models that call the [AssemblyAI transcription API](https://www.assemblyai.com/docs/getting-started/transcribe-an-audio-file/typescript) using the `.transcription()` factory method. The first argument is the model id e.g. `best`. ```ts const model = assemblyai.transcription('best'); ``` You can also pass additional provider-specific options using the `providerOptions` argument. For example, supplying the `contentSafety` option will enable content safety filtering. ```ts highlight="6" import { experimental_transcribe as transcribe } from 'ai'; import { assemblyai } from '@ai-sdk/assemblyai'; import { readFile } from 'fs/promises'; const result = await transcribe({ model: assemblyai.transcription('best'), audio: await readFile('audio.mp3'), providerOptions: { assemblyai: { contentSafety: true } }, }); ``` The following provider options are available: - **audioEndAt** _number_ End time of the audio in milliseconds. Optional. - **audioStartFrom** _number_ Start time of the audio in milliseconds. Optional. - **autoChapters** _boolean_ Whether to automatically generate chapters for the transcription. Optional. - **autoHighlights** _boolean_ Whether to automatically generate highlights for the transcription. Optional. - **boostParam** _enum_ Boost parameter for the transcription. Allowed values: `'low'`, `'default'`, `'high'`. Optional. - **contentSafety** _boolean_ Whether to enable content safety filtering. Optional. - **contentSafetyConfidence** _number_ Confidence threshold for content safety filtering (25-100). Optional. - **customSpelling** _array of objects_ Custom spelling rules for the transcription. Each object has `from` (array of strings) and `to` (string) properties. Optional. - **disfluencies** _boolean_ Whether to include disfluencies (um, uh, etc.) in the transcription. Optional. - **entityDetection** _boolean_ Whether to detect entities in the transcription. Optional. - **filterProfanity** _boolean_ Whether to filter profanity in the transcription. Optional. - **formatText** _boolean_ Whether to format the text in the transcription. Optional. - **iabCategories** _boolean_ Whether to include IAB categories in the transcription. Optional. - **languageCode** _string_ Language code for the audio. Supports numerous ISO-639-1 and ISO-639-3 language codes. Optional. - **languageConfidenceThreshold** _number_ Confidence threshold for language detection. Optional. - **languageDetection** _boolean_ Whether to enable language detection. Optional. - **multichannel** _boolean_ Whether to process multiple audio channels separately. Optional. - **punctuate** _boolean_ Whether to add punctuation to the transcription. Optional. - **redactPii** _boolean_ Whether to redact personally identifiable information. Optional. - **redactPiiAudio** _boolean_ Whether to redact PII in the audio file. Optional. - **redactPiiAudioQuality** _enum_ Quality of the redacted audio file. Allowed values: `'mp3'`, `'wav'`. Optional. - **redactPiiPolicies** _array of enums_ Policies for PII redaction, specifying which types of information to redact. Supports numerous types like `'person_name'`, `'phone_number'`, etc. Optional. - **redactPiiSub** _enum_ Substitution method for redacted PII. Allowed values: `'entity_name'`, `'hash'`. Optional. - **sentimentAnalysis** _boolean_ Whether to perform sentiment analysis on the transcription. Optional. - **speakerLabels** _boolean_ Whether to label different speakers in the transcription. Optional. - **speakersExpected** _number_ Expected number of speakers in the audio. Optional. - **speechThreshold** _number_ Threshold for speech detection (0-1). Optional. - **summarization** _boolean_ Whether to generate a summary of the transcription. Optional. - **summaryModel** _enum_ Model to use for summarization. Allowed values: `'informative'`, `'conversational'`, `'catchy'`. Optional. - **summaryType** _enum_ Type of summary to generate. Allowed values: `'bullets'`, `'bullets_verbose'`, `'gist'`, `'headline'`, `'paragraph'`. Optional. - **topics** _array of strings_ List of topics to detect in the transcription. Optional. - **webhookAuthHeaderName** _string_ Name of the authentication header for webhook requests. Optional. - **webhookAuthHeaderValue** _string_ Value of the authentication header for webhook requests. Optional. - **webhookUrl** _string_ URL to send webhook notifications to. Optional. - **wordBoost** _array of strings_ List of words to boost in the transcription. Optional. ### Model Capabilities | Model | Transcription | Duration | Segments | Language | | ------ | ------------------- | ------------------- | ------------------- | ------------------- | | `best` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `nano` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | --- File: /ai/content/providers/01-ai-sdk-providers/11-deepinfra.mdx --- --- title: DeepInfra description: Learn how to use DeepInfra's models with the AI SDK. --- # DeepInfra Provider The [DeepInfra](https://deepinfra.com) provider contains support for state-of-the-art models through the DeepInfra API, including Llama 3, Mixtral, Qwen, and many other popular open-source models. ## Setup The DeepInfra provider is available via the `@ai-sdk/deepinfra` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/deepinfra" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/deepinfra" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/deepinfra" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `deepinfra` from `@ai-sdk/deepinfra`: ```ts import { deepinfra } from '@ai-sdk/deepinfra'; ``` If you need a customized setup, you can import `createDeepInfra` from `@ai-sdk/deepinfra` and create a provider instance with your settings: ```ts import { createDeepInfra } from '@ai-sdk/deepinfra'; const deepinfra = createDeepInfra({ apiKey: process.env.DEEPINFRA_API_KEY ?? '', }); ``` You can use the following optional settings to customize the DeepInfra provider instance: - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.deepinfra.com/v1`. Note: Language models and embeddings use OpenAI-compatible endpoints at `{baseURL}/openai`, while image models use `{baseURL}/inference`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `DEEPINFRA_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Language Models You can create language models using a provider instance. The first argument is the model ID, for example: ```ts import { deepinfra } from '@ai-sdk/deepinfra'; import { generateText } from 'ai'; const { text } = await generateText({ model: deepinfra('meta-llama/Meta-Llama-3.1-70B-Instruct'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` DeepInfra language models can also be used in the `streamText` function (see [AI SDK Core](/docs/ai-sdk-core)). ## Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | --------------------------------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `meta-llama/Llama-4-Scout-17B-16E-Instruct` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `meta-llama/Llama-3.3-70B-Instruct-Turbo` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `meta-llama/Llama-3.3-70B-Instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `meta-llama/Meta-Llama-3.1-405B-Instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `meta-llama/Meta-Llama-3.1-70B-Instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `meta-llama/Meta-Llama-3.1-8B-Instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `meta-llama/Llama-3.2-11B-Vision-Instruct` | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `meta-llama/Llama-3.2-90B-Vision-Instruct` | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `mistralai/Mixtral-8x7B-Instruct-v0.1` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `deepseek-ai/DeepSeek-V3` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `deepseek-ai/DeepSeek-R1` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `deepseek-ai/DeepSeek-R1-Distill-Llama-70B` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `deepseek-ai/DeepSeek-R1-Turbo` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `nvidia/Llama-3.1-Nemotron-70B-Instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `Qwen/Qwen2-7B-Instruct` | <Cross size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `Qwen/Qwen2.5-72B-Instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `Qwen/Qwen2.5-Coder-32B-Instruct` | <Cross size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `Qwen/QwQ-32B-Preview` | <Cross size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `google/codegemma-7b-it` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `google/gemma-2-9b-it` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `microsoft/WizardLM-2-8x22B` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Note> The table above lists popular models. Please see the [DeepInfra docs](https://deepinfra.com) for a full list of available models. You can also pass any available provider model ID as a string if needed. </Note> ## Image Models You can create DeepInfra image models using the `.image()` factory method. For more on image generation with the AI SDK see [generateImage()](/docs/reference/ai-sdk-core/generate-image). ```ts import { deepinfra } from '@ai-sdk/deepinfra'; import { experimental_generateImage as generateImage } from 'ai'; const { image } = await generateImage({ model: deepinfra.image('stabilityai/sd3.5'), prompt: 'A futuristic cityscape at sunset', aspectRatio: '16:9', }); ``` <Note> Model support for `size` and `aspectRatio` parameters varies by model. Please check the individual model documentation on [DeepInfra's models page](https://deepinfra.com/models/text-to-image) for supported options and additional parameters. </Note> ### Model-specific options You can pass model-specific parameters using the `providerOptions.deepinfra` field: ```ts import { deepinfra } from '@ai-sdk/deepinfra'; import { experimental_generateImage as generateImage } from 'ai'; const { image } = await generateImage({ model: deepinfra.image('stabilityai/sd3.5'), prompt: 'A futuristic cityscape at sunset', aspectRatio: '16:9', providerOptions: { deepinfra: { num_inference_steps: 30, // Control the number of denoising steps (1-50) }, }, }); ``` ### Model Capabilities For models supporting aspect ratios, the following ratios are typically supported: `1:1 (default), 16:9, 1:9, 3:2, 2:3, 4:5, 5:4, 9:16, 9:21` For models supporting size parameters, dimensions must typically be: - Multiples of 32 - Width and height between 256 and 1440 pixels - Default size is 1024x1024 | Model | Dimensions Specification | Notes | | ---------------------------------- | ------------------------ | -------------------------------------------------------- | | `stabilityai/sd3.5` | Aspect Ratio | Premium quality base model, 8B parameters | | `black-forest-labs/FLUX-1.1-pro` | Size | Latest state-of-art model with superior prompt following | | `black-forest-labs/FLUX-1-schnell` | Size | Fast generation in 1-4 steps | | `black-forest-labs/FLUX-1-dev` | Size | Optimized for anatomical accuracy | | `black-forest-labs/FLUX-pro` | Size | Flagship Flux model | | `stabilityai/sd3.5-medium` | Aspect Ratio | Balanced 2.5B parameter model | | `stabilityai/sdxl-turbo` | Aspect Ratio | Optimized for fast generation | For more details and pricing information, see the [DeepInfra text-to-image models page](https://deepinfra.com/models/text-to-image). ## Embedding Models You can create DeepInfra embedding models using the `.textEmbedding()` factory method. For more on embedding models with the AI SDK see [embed()](/docs/reference/ai-sdk-core/embed). ```ts import { deepinfra } from '@ai-sdk/deepinfra'; import { embed } from 'ai'; const { embedding } = await embed({ model: deepinfra.textEmbedding('BAAI/bge-large-en-v1.5'), value: 'sunny day at the beach', }); ``` ### Model Capabilities | Model | Dimensions | Max Tokens | | ----------------------------------------------------- | ---------- | ---------- | | `BAAI/bge-base-en-v1.5` | 768 | 512 | | `BAAI/bge-large-en-v1.5` | 1024 | 512 | | `BAAI/bge-m3` | 1024 | 8192 | | `intfloat/e5-base-v2` | 768 | 512 | | `intfloat/e5-large-v2` | 1024 | 512 | | `intfloat/multilingual-e5-large` | 1024 | 512 | | `sentence-transformers/all-MiniLM-L12-v2` | 384 | 256 | | `sentence-transformers/all-MiniLM-L6-v2` | 384 | 256 | | `sentence-transformers/all-mpnet-base-v2` | 768 | 384 | | `sentence-transformers/clip-ViT-B-32` | 512 | 77 | | `sentence-transformers/clip-ViT-B-32-multilingual-v1` | 512 | 77 | | `sentence-transformers/multi-qa-mpnet-base-dot-v1` | 768 | 512 | | `sentence-transformers/paraphrase-MiniLM-L6-v2` | 384 | 128 | | `shibing624/text2vec-base-chinese` | 768 | 512 | | `thenlper/gte-base` | 768 | 512 | | `thenlper/gte-large` | 1024 | 512 | <Note> For a complete list of available embedding models, see the [DeepInfra embeddings page](https://deepinfra.com/models/embeddings). </Note> --- File: /ai/content/providers/01-ai-sdk-providers/110-deepgram.mdx --- --- title: Deepgram description: Learn how to use the Deepgram provider for the AI SDK. --- # Deepgram Provider The [Deepgram](https://deepgram.com/) provider contains language model support for the Deepgram transcription API. ## Setup The Deepgram provider is available in the `@ai-sdk/deepgram` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/deepgram" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/deepgram" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/deepgram" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `deepgram` from `@ai-sdk/deepgram`: ```ts import { deepgram } from '@ai-sdk/deepgram'; ``` If you need a customized setup, you can import `createDeepgram` from `@ai-sdk/deepgram` and create a provider instance with your settings: ```ts import { createDeepgram } from '@ai-sdk/deepgram'; const deepgram = createDeepgram({ // custom settings, e.g. fetch: customFetch, }); ``` You can use the following optional settings to customize the Deepgram provider instance: - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `DEEPGRAM_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Transcription Models You can create models that call the [Deepgram transcription API](https://developers.deepgram.com/docs/pre-recorded-audio) using the `.transcription()` factory method. The first argument is the model id e.g. `nova-3`. ```ts const model = deepgram.transcription('nova-3'); ``` You can also pass additional provider-specific options using the `providerOptions` argument. For example, supplying the `summarize` option will enable summaries for sections of content. ```ts highlight="6" import { experimental_transcribe as transcribe } from 'ai'; import { deepgram } from '@ai-sdk/deepgram'; import { readFile } from 'fs/promises'; const result = await transcribe({ model: deepgram.transcription('nova-3'), audio: await readFile('audio.mp3'), providerOptions: { deepgram: { summarize: true } }, }); ``` The following provider options are available: - **language** _string_ Language code for the audio. Supports numerous ISO-639-1 and ISO-639-3 language codes. Optional. - **smartFormat** _boolean_ Whether to apply smart formatting to the transcription. Optional. - **punctuate** _boolean_ Whether to add punctuation to the transcription. Optional. - **paragraphs** _boolean_ Whether to format the transcription into paragraphs. Optional. - **summarize** _enum | boolean_ Whether to generate a summary of the transcription. Allowed values: `'v2'`, `false`. Optional. - **topics** _boolean_ Whether to detect topics in the transcription. Optional. - **intents** _boolean_ Whether to detect intents in the transcription. Optional. - **sentiment** _boolean_ Whether to perform sentiment analysis on the transcription. Optional. - **detectEntities** _boolean_ Whether to detect entities in the transcription. Optional. - **redact** _string | array of strings_ Specifies what content to redact from the transcription. Optional. - **replace** _string_ Replacement string for redacted content. Optional. - **search** _string_ Search term to find in the transcription. Optional. - **keyterm** _string_ Key terms to identify in the transcription. Optional. - **diarize** _boolean_ Whether to identify different speakers in the transcription. Defaults to `true`. Optional. - **utterances** _boolean_ Whether to segment the transcription into utterances. Optional. - **uttSplit** _number_ Threshold for splitting utterances. Optional. - **fillerWords** _boolean_ Whether to include filler words (um, uh, etc.) in the transcription. Optional. ### Model Capabilities | Model | Transcription | Duration | Segments | Language | | -------------------------------------------------------------------------------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `nova-3` (+ [variants](https://developers.deepgram.com/docs/models-languages-overview#nova-3)) | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `nova-2` (+ [variants](https://developers.deepgram.com/docs/models-languages-overview#nova-2)) | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `nova` (+ [variants](https://developers.deepgram.com/docs/models-languages-overview#nova)) | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `enhanced` (+ [variants](https://developers.deepgram.com/docs/models-languages-overview#enhanced)) | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `base` (+ [variants](https://developers.deepgram.com/docs/models-languages-overview#base)) | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | --- File: /ai/content/providers/01-ai-sdk-providers/120-gladia.mdx --- --- title: Gladia description: Learn how to use the Gladia provider for the AI SDK. --- # Gladia Provider The [Gladia](https://gladia.io/) provider contains language model support for the Gladia transcription API. ## Setup The Gladia provider is available in the `@ai-sdk/gladia` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/gladia" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/gladia" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/gladia" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `gladia` from `@ai-sdk/gladia`: ```ts import { gladia } from '@ai-sdk/gladia'; ``` If you need a customized setup, you can import `createGladia` from `@ai-sdk/gladia` and create a provider instance with your settings: ```ts import { createGladia } from '@ai-sdk/gladia'; const gladia = createGladia({ // custom settings, e.g. fetch: customFetch, }); ``` You can use the following optional settings to customize the Gladia provider instance: - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `GLADIA_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Transcription Models You can create models that call the [Gladia transcription API](https://docs.gladia.io/chapters/pre-recorded-stt/getting-started) using the `.transcription()` factory method. ```ts const model = gladia.transcription(); ``` You can also pass additional provider-specific options using the `providerOptions` argument. For example, supplying the `summarize` option will enable summaries for sections of content. ```ts highlight="6" import { experimental_transcribe as transcribe } from 'ai'; import { gladia } from '@ai-sdk/gladia'; import { readFile } from 'fs/promises'; const result = await transcribe({ model: gladia.transcription(), audio: await readFile('audio.mp3'), providerOptions: { gladia: { summarize: true } }, }); ``` <Note> Gladia does not have various models, so you can omit the standard `model` id parameter. </Note> The following provider options are available: - **contextPrompt** _string_ Context to feed the transcription model with for possible better accuracy. Optional. - **customVocabulary** _boolean | any[]_ Custom vocabulary to improve transcription accuracy. Optional. - **customVocabularyConfig** _object_ Configuration for custom vocabulary. Optional. - **vocabulary** _Array&lt;string | \{ value: string, intensity?: number, pronunciations?: string[], language?: string \}&gt;_ - **defaultIntensity** _number_ - **detectLanguage** _boolean_ Whether to automatically detect the language. Optional. - **enableCodeSwitching** _boolean_ Enable code switching for multilingual audio. Optional. - **codeSwitchingConfig** _object_ Configuration for code switching. Optional. - **languages** _string[]_ - **language** _string_ Specify the language of the audio. Optional. - **callback** _boolean_ Enable callback when transcription is complete. Optional. - **callbackConfig** _object_ Configuration for callback. Optional. - **url** _string_ - **method** _'POST' | 'PUT'_ - **subtitles** _boolean_ Generate subtitles from the transcription. Optional. - **subtitlesConfig** _object_ Configuration for subtitles. Optional. - **formats** _Array&lt;'srt' | 'vtt'&gt;_ - **minimumDuration** _number_ - **maximumDuration** _number_ - **maximumCharactersPerRow** _number_ - **maximumRowsPerCaption** _number_ - **style** _'default' | 'compliance'_ - **diarization** _boolean_ Enable speaker diarization. Defaults to `true`. Optional. - **diarizationConfig** _object_ Configuration for diarization. Optional. - **numberOfSpeakers** _number_ - **minSpeakers** _number_ - **maxSpeakers** _number_ - **enhanced** _boolean_ - **translation** _boolean_ Enable translation of the transcription. Optional. - **translationConfig** _object_ Configuration for translation. Optional. - **targetLanguages** _string[]_ - **model** _'base' | 'enhanced'_ - **matchOriginalUtterances** _boolean_ - **summarization** _boolean_ Enable summarization of the transcription. Optional. - **summarizationConfig** _object_ Configuration for summarization. Optional. - **type** _'general' | 'bullet_points' | 'concise'_ - **moderation** _boolean_ Enable content moderation. Optional. - **namedEntityRecognition** _boolean_ Enable named entity recognition. Optional. - **chapterization** _boolean_ Enable chapterization of the transcription. Optional. - **nameConsistency** _boolean_ Enable name consistency in the transcription. Optional. - **customSpelling** _boolean_ Enable custom spelling. Optional. - **customSpellingConfig** _object_ Configuration for custom spelling. Optional. - **spellingDictionary** _Record&lt;string, string[]&gt;_ - **structuredDataExtraction** _boolean_ Enable structured data extraction. Optional. - **structuredDataExtractionConfig** _object_ Configuration for structured data extraction. Optional. - **classes** _string[]_ - **sentimentAnalysis** _boolean_ Enable sentiment analysis. Optional. - **audioToLlm** _boolean_ Enable audio to LLM processing. Optional. - **audioToLlmConfig** _object_ Configuration for audio to LLM. Optional. - **prompts** _string[]_ - **customMetadata** _Record&lt;string, any&gt;_ Custom metadata to include with the request. Optional. - **sentences** _boolean_ Enable sentence detection. Optional. - **displayMode** _boolean_ Enable display mode. Optional. - **punctuationEnhanced** _boolean_ Enable enhanced punctuation. Optional. ### Model Capabilities | Model | Transcription | Duration | Segments | Language | | --------- | ------------------- | ------------------- | ------------------- | ------------------- | | `Default` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | --- File: /ai/content/providers/01-ai-sdk-providers/140-lmnt.mdx --- --- title: LMNT description: Learn how to use the LMNT provider for the AI SDK. --- # LMNT Provider The [LMNT](https://lmnt.com/) provider contains language model support for the LMNT transcription API. ## Setup The LMNT provider is available in the `@ai-sdk/lmnt` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/lmnt" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/lmnt" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/lmnt" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `lmnt` from `@ai-sdk/lmnt`: ```ts import { lmnt } from '@ai-sdk/lmnt'; ``` If you need a customized setup, you can import `createLMNT` from `@ai-sdk/lmnt` and create a provider instance with your settings: ```ts import { createLMNT } from '@ai-sdk/lmnt'; const lmnt = createLMNT({ // custom settings, e.g. fetch: customFetch, }); ``` You can use the following optional settings to customize the LMNT provider instance: - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `LMNT_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Speech Models You can create models that call the [LMNT speech API](https://docs.lmnt.com/api-reference/speech/synthesize-speech-bytes) using the `.speech()` factory method. The first argument is the model id e.g. `aurora`. ```ts const model = lmnt.speech('aurora'); ``` You can also pass additional provider-specific options using the `providerOptions` argument. For example, supplying a voice to use for the generated audio. ```ts highlight="6" import { experimental_generateSpeech as generateSpeech } from 'ai'; import { lmnt } from '@ai-sdk/lmnt'; const result = await generateSpeech({ model: lmnt.speech('aurora'), text: 'Hello, world!', language: 'en', // Standardized language parameter }); ``` ### Provider Options The LMNT provider accepts the following options: - **model** _'aurora' | 'blizzard'_ The LMNT model to use. Defaults to `'aurora'`. - **language** _'auto' | 'en' | 'es' | 'pt' | 'fr' | 'de' | 'zh' | 'ko' | 'hi' | 'ja' | 'ru' | 'it' | 'tr'_ The language to use for speech synthesis. Defaults to `'auto'`. - **format** _'aac' | 'mp3' | 'mulaw' | 'raw' | 'wav'_ The audio format to return. Defaults to `'mp3'`. - **sampleRate** _number_ The sample rate of the audio in Hz. Defaults to `24000`. - **speed** _number_ The speed of the speech. Must be between 0.25 and 2. Defaults to `1`. - **seed** _number_ An optional seed for deterministic generation. - **conversational** _boolean_ Whether to use a conversational style. Defaults to `false`. - **length** _number_ Maximum length of the audio in seconds. Maximum value is 300. - **topP** _number_ Top-p sampling parameter. Must be between 0 and 1. Defaults to `1`. - **temperature** _number_ Temperature parameter for sampling. Must be at least 0. Defaults to `1`. ### Model Capabilities | Model | Instructions | | ---------- | ------------------- | | `aurora` | <Check size={18} /> | | `blizzard` | <Check size={18} /> | --- File: /ai/content/providers/01-ai-sdk-providers/15-google-generative-ai.mdx --- --- title: Google Generative AI description: Learn how to use Google Generative AI Provider. --- # Google Generative AI Provider The [Google Generative AI](https://ai.google.dev) provider contains language and embedding model support for the [Google Generative AI](https://ai.google.dev/api/rest) APIs. ## Setup The Google provider is available in the `@ai-sdk/google` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/google" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/google" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/google" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `google` from `@ai-sdk/google`: ```ts import { google } from '@ai-sdk/google'; ``` If you need a customized setup, you can import `createGoogleGenerativeAI` from `@ai-sdk/google` and create a provider instance with your settings: ```ts import { createGoogleGenerativeAI } from '@ai-sdk/google'; const google = createGoogleGenerativeAI({ // custom settings }); ``` You can use the following optional settings to customize the Google Generative AI provider instance: - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://generativelanguage.googleapis.com/v1beta`. - **apiKey** _string_ API key that is being sent using the `x-goog-api-key` header. It defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Language Models You can create models that call the [Google Generative AI API](https://ai.google.dev/api/rest) using the provider instance. The first argument is the model id, e.g. `gemini-2.5-flash`. The models support tool calls and some have multi-modal capabilities. ```ts const model = google('gemini-2.5-flash'); ``` You can use Google Generative AI language models to generate text with the `generateText` function: ```ts import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; const { text } = await generateText({ model: google('gemini-2.5-flash'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` Google Generative AI language models can also be used in the `streamText`, `generateObject`, and `streamObject` functions (see [AI SDK Core](/docs/ai-sdk-core)). Google Generative AI also supports some model specific settings that are not part of the [standard call settings](/docs/ai-sdk-core/settings). You can pass them as an options argument: ```ts const model = google('gemini-2.5-flash'); await generateText({ model, providerOptions: { google: { safetySettings: [ { category: 'HARM_CATEGORY_UNSPECIFIED', threshold: 'BLOCK_LOW_AND_ABOVE', }, ], responseModalities: ['TEXT', 'IMAGE'], }, }, }); ``` The following optional provider options are available for Google Generative AI models: - **cachedContent** _string_ Optional. The name of the cached content used as context to serve the prediction. Format: cachedContents/\{cachedContent\} - **structuredOutputs** _boolean_ Optional. Enable structured output. Default is true. This is useful when the JSON Schema contains elements that are not supported by the OpenAPI schema version that Google Generative AI uses. You can use this to disable structured outputs if you need to. See [Troubleshooting: Schema Limitations](#schema-limitations) for more details. - **safetySettings** _Array\<\{ category: string; threshold: string \}\>_ Optional. Safety settings for the model. - **category** _string_ The category of the safety setting. Can be one of the following: - `HARM_CATEGORY_HATE_SPEECH` - `HARM_CATEGORY_DANGEROUS_CONTENT` - `HARM_CATEGORY_HARASSMENT` - `HARM_CATEGORY_SEXUALLY_EXPLICIT` - **threshold** _string_ The threshold of the safety setting. Can be one of the following: - `HARM_BLOCK_THRESHOLD_UNSPECIFIED` - `BLOCK_LOW_AND_ABOVE` - `BLOCK_MEDIUM_AND_ABOVE` - `BLOCK_ONLY_HIGH` - `BLOCK_NONE` - **responseModalities** _string[]_ The modalities to use for the response. The following modalities are supported: `TEXT`, `IMAGE`. When not defined or empty, the model defaults to returning only text. - **thinkingConfig** _\{ thinkingBudget: number; includeThoughts: boolean \}_ Optional. Configuration for the model's thinking process. Only supported by specific [Google Generative AI models](https://ai.google.dev/gemini-api/docs/thinking). - **thinkingBudget** _number_ Optional. Gives the model guidance on the number of thinking tokens it can use when generating a response. Setting it to 0 disables thinking, if the model supports it. For more information about the possible value ranges for each model see [Google Generative AI thinking documentation](https://ai.google.dev/gemini-api/docs/thinking#set-budget). - **includeThoughts** _boolean_ Optional. If set to true, thought summaries are returned, which are synthisized versions of the model's raw thoughts and offer insights into the model's internal reasoning process. ### Thinking The Gemini 2.5 series models use an internal "thinking process" that significantly improves their reasoning and multi-step planning abilities, making them highly effective for complex tasks such as coding, advanced mathematics, and data analysis. For more information see [Google Generative AI thinking documentation](https://ai.google.dev/gemini-api/docs/thinking). You can control thinking budgets and enable a thought summary by setting the `thinkingConfig` parameter. ```ts import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; const model = google('gemini-2.5-flash'); const { text, reasoning } = await generateText({ model: model, prompt: 'What is the sum of the first 10 prime numbers?', providerOptions: { google: { thinkingConfig: { thinkingBudget: 8192, includeThoughts: true, }, }, }, }); console.log(text); console.log(reasoning); // Reasoning summary ``` ### File Inputs The Google Generative AI provider supports file inputs, e.g. PDF files. ```ts import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; const result = await generateText({ model: google('gemini-2.5-flash'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', }, ], }, ], }); ``` You can also use YouTube URLs directly: ```ts import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; const result = await generateText({ model: google('gemini-2.5-flash'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Summarize this video', }, { type: 'file', data: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', mediaType: 'video/mp4', }, ], }, ], }); ``` <Note> The AI SDK will automatically download URLs if you pass them as data, except for `https://generativelanguage.googleapis.com/v1beta/files/` and YouTube URLs. You can use the Google Generative AI Files API to upload larger files to that location. YouTube URLs (public or unlisted videos) are supported directly - you can specify one YouTube video URL per request. </Note> See [File Parts](/docs/foundations/prompts#file-parts) for details on how to use files in prompts. ### Cached Content Google Generative AI supports both explicit and implicit caching to help reduce costs on repetitive content. #### Implicit Caching Gemini 2.5 models automatically provide cache cost savings without needing to create an explicit cache. When you send requests that share common prefixes with previous requests, you'll receive a 75% token discount on cached content. To maximize cache hits with implicit caching: - Keep content at the beginning of requests consistent - Add variable content (like user questions) at the end of prompts - Ensure requests meet minimum token requirements: - Gemini 2.5 Flash: 1024 tokens minimum - Gemini 2.5 Pro: 2048 tokens minimum ```ts import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; // Structure prompts with consistent content at the beginning const baseContext = 'You are a cooking assistant with expertise in Italian cuisine. Here are 1000 lasagna recipes for reference...'; const { text: veggieLasagna } = await generateText({ model: google('gemini-2.5-pro'), prompt: `${baseContext}\n\nWrite a vegetarian lasagna recipe for 4 people.`, }); // Second request with same prefix - eligible for cache hit const { text: meatLasagna, providerMetadata } = await generateText({ model: google('gemini-2.5-pro'), prompt: `${baseContext}\n\nWrite a meat lasagna recipe for 12 people.`, }); // Check cached token count in usage metadata console.log('Cached tokens:', providerMetadata.google?.usageMetadata); // e.g. // { // groundingMetadata: null, // safetyRatings: null, // usageMetadata: { // cachedContentTokenCount: 2027, // thoughtsTokenCount: 702, // promptTokenCount: 2152, // candidatesTokenCount: 710, // totalTokenCount: 3564 // } // } ``` <Note> Usage metadata was added to `providerMetadata` in `@ai-sdk/google@1.2.23`. If you are using an older version, usage metadata is available in the raw HTTP `response` body returned as part of the return value from `generateText`. </Note> #### Explicit Caching For guaranteed cost savings, you can still use explicit caching with Gemini 2.5 and 2.0 models. See the [models page](https://ai.google.dev/gemini-api/docs/models) to check if caching is supported for the used model: ```ts import { google } from '@ai-sdk/google'; import { GoogleAICacheManager } from '@google/generative-ai/server'; import { generateText } from 'ai'; const cacheManager = new GoogleAICacheManager( process.env.GOOGLE_GENERATIVE_AI_API_KEY, ); const model = 'gemini-2.5-pro'; const { name: cachedContent } = await cacheManager.create({ model, contents: [ { role: 'user', parts: [{ text: '1000 Lasagna Recipes...' }], }, ], ttlSeconds: 60 * 5, }); const { text: veggieLasangaRecipe } = await generateText({ model: google(model), prompt: 'Write a vegetarian lasagna recipe for 4 people.', providerOptions: { google: { cachedContent, }, }, }); const { text: meatLasangaRecipe } = await generateText({ model: google(model), prompt: 'Write a meat lasagna recipe for 12 people.', providerOptions: { google: { cachedContent, }, }, }); ``` ### Code Execution With [Code Execution](https://ai.google.dev/gemini-api/docs/code-execution), certain models can generate and execute Python code to perform calculations, solve problems, or provide more accurate information. You can enable code execution by adding the `code_execution` tool to your request. ```ts import { google } from '@ai-sdk/google'; import { googleTools } from '@ai-sdk/google/internal'; import { generateText } from 'ai'; const { text, toolCalls, toolResults } = await generateText({ model: google('gemini-2.5-pro'), tools: { code_execution: google.tools.codeExecution({}) }, prompt: 'Use python to calculate the 20th fibonacci number.', }); ``` The response will contain the tool calls and results from the code execution. ### Google Search With [search grounding](https://ai.google.dev/gemini-api/docs/google-search), the model has access to the latest information using Google search. Google search can be used to provide answers around current events: ```ts highlight="8,17-20" import { google } from '@ai-sdk/google'; import { GoogleGenerativeAIProviderMetadata } from '@ai-sdk/google'; import { generateText } from 'ai'; const { text, sources, providerMetadata } = await generateText({ model: google('gemini-2.5-flash'), tools: { google_search: google.tools.googleSearch({}), }, prompt: 'List the top 5 San Francisco news from the past week.' + 'You must include the date of each article.', }); // access the grounding metadata. Casting to the provider metadata type // is optional but provides autocomplete and type safety. const metadata = providerMetadata?.google as | GoogleGenerativeAIProviderMetadata | undefined; const groundingMetadata = metadata?.groundingMetadata; const safetyRatings = metadata?.safetyRatings; ``` When Search Grounding is enabled, the model will include sources in the response. Additionally, the grounding metadata includes detailed information about how search results were used to ground the model's response. Here are the available fields: - **`webSearchQueries`** (`string[] | null`) - Array of search queries used to retrieve information - Example: `["What's the weather in Chicago this weekend?"]` - **`searchEntryPoint`** (`{ renderedContent: string } | null`) - Contains the main search result content used as an entry point - The `renderedContent` field contains the formatted content - **`groundingSupports`** (Array of support objects | null) - Contains details about how specific response parts are supported by search results - Each support object includes: - **`segment`**: Information about the grounded text segment - `text`: The actual text segment - `startIndex`: Starting position in the response - `endIndex`: Ending position in the response - **`groundingChunkIndices`**: References to supporting search result chunks - **`confidenceScores`**: Confidence scores (0-1) for each supporting chunk Example response: ```json { "groundingMetadata": { "webSearchQueries": ["What's the weather in Chicago this weekend?"], "searchEntryPoint": { "renderedContent": "..." }, "groundingSupports": [ { "segment": { "startIndex": 0, "endIndex": 65, "text": "Chicago weather changes rapidly, so layers let you adjust easily." }, "groundingChunkIndices": [0], "confidenceScores": [0.99] } ] } } ``` ### URL Context Google provides a provider-defined URL context tool. The URL context tool allows the you to provide specific URLs that you want the model to analyze directly in from the prompt. ```ts highlight="9,13-17" import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; const { text, sources, providerMetadata } = await generateText({ model: google('gemini-2.5-flash'), prompt: `Based on the document: https://ai.google.dev/gemini-api/docs/url-context. Answer this question: How many links we can consume in one request?`, tools: { url_context: google.tools.urlContext({}), }, }); const metadata = providerMetadata?.google as | GoogleGenerativeAIProviderMetadata | undefined; const groundingMetadata = metadata?.groundingMetadata; const urlContextMetadata = metadata?.urlContextMetadata; ``` The URL context metadata includes detailed information about how the model used the URL context to generate the response. Here are the available fields: - **`urlMetadata`** (`{ retrievedUrl: string; urlRetrievalStatus: string; }[] | null`) - Array of URL context metadata - Each object includes: - **`retrievedUrl`**: The URL of the context - **`urlRetrievalStatus`**: The status of the URL retrieval Example response: ```json { "urlMetadata": [ { "retrievedUrl": "https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai", "urlRetrievalStatus": "URL_RETRIEVAL_STATUS_SUCCESS" } ] } ``` With the URL context tool, you will also get the `groundingMetadata`. ```json "groundingMetadata": { "groundingChunks": [ { "web": { "uri": "https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai", "title": "Google Generative AI - AI SDK Providers" } } ], "groundingSupports": [ { "segment": { "startIndex": 67, "endIndex": 157, "text": "**Installation**: Install the `@ai-sdk/google` module using your preferred package manager" }, "groundingChunkIndices": [ 0 ] }, ] } ``` <Note>You can add up to 20 URLs per request.</Note> <Note> The URL context tool is only supported for Gemini 2.0 Flash models and above. Check the [supported models for URL context tool](https://ai.google.dev/gemini-api/docs/url-context#supported-models). </Note> #### Combine URL Context with Search Grounding You can combine the URL context tool with search grounding to provide the model with the latest information from the web. ```ts highlight="9-10" import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; const { text, sources, providerMetadata } = await generateText({ model: google('gemini-2.5-flash'), prompt: `Based on this context: https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai, tell me how to use Gemini with AI SDK. Also, provide the latest news about AI SDK V5.`, tools: { google_search: google.tools.googleSearch({}), url_context: google.tools.urlContext({}), }, }); const metadata = providerMetadata?.google as | GoogleGenerativeAIProviderMetadata | undefined; const groundingMetadata = metadata?.groundingMetadata; const urlContextMetadata = metadata?.urlContextMetadata; ``` ### Image Outputs The model `gemini-2.0-flash-preview-image-generation` supports image generation. Images are exposed as files in the response. You need to enable image output in the provider options using the `responseModalities` option. ```ts import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; const result = await generateText({ model: google('gemini-2.0-flash-preview-image-generation'), providerOptions: { google: { responseModalities: ['TEXT', 'IMAGE'] }, }, prompt: 'Generate an image of a comic cat', }); for (const file of result.files) { if (file.mediaType.startsWith('image/')) { // show the image } } ``` ### Safety Ratings The safety ratings provide insight into the safety of the model's response. See [Google AI documentation on safety settings](https://ai.google.dev/gemini-api/docs/safety-settings). Example response excerpt: ```json { "safetyRatings": [ { "category": "HARM_CATEGORY_HATE_SPEECH", "probability": "NEGLIGIBLE", "probabilityScore": 0.11027937, "severity": "HARM_SEVERITY_LOW", "severityScore": 0.28487435 }, { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "probability": "HIGH", "blocked": true, "probabilityScore": 0.95422274, "severity": "HARM_SEVERITY_MEDIUM", "severityScore": 0.43398145 }, { "category": "HARM_CATEGORY_HARASSMENT", "probability": "NEGLIGIBLE", "probabilityScore": 0.11085559, "severity": "HARM_SEVERITY_NEGLIGIBLE", "severityScore": 0.19027223 }, { "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability": "NEGLIGIBLE", "probabilityScore": 0.22901751, "severity": "HARM_SEVERITY_NEGLIGIBLE", "severityScore": 0.09089675 } ] } ``` ### Troubleshooting #### Schema Limitations The Google Generative AI API uses a subset of the OpenAPI 3.0 schema, which does not support features such as unions. The errors that you get in this case look like this: `GenerateContentRequest.generation_config.response_schema.properties[occupation].type: must be specified` By default, structured outputs are enabled (and for tool calling they are required). You can disable structured outputs for object generation as a workaround: ```ts highlight="3,8" const { object } = await generateObject({ model: google('gemini-2.5-flash'), providerOptions: { google: { structuredOutputs: false, }, }, schema: z.object({ name: z.string(), age: z.number(), contact: z.union([ z.object({ type: z.literal('email'), value: z.string(), }), z.object({ type: z.literal('phone'), value: z.string(), }), ]), }), prompt: 'Generate an example person for testing.', }); ``` The following Zod features are known to not work with Google Generative AI: - `z.union` - `z.record` ### Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | Google Search | URL Context | | ------------------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `gemini-2.5-pro` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gemini-2.5-flash` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gemini-2.5-flash-lite` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gemini-2.5-flash-lite-preview-06-17` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gemini-2.0-flash` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gemini-1.5-pro` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `gemini-1.5-pro-latest` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `gemini-1.5-flash` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `gemini-1.5-flash-latest` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `gemini-1.5-flash-8b` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `gemini-1.5-flash-8b-latest` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Note> The table above lists popular models. Please see the [Google Generative AI docs](https://ai.google.dev/gemini-api/docs/models/) for a full list of available models. The table above lists popular models. You can also pass any available provider model ID as a string if needed. </Note> ## Gemma Models You can use [Gemma models](https://deepmind.google/models/gemma/) with the Google Generative AI API. Gemma models don't natively support the `systemInstruction` parameter, but the provider automatically handles system instructions by prepending them to the first user message. This allows you to use system instructions with Gemma models seamlessly: ```ts import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; const { text } = await generateText({ model: google('gemma-3-27b-it'), system: 'You are a helpful assistant that responds concisely.', prompt: 'What is machine learning?', }); ``` The system instruction is automatically formatted and included in the conversation, so Gemma models can follow the guidance without any additional configuration. ## Embedding Models You can create models that call the [Google Generative AI embeddings API](https://ai.google.dev/gemini-api/docs/embeddings) using the `.textEmbedding()` factory method. ```ts const model = google.textEmbedding('gemini-embedding-001'); ``` The Google Generative AI provider sends API calls to the right endpoint based on the type of embedding: - **Single embeddings**: When embedding a single value with `embed()`, the provider uses the single `:embedContent` endpoint, which typically has higher rate limits compared to the batch endpoint. - **Batch embeddings**: When embedding multiple values with `embedMany()` or multiple values in `embed()`, the provider uses the `:batchEmbedContents` endpoint. Google Generative AI embedding models support aditional settings. You can pass them as an options argument: ```ts import { google } from '@ai-sdk/google'; import { embed } from 'ai'; const model = google.textEmbedding('gemini-embedding-001'); const { embedding } = await embed({ model, value: 'sunny day at the beach', providerOptions: { google: { outputDimensionality: 512, // optional, number of dimensions for the embedding taskType: 'SEMANTIC_SIMILARITY', // optional, specifies the task type for generating embeddings }, }, }); ``` The following optional provider options are available for Google Generative AI embedding models: - **outputDimensionality**: _number_ Optional reduced dimension for the output embedding. If set, excessive values in the output embedding are truncated from the end. - **taskType**: _string_ Optional. Specifies the task type for generating embeddings. Supported task types include: - `SEMANTIC_SIMILARITY`: Optimized for text similarity. - `CLASSIFICATION`: Optimized for text classification. - `CLUSTERING`: Optimized for clustering texts based on similarity. - `RETRIEVAL_DOCUMENT`: Optimized for document retrieval. - `RETRIEVAL_QUERY`: Optimized for query-based retrieval. - `QUESTION_ANSWERING`: Optimized for answering questions. - `FACT_VERIFICATION`: Optimized for verifying factual information. - `CODE_RETRIEVAL_QUERY`: Optimized for retrieving code blocks based on natural language queries. ### Model Capabilities | Model | Default Dimensions | Custom Dimensions | | ---------------------- | ------------------ | ------------------- | | `gemini-embedding-001` | 3072 | <Check size={18} /> | | `text-embedding-004` | 768 | <Check size={18} /> | ## Image Models You can create [Imagen](https://ai.google.dev/gemini-api/imagen) models that call the Google Generative AI API using the `.image()` factory method. For more on image generation with the AI SDK see [generateImage()](/docs/reference/ai-sdk-core/generate-image). ```ts import { google } from '@ai-sdk/google'; import { experimental_generateImage as generateImage } from 'ai'; const { image } = await generateImage({ model: google.image('imagen-3.0-generate-002'), prompt: 'A futuristic cityscape at sunset', aspectRatio: '16:9', }); ``` Further configuration can be done using Google provider options. You can validate the provider options using the `GoogleGenerativeAIImageProviderOptions` type. ```ts import { google } from '@ai-sdk/google'; import { GoogleGenerativeAIImageProviderOptions } from '@ai-sdk/google'; import { experimental_generateImage as generateImage } from 'ai'; const { image } = await generateImage({ model: google.image('imagen-3.0-generate-002'), providerOptions: { google: { personGeneration: 'dont_allow', } satisfies GoogleGenerativeAIImageProviderOptions, }, // ... }); ``` The following provider options are available: - **personGeneration** `allow_adult` | `allow_all` | `dont_allow` Whether to allow person generation. Defaults to `allow_adult`. <Note> Imagen models do not support the `size` parameter. Use the `aspectRatio` parameter instead. </Note> #### Model Capabilities | Model | Aspect Ratios | | ------------------------- | ------------------------- | | `imagen-3.0-generate-002` | 1:1, 3:4, 4:3, 9:16, 16:9 | --- File: /ai/content/providers/01-ai-sdk-providers/150-hume.mdx --- --- title: Hume description: Learn how to use the Hume provider for the AI SDK. --- # Hume Provider The [Hume](https://hume.ai/) provider contains language model support for the Hume transcription API. ## Setup The Hume provider is available in the `@ai-sdk/hume` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/hume" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/hume" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/hume" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `hume` from `@ai-sdk/hume`: ```ts import { hume } from '@ai-sdk/hume'; ``` If you need a customized setup, you can import `createHume` from `@ai-sdk/hume` and create a provider instance with your settings: ```ts import { createHume } from '@ai-sdk/hume'; const hume = createHume({ // custom settings, e.g. fetch: customFetch, }); ``` You can use the following optional settings to customize the Hume provider instance: - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `HUME_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Speech Models You can create models that call the [Hume speech API](https://dev.hume.ai/docs/text-to-speech-tts/overview) using the `.speech()` factory method. ```ts const model = hume.speech(); ``` You can also pass additional provider-specific options using the `providerOptions` argument. For example, supplying a voice to use for the generated audio. ```ts highlight="6" import { experimental_generateSpeech as generateSpeech } from 'ai'; import { hume } from '@ai-sdk/hume'; const result = await generateSpeech({ model: hume.speech(), text: 'Hello, world!', voice: 'd8ab67c6-953d-4bd8-9370-8fa53a0f1453', providerOptions: { hume: {} }, }); ``` The following provider options are available: - **context** _object_ Either: - `{ generationId: string }` - A generation ID to use for context. - `{ utterances: HumeUtterance[] }` - An array of utterance objects for context. ### Model Capabilities | Model | Instructions | | --------- | ------------------- | | `default` | <Check size={18} /> | --- File: /ai/content/providers/01-ai-sdk-providers/16-google-vertex.mdx --- --- title: Google Vertex AI description: Learn how to use the Google Vertex AI provider. --- # Google Vertex Provider The Google Vertex provider for the [AI SDK](/docs) contains language model support for the [Google Vertex AI](https://cloud.google.com/vertex-ai) APIs. This includes support for [Google's Gemini models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models) and [Anthropic's Claude partner models](https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude). <Note> The Google Vertex provider is compatible with both Node.js and Edge runtimes. The Edge runtime is supported through the `@ai-sdk/google-vertex/edge` sub-module. More details can be found in the [Google Vertex Edge Runtime](#google-vertex-edge-runtime) and [Google Vertex Anthropic Edge Runtime](#google-vertex-anthropic-edge-runtime) sections below. </Note> ## Setup The Google Vertex and Google Vertex Anthropic providers are both available in the `@ai-sdk/google-vertex` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/google-vertex" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/google-vertex" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/google-vertex @google-cloud/vertexai" dark /> </Tab> </Tabs> ## Google Vertex Provider Usage The Google Vertex provider instance is used to create model instances that call the Vertex AI API. The models available with this provider include [Google's Gemini models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models). If you're looking to use [Anthropic's Claude models](https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude), see the [Google Vertex Anthropic Provider](#google-vertex-anthropic-provider-usage) section below. ### Provider Instance You can import the default provider instance `vertex` from `@ai-sdk/google-vertex`: ```ts import { vertex } from '@ai-sdk/google-vertex'; ``` If you need a customized setup, you can import `createVertex` from `@ai-sdk/google-vertex` and create a provider instance with your settings: ```ts import { createVertex } from '@ai-sdk/google-vertex'; const vertex = createVertex({ project: 'my-project', // optional location: 'us-central1', // optional }); ``` Google Vertex supports two different authentication implementations depending on your runtime environment. #### Node.js Runtime The Node.js runtime is the default runtime supported by the AI SDK. It supports all standard Google Cloud authentication options through the [`google-auth-library`](https://github.com/googleapis/google-auth-library-nodejs?tab=readme-ov-file#ways-to-authenticate). Typical use involves setting a path to a json credentials file in the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. The credentials file can be obtained from the [Google Cloud Console](https://console.cloud.google.com/apis/credentials). If you want to customize the Google authentication options you can pass them as options to the `createVertex` function, for example: ```ts import { createVertex } from '@ai-sdk/google-vertex'; const vertex = createVertex({ googleAuthOptions: { credentials: { client_email: 'my-email', private_key: 'my-private-key', }, }, }); ``` ##### Optional Provider Settings You can use the following optional settings to customize the provider instance: - **project** _string_ The Google Cloud project ID that you want to use for the API calls. It uses the `GOOGLE_VERTEX_PROJECT` environment variable by default. - **location** _string_ The Google Cloud location that you want to use for the API calls, e.g. `us-central1`. It uses the `GOOGLE_VERTEX_LOCATION` environment variable by default. - **googleAuthOptions** _object_ Optional. The Authentication options used by the [Google Auth Library](https://github.com/googleapis/google-auth-library-nodejs/). See also the [GoogleAuthOptions](https://github.com/googleapis/google-auth-library-nodejs/blob/08978822e1b7b5961f0e355df51d738e012be392/src/auth/googleauth.ts#L87C18-L87C35) interface. - **authClient** _object_ An `AuthClient` to use. - **keyFilename** _string_ Path to a .json, .pem, or .p12 key file. - **keyFile** _string_ Path to a .json, .pem, or .p12 key file. - **credentials** _object_ Object containing client_email and private_key properties, or the external account client options. - **clientOptions** _object_ Options object passed to the constructor of the client. - **scopes** _string | string[]_ Required scopes for the desired API request. - **projectId** _string_ Your project ID. - **universeDomain** _string_ The default service domain for a given Cloud universe. - **headers** _Resolvable&lt;Record&lt;string, string | undefined&gt;&gt;_ Headers to include in the requests. Can be provided in multiple formats: - A record of header key-value pairs: `Record<string, string | undefined>` - A function that returns headers: `() => Record<string, string | undefined>` - An async function that returns headers: `async () => Record<string, string | undefined>` - A promise that resolves to headers: `Promise<Record<string, string | undefined>>` - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. - **baseURL** _string_ Optional. Base URL for the Google Vertex API calls e.g. to use proxy servers. By default, it is constructed using the location and project: `https://${location}-aiplatform.googleapis.com/v1/projects/${project}/locations/${location}/publishers/google` <a id="google-vertex-edge-runtime"></a> #### Edge Runtime Edge runtimes (like Vercel Edge Functions and Cloudflare Workers) are lightweight JavaScript environments that run closer to users at the network edge. They only provide a subset of the standard Node.js APIs. For example, direct file system access is not available, and many Node.js-specific libraries (including the standard Google Auth library) are not compatible. The Edge runtime version of the Google Vertex provider supports Google's [Application Default Credentials](https://github.com/googleapis/google-auth-library-nodejs?tab=readme-ov-file#application-default-credentials) through environment variables. The values can be obtained from a json credentials file from the [Google Cloud Console](https://console.cloud.google.com/apis/credentials). You can import the default provider instance `vertex` from `@ai-sdk/google-vertex/edge`: ```ts import { vertex } from '@ai-sdk/google-vertex/edge'; ``` <Note> The `/edge` sub-module is included in the `@ai-sdk/google-vertex` package, so you don't need to install it separately. You must import from `@ai-sdk/google-vertex/edge` to differentiate it from the Node.js provider. </Note> If you need a customized setup, you can import `createVertex` from `@ai-sdk/google-vertex/edge` and create a provider instance with your settings: ```ts import { createVertex } from '@ai-sdk/google-vertex/edge'; const vertex = createVertex({ project: 'my-project', // optional location: 'us-central1', // optional }); ``` For Edge runtime authentication, you'll need to set these environment variables from your Google Default Application Credentials JSON file: - `GOOGLE_CLIENT_EMAIL` - `GOOGLE_PRIVATE_KEY` - `GOOGLE_PRIVATE_KEY_ID` (optional) These values can be obtained from a service account JSON file from the [Google Cloud Console](https://console.cloud.google.com/apis/credentials). ##### Optional Provider Settings You can use the following optional settings to customize the provider instance: - **project** _string_ The Google Cloud project ID that you want to use for the API calls. It uses the `GOOGLE_VERTEX_PROJECT` environment variable by default. - **location** _string_ The Google Cloud location that you want to use for the API calls, e.g. `us-central1`. It uses the `GOOGLE_VERTEX_LOCATION` environment variable by default. - **googleCredentials** _object_ Optional. The credentials used by the Edge provider for authentication. These credentials are typically set through environment variables and are derived from a service account JSON file. - **clientEmail** _string_ The client email from the service account JSON file. Defaults to the contents of the `GOOGLE_CLIENT_EMAIL` environment variable. - **privateKey** _string_ The private key from the service account JSON file. Defaults to the contents of the `GOOGLE_PRIVATE_KEY` environment variable. - **privateKeyId** _string_ The private key ID from the service account JSON file (optional). Defaults to the contents of the `GOOGLE_PRIVATE_KEY_ID` environment variable. - **headers** _Resolvable&lt;Record&lt;string, string | undefined&gt;&gt;_ Headers to include in the requests. Can be provided in multiple formats: - A record of header key-value pairs: `Record<string, string | undefined>` - A function that returns headers: `() => Record<string, string | undefined>` - An async function that returns headers: `async () => Record<string, string | undefined>` - A promise that resolves to headers: `Promise<Record<string, string | undefined>>` - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ### Language Models You can create models that call the Vertex API using the provider instance. The first argument is the model id, e.g. `gemini-1.5-pro`. ```ts const model = vertex('gemini-1.5-pro'); ``` <Note> If you are using [your own models](https://cloud.google.com/vertex-ai/docs/training-overview), the name of your model needs to start with `projects/`. </Note> Google Vertex models support also some model specific settings that are not part of the [standard call settings](/docs/ai-sdk-core/settings). You can pass them as an options argument: ```ts const model = vertex('gemini-1.5-pro'); await generateText({ model, providerOptions: { google: { safetySettings: [ { category: 'HARM_CATEGORY_UNSPECIFIED', threshold: 'BLOCK_LOW_AND_ABOVE', }, ], }, }, }); ``` The following optional provider options are available for Google Vertex models: - **structuredOutputs** _boolean_ Optional. Enable structured output. Default is true. This is useful when the JSON Schema contains elements that are not supported by the OpenAPI schema version that Google Vertex uses. You can use this to disable structured outputs if you need to. See [Troubleshooting: Schema Limitations](#schema-limitations) for more details. - **safetySettings** _Array\<\{ category: string; threshold: string \}\>_ Optional. Safety settings for the model. - **category** _string_ The category of the safety setting. Can be one of the following: - `HARM_CATEGORY_UNSPECIFIED` - `HARM_CATEGORY_HATE_SPEECH` - `HARM_CATEGORY_DANGEROUS_CONTENT` - `HARM_CATEGORY_HARASSMENT` - `HARM_CATEGORY_SEXUALLY_EXPLICIT` - `HARM_CATEGORY_CIVIC_INTEGRITY` - **threshold** _string_ The threshold of the safety setting. Can be one of the following: - `HARM_BLOCK_THRESHOLD_UNSPECIFIED` - `BLOCK_LOW_AND_ABOVE` - `BLOCK_MEDIUM_AND_ABOVE` - `BLOCK_ONLY_HIGH` - `BLOCK_NONE` - **useSearchGrounding** _boolean_ Optional. When enabled, the model will [use Google search to ground the response](https://cloud.google.com/vertex-ai/generative-ai/docs/grounding/overview). - **audioTimestamp** _boolean_ Optional. Enables timestamp understanding for audio files. Defaults to false. This is useful for generating transcripts with accurate timestamps. Consult [Google's Documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/audio-understanding) for usage details. You can use Google Vertex language models to generate text with the `generateText` function: ```ts highlight="1,4" import { vertex } from '@ai-sdk/google-vertex'; import { generateText } from 'ai'; const { text } = await generateText({ model: vertex('gemini-1.5-pro'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` Google Vertex language models can also be used in the `streamText` function (see [AI SDK Core](/docs/ai-sdk-core)). #### Code Execution With [Code Execution](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/code-execution), certain Gemini models on Vertex AI can generate and execute Python code. This allows the model to perform calculations, data manipulation, and other programmatic tasks to enhance its responses. You can enable code execution by adding the `code_execution` tool to your request. ```ts import { vertex } from '@ai-sdk/google-vertex'; import { googleTools } from '@ai-sdk/google/internal'; import { generateText } from 'ai'; const result = await generateText({ model: vertex('gemini-2.5-pro'), tools: { code_execution: googleTools.codeExecution({}) }, prompt: 'Use python to calculate 20th fibonacci number. Then find the nearest palindrome to it.', }); ``` The response will contain `tool-call` and `tool-result` parts for the executed code. #### Reasoning (Thinking Tokens) Google Vertex AI, through its support for Gemini models, can also emit "thinking" tokens, representing the model's reasoning process. The AI SDK exposes these as reasoning information. To enable thinking tokens for compatible Gemini models via Vertex, set `includeThoughts: true` in the `thinkingConfig` provider option. Since the Vertex provider uses the Google provider's underlying language model, these options are passed through `providerOptions.google`: ```ts import { vertex } from '@ai-sdk/google-vertex'; import { GoogleGenerativeAIProviderOptions } from '@ai-sdk/google'; // Note: importing from @ai-sdk/google import { generateText, streamText } from 'ai'; // For generateText: const { text, reasoning, reasoningDetails } = await generateText({ model: vertex('gemini-2.0-flash-001'), // Or other supported model via Vertex providerOptions: { google: { // Options are nested under 'google' for Vertex provider thinkingConfig: { includeThoughts: true, // thinkingBudget: 2048, // Optional }, } satisfies GoogleGenerativeAIProviderOptions, }, prompt: 'Explain quantum computing in simple terms.', }); console.log('Reasoning:', reasoning); console.log('Reasoning Details:', reasoningDetails); console.log('Final Text:', text); // For streamText: const result = streamText({ model: vertex('gemini-2.0-flash-001'), // Or other supported model via Vertex providerOptions: { google: { // Options are nested under 'google' for Vertex provider thinkingConfig: { includeThoughts: true, // thinkingBudget: 2048, // Optional }, } satisfies GoogleGenerativeAIProviderOptions, }, prompt: 'Explain quantum computing in simple terms.', }); for await (const part of result.fullStream) { if (part.type === 'reasoning') { process.stdout.write(`THOUGHT: ${part.textDelta}\n`); } else if (part.type === 'text-delta') { process.stdout.write(part.textDelta); } } ``` When `includeThoughts` is true, parts of the API response marked with `thought: true` will be processed as reasoning. - In `generateText`, these contribute to the `reasoning` (string) and `reasoningDetails` (array) fields. - In `streamText`, these are emitted as `reasoning` stream parts. <Note> Refer to the [Google Vertex AI documentation on "thinking"](https://cloud.google.com/vertex-ai/generative-ai/docs/thinking) for model compatibility and further details. </Note> #### File Inputs The Google Vertex provider supports file inputs, e.g. PDF files. ```ts import { vertex } from '@ai-sdk/google-vertex'; import { generateText } from 'ai'; const { text } = await generateText({ model: vertex('gemini-1.5-pro'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', }, ], }, ], }); ``` <Note> The AI SDK will automatically download URLs if you pass them as data, except for `gs://` URLs. You can use the Google Cloud Storage API to upload larger files to that location. </Note> See [File Parts](/docs/foundations/prompts#file-parts) for details on how to use files in prompts. #### Search Grounding With [search grounding](https://cloud.google.com/vertex-ai/generative-ai/docs/grounding/overview), the model has access to the latest information using Google search. Search grounding can be used to provide answers around current events: ```ts highlight="7,14-20" import { vertex } from '@ai-sdk/google-vertex'; import { GoogleGenerativeAIProviderMetadata } from '@ai-sdk/google'; import { generateText } from 'ai'; const { text, providerMetadata } = await generateText({ model: vertex('gemini-1.5-pro'), providerOptions: { google: { useSearchGrounding: true, }, }, prompt: 'List the top 5 San Francisco news from the past week.' + 'You must include the date of each article.', }); // access the grounding metadata. Casting to the provider metadata type // is optional but provides autocomplete and type safety. const metadata = providerMetadata?.google as | GoogleGenerativeAIProviderMetadata | undefined; const groundingMetadata = metadata?.groundingMetadata; const safetyRatings = metadata?.safetyRatings; ``` The grounding metadata includes detailed information about how search results were used to ground the model's response. Here are the available fields: - **`webSearchQueries`** (`string[] | null`) - Array of search queries used to retrieve information - Example: `["What's the weather in Chicago this weekend?"]` - **`searchEntryPoint`** (`{ renderedContent: string } | null`) - Contains the main search result content used as an entry point - The `renderedContent` field contains the formatted content - **`groundingSupports`** (Array of support objects | null) - Contains details about how specific response parts are supported by search results - Each support object includes: - **`segment`**: Information about the grounded text segment - `text`: The actual text segment - `startIndex`: Starting position in the response - `endIndex`: Ending position in the response - **`groundingChunkIndices`**: References to supporting search result chunks - **`confidenceScores`**: Confidence scores (0-1) for each supporting chunk Example response excerpt: ```json { "groundingMetadata": { "retrievalQueries": ["What's the weather in Chicago this weekend?"], "searchEntryPoint": { "renderedContent": "..." }, "groundingSupports": [ { "segment": { "startIndex": 0, "endIndex": 65, "text": "Chicago weather changes rapidly, so layers let you adjust easily." }, "groundingChunkIndices": [0], "confidenceScores": [0.99] } ] } } ``` <Note> The Google Vertex provider does not yet support [dynamic retrieval mode and threshold](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/ground-gemini#dynamic-retrieval). </Note> ### Sources When you use [Search Grounding](#search-grounding), the model will include sources in the response. You can access them using the `sources` property of the result: ```ts import { vertex } from '@ai-sdk/google-vertex'; import { generateText } from 'ai'; const { sources } = await generateText({ model: vertex('gemini-1.5-pro'), providerOptions: { google: { useSearchGrounding: true, }, }, prompt: 'List the top 5 San Francisco news from the past week.', }); ``` ### Safety Ratings The safety ratings provide insight into the safety of the model's response. See [Google Vertex AI documentation on configuring safety filters](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/configure-safety-filters). Example response excerpt: ```json { "safetyRatings": [ { "category": "HARM_CATEGORY_HATE_SPEECH", "probability": "NEGLIGIBLE", "probabilityScore": 0.11027937, "severity": "HARM_SEVERITY_LOW", "severityScore": 0.28487435 }, { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "probability": "HIGH", "blocked": true, "probabilityScore": 0.95422274, "severity": "HARM_SEVERITY_MEDIUM", "severityScore": 0.43398145 }, { "category": "HARM_CATEGORY_HARASSMENT", "probability": "NEGLIGIBLE", "probabilityScore": 0.11085559, "severity": "HARM_SEVERITY_NEGLIGIBLE", "severityScore": 0.19027223 }, { "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability": "NEGLIGIBLE", "probabilityScore": 0.22901751, "severity": "HARM_SEVERITY_NEGLIGIBLE", "severityScore": 0.09089675 } ] } ``` For more details, see the [Google Vertex AI documentation on grounding with Google Search](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/ground-gemini#ground-to-search). ### Troubleshooting #### Schema Limitations The Google Vertex API uses a subset of the OpenAPI 3.0 schema, which does not support features such as unions. The errors that you get in this case look like this: `GenerateContentRequest.generation_config.response_schema.properties[occupation].type: must be specified` By default, structured outputs are enabled (and for tool calling they are required). You can disable structured outputs for object generation as a workaround: ```ts highlight="3,8" const result = await generateObject({ model: vertex('gemini-1.5-pro'), providerOptions: { google: { structuredOutputs: false, }, }, schema: z.object({ name: z.string(), age: z.number(), contact: z.union([ z.object({ type: z.literal('email'), value: z.string(), }), z.object({ type: z.literal('phone'), value: z.string(), }), ]), }), prompt: 'Generate an example person for testing.', }); ``` The following Zod features are known to not work with Google Vertex: - `z.union` - `z.record` ### Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | ---------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `gemini-2.0-flash-001` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gemini-2.0-flash-exp` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gemini-1.5-flash` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gemini-1.5-pro` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Note> The table above lists popular models. Please see the [Google Vertex AI docs](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#supported-models) for a full list of available models. The table above lists popular models. You can also pass any available provider model ID as a string if needed. </Note> ### Embedding Models You can create models that call the Google Vertex AI embeddings API using the `.textEmbedding()` factory method: ```ts const model = vertex.textEmbedding('text-embedding-004'); ``` Google Vertex AI embedding models support additional settings. You can pass them as an options argument: ```ts import { vertex } from '@ai-sdk/google-vertex'; import { embed } from 'ai'; const model = vertex.textEmbedding('text-embedding-004'); const { embedding } = await embed({ model, value: 'sunny day at the beach', providerOptions: { google: { outputDimensionality: 512, // optional, number of dimensions for the embedding taskType: 'SEMANTIC_SIMILARITY', // optional, specifies the task type for generating embeddings }, }, }); ``` The following optional provider options are available for Google Vertex AI embedding models: - **outputDimensionality**: _number_ Optional reduced dimension for the output embedding. If set, excessive values in the output embedding are truncated from the end. - **taskType**: _string_ Optional. Specifies the task type for generating embeddings. Supported task types include: - `SEMANTIC_SIMILARITY`: Optimized for text similarity. - `CLASSIFICATION`: Optimized for text classification. - `CLUSTERING`: Optimized for clustering texts based on similarity. - `RETRIEVAL_DOCUMENT`: Optimized for document retrieval. - `RETRIEVAL_QUERY`: Optimized for query-based retrieval. - `QUESTION_ANSWERING`: Optimized for answering questions. - `FACT_VERIFICATION`: Optimized for verifying factual information. - `CODE_RETRIEVAL_QUERY`: Optimized for retrieving code blocks based on natural language queries. #### Model Capabilities | Model | Max Values Per Call | Parallel Calls | | -------------------- | ------------------- | ------------------- | | `text-embedding-004` | 2048 | <Check size={18} /> | <Note> The table above lists popular models. You can also pass any available provider model ID as a string if needed. </Note> ### Image Models You can create [Imagen](https://cloud.google.com/vertex-ai/generative-ai/docs/image/overview) models that call the [Imagen on Vertex AI API](https://cloud.google.com/vertex-ai/generative-ai/docs/image/generate-images) using the `.image()` factory method. For more on image generation with the AI SDK see [generateImage()](/docs/reference/ai-sdk-core/generate-image). ```ts import { vertex } from '@ai-sdk/google-vertex'; import { experimental_generateImage as generateImage } from 'ai'; const { image } = await generateImage({ model: vertex.image('imagen-3.0-generate-002'), prompt: 'A futuristic cityscape at sunset', aspectRatio: '16:9', }); ``` Further configuration can be done using Google Vertex provider options. You can validate the provider options using the `GoogleVertexImageProviderOptions` type. ```ts import { vertex } from '@ai-sdk/google-vertex'; import { GoogleVertexImageProviderOptions } from '@ai-sdk/google-vertex'; import { experimental_generateImage as generateImage } from 'ai'; const { image } = await generateImage({ model: vertex.image('imagen-3.0-generate-002'), providerOptions: { vertex: { negativePrompt: 'pixelated, blurry, low-quality', } satisfies GoogleVertexImageProviderOptions, }, // ... }); ``` The following provider options are available: - **negativePrompt** _string_ A description of what to discourage in the generated images. - **personGeneration** `allow_adult` | `allow_all` | `dont_allow` Whether to allow person generation. Defaults to `allow_adult`. - **safetySetting** `block_low_and_above` | `block_medium_and_above` | `block_only_high` | `block_none` Whether to block unsafe content. Defaults to `block_medium_and_above`. - **addWatermark** _boolean_ Whether to add an invisible watermark to the generated images. Defaults to `true`. - **storageUri** _string_ Cloud Storage URI to store the generated images. <Note> Imagen models do not support the `size` parameter. Use the `aspectRatio` parameter instead. </Note> Additional information about the images can be retrieved using Google Vertex meta data. ```ts import { vertex } from '@ai-sdk/google-vertex'; import { GoogleVertexImageProviderOptions } from '@ai-sdk/google-vertex'; import { experimental_generateImage as generateImage } from 'ai'; const { image, providerMetadata } = await generateImage({ model: vertex.image('imagen-3.0-generate-002'), prompt: 'A futuristic cityscape at sunset', aspectRatio: '16:9', }); console.log( `Revised prompt: ${providerMetadata.vertex.images[0].revisedPrompt}`, ); ``` #### Model Capabilities | Model | Aspect Ratios | | ----------------------------------------- | ------------------------- | | `imagen-3.0-generate-001` | 1:1, 3:4, 4:3, 9:16, 16:9 | | `imagen-3.0-generate-002` | 1:1, 3:4, 4:3, 9:16, 16:9 | | `imagen-3.0-fast-generate-001` | 1:1, 3:4, 4:3, 9:16, 16:9 | | `imagen-4.0-generate-preview-06-06` | 1:1, 3:4, 4:3, 9:16, 16:9 | | `imagen-4.0-fast-generate-preview-06-06` | 1:1, 3:4, 4:3, 9:16, 16:9 | | `imagen-4.0-ultra-generate-preview-06-06` | 1:1, 3:4, 4:3, 9:16, 16:9 | ## Google Vertex Anthropic Provider Usage The Google Vertex Anthropic provider for the [AI SDK](/docs) offers support for Anthropic's Claude models through the Google Vertex AI APIs. This section provides details on how to set up and use the Google Vertex Anthropic provider. ### Provider Instance You can import the default provider instance `vertexAnthropic` from `@ai-sdk/google-vertex/anthropic`: ```typescript import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; ``` If you need a customized setup, you can import `createVertexAnthropic` from `@ai-sdk/google-vertex/anthropic` and create a provider instance with your settings: ```typescript import { createVertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; const vertexAnthropic = createVertexAnthropic({ project: 'my-project', // optional location: 'us-central1', // optional }); ``` #### Node.js Runtime For Node.js environments, the Google Vertex Anthropic provider supports all standard Google Cloud authentication options through the `google-auth-library`. You can customize the authentication options by passing them to the `createVertexAnthropic` function: ```typescript import { createVertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; const vertexAnthropic = createVertexAnthropic({ googleAuthOptions: { credentials: { client_email: 'my-email', private_key: 'my-private-key', }, }, }); ``` ##### Optional Provider Settings You can use the following optional settings to customize the Google Vertex Anthropic provider instance: - **project** _string_ The Google Cloud project ID that you want to use for the API calls. It uses the `GOOGLE_VERTEX_PROJECT` environment variable by default. - **location** _string_ The Google Cloud location that you want to use for the API calls, e.g. `us-central1`. It uses the `GOOGLE_VERTEX_LOCATION` environment variable by default. - **googleAuthOptions** _object_ Optional. The Authentication options used by the [Google Auth Library](https://github.com/googleapis/google-auth-library-nodejs/). See also the [GoogleAuthOptions](https://github.com/googleapis/google-auth-library-nodejs/blob/08978822e1b7b5961f0e355df51d738e012be392/src/auth/googleauth.ts#L87C18-L87C35) interface. - **authClient** _object_ An `AuthClient` to use. - **keyFilename** _string_ Path to a .json, .pem, or .p12 key file. - **keyFile** _string_ Path to a .json, .pem, or .p12 key file. - **credentials** _object_ Object containing client_email and private_key properties, or the external account client options. - **clientOptions** _object_ Options object passed to the constructor of the client. - **scopes** _string | string[]_ Required scopes for the desired API request. - **projectId** _string_ Your project ID. - **universeDomain** _string_ The default service domain for a given Cloud universe. - **headers** _Resolvable&lt;Record&lt;string, string | undefined&gt;&gt;_ Headers to include in the requests. Can be provided in multiple formats: - A record of header key-value pairs: `Record<string, string | undefined>` - A function that returns headers: `() => Record<string, string | undefined>` - An async function that returns headers: `async () => Record<string, string | undefined>` - A promise that resolves to headers: `Promise<Record<string, string | undefined>>` - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. <a id="google-vertex-anthropic-edge-runtime"></a> #### Edge Runtime Edge runtimes (like Vercel Edge Functions and Cloudflare Workers) are lightweight JavaScript environments that run closer to users at the network edge. They only provide a subset of the standard Node.js APIs. For example, direct file system access is not available, and many Node.js-specific libraries (including the standard Google Auth library) are not compatible. The Edge runtime version of the Google Vertex Anthropic provider supports Google's [Application Default Credentials](https://github.com/googleapis/google-auth-library-nodejs?tab=readme-ov-file#application-default-credentials) through environment variables. The values can be obtained from a json credentials file from the [Google Cloud Console](https://console.cloud.google.com/apis/credentials). For Edge runtimes, you can import the provider instance from `@ai-sdk/google-vertex/anthropic/edge`: ```typescript import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic/edge'; ``` To customize the setup, use `createVertexAnthropic` from the same module: ```typescript import { createVertexAnthropic } from '@ai-sdk/google-vertex/anthropic/edge'; const vertexAnthropic = createVertexAnthropic({ project: 'my-project', // optional location: 'us-central1', // optional }); ``` For Edge runtime authentication, set these environment variables from your Google Default Application Credentials JSON file: - `GOOGLE_CLIENT_EMAIL` - `GOOGLE_PRIVATE_KEY` - `GOOGLE_PRIVATE_KEY_ID` (optional) ##### Optional Provider Settings You can use the following optional settings to customize the provider instance: - **project** _string_ The Google Cloud project ID that you want to use for the API calls. It uses the `GOOGLE_VERTEX_PROJECT` environment variable by default. - **location** _string_ The Google Cloud location that you want to use for the API calls, e.g. `us-central1`. It uses the `GOOGLE_VERTEX_LOCATION` environment variable by default. - **googleCredentials** _object_ Optional. The credentials used by the Edge provider for authentication. These credentials are typically set through environment variables and are derived from a service account JSON file. - **clientEmail** _string_ The client email from the service account JSON file. Defaults to the contents of the `GOOGLE_CLIENT_EMAIL` environment variable. - **privateKey** _string_ The private key from the service account JSON file. Defaults to the contents of the `GOOGLE_PRIVATE_KEY` environment variable. - **privateKeyId** _string_ The private key ID from the service account JSON file (optional). Defaults to the contents of the `GOOGLE_PRIVATE_KEY_ID` environment variable. - **headers** _Resolvable&lt;Record&lt;string, string | undefined&gt;&gt;_ Headers to include in the requests. Can be provided in multiple formats: - A record of header key-value pairs: `Record<string, string | undefined>` - A function that returns headers: `() => Record<string, string | undefined>` - An async function that returns headers: `async () => Record<string, string | undefined>` - A promise that resolves to headers: `Promise<Record<string, string | undefined>>` - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ### Language Models You can create models that call the [Anthropic Messages API](https://docs.anthropic.com/claude/reference/messages_post) using the provider instance. The first argument is the model id, e.g. `claude-3-haiku-20240307`. Some models have multi-modal capabilities. ```ts const model = anthropic('claude-3-haiku-20240307'); ``` You can use Anthropic language models to generate text with the `generateText` function: ```ts import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText } from 'ai'; const { text } = await generateText({ model: vertexAnthropic('claude-3-haiku-20240307'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` Anthropic language models can also be used in the `streamText`, `generateObject`, and `streamObject` functions (see [AI SDK Core](/docs/ai-sdk-core)). <Note> The Anthropic API returns streaming tool calls all at once after a delay. This causes the `streamObject` function to generate the object fully after a delay instead of streaming it incrementally. </Note> The following optional provider options are available for Anthropic models: - `sendReasoning` _boolean_ Optional. Include reasoning content in requests sent to the model. Defaults to `true`. If you are experiencing issues with the model handling requests involving reasoning content, you can set this to `false` to omit them from the request. - `thinking` _object_ Optional. See [Reasoning section](#reasoning) for more details. ### Reasoning Anthropic has reasoning support for the `claude-3-7-sonnet@20250219` model. You can enable it using the `thinking` provider option and specifying a thinking budget in tokens. ```ts import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText } from 'ai'; const { text, reasoning, reasoningDetails } = await generateText({ model: vertexAnthropic('claude-3-7-sonnet@20250219'), prompt: 'How many people will live in the world in 2040?', providerOptions: { anthropic: { thinking: { type: 'enabled', budgetTokens: 12000 }, }, }, }); console.log(reasoning); // reasoning text console.log(reasoningDetails); // reasoning details including redacted reasoning console.log(text); // text response ``` See [AI SDK UI: Chatbot](/docs/ai-sdk-ui/chatbot#reasoning) for more details on how to integrate reasoning into your chatbot. #### Cache Control <Note> Anthropic cache control is in a Pre-Generally Available (GA) state on Google Vertex. For more see [Google Vertex Anthropic cache control documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/claude-prompt-caching). </Note> In the messages and message parts, you can use the `providerOptions` property to set cache control breakpoints. You need to set the `anthropic` property in the `providerOptions` object to `{ cacheControl: { type: 'ephemeral' } }` to set a cache control breakpoint. The cache creation input tokens are then returned in the `providerMetadata` object for `generateText` and `generateObject`, again under the `anthropic` property. When you use `streamText` or `streamObject`, the response contains a promise that resolves to the metadata. Alternatively you can receive it in the `onFinish` callback. ```ts highlight="8,18-20,29-30" import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText } from 'ai'; const errorMessage = '... long error message ...'; const result = await generateText({ model: vertexAnthropic('claude-3-5-sonnet-20240620'), messages: [ { role: 'user', content: [ { type: 'text', text: 'You are a JavaScript expert.' }, { type: 'text', text: `Error message: ${errorMessage}`, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } }, }, }, { type: 'text', text: 'Explain the error message.' }, ], }, ], }); console.log(result.text); console.log(result.providerMetadata?.anthropic); // e.g. { cacheCreationInputTokens: 2118, cacheReadInputTokens: 0 } ``` You can also use cache control on system messages by providing multiple system messages at the head of your messages array: ```ts highlight="3,9-11" const result = await generateText({ model: vertexAnthropic('claude-3-5-sonnet-20240620'), messages: [ { role: 'system', content: 'Cached system message part', providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } }, }, }, { role: 'system', content: 'Uncached system message part', }, { role: 'user', content: 'User prompt', }, ], }); ``` For more on prompt caching with Anthropic, see [Google Vertex AI's Claude prompt caching documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/claude-prompt-caching) and [Anthropic's Cache Control documentation](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching). ### Computer Use Anthropic provides three built-in tools that can be used to interact with external systems: 1. **Bash Tool**: Allows running bash commands. 2. **Text Editor Tool**: Provides functionality for viewing and editing text files. 3. **Computer Tool**: Enables control of keyboard and mouse actions on a computer. They are available via the `tools` property of the provider instance. For more background see [Anthropic's Computer Use documentation](https://docs.anthropic.com/en/docs/build-with-claude/computer-use). #### Bash Tool The Bash Tool allows running bash commands. Here's how to create and use it: ```ts const bashTool = vertexAnthropic.tools.bash_20241022({ execute: async ({ command, restart }) => { // Implement your bash command execution logic here // Return the result of the command execution }, }); ``` Parameters: - `command` (string): The bash command to run. Required unless the tool is being restarted. - `restart` (boolean, optional): Specifying true will restart this tool. #### Text Editor Tool The Text Editor Tool provides functionality for viewing and editing text files: ```ts const textEditorTool = vertexAnthropic.tools.textEditor_20241022({ execute: async ({ command, path, file_text, insert_line, new_str, old_str, view_range, }) => { // Implement your text editing logic here // Return the result of the text editing operation }, }); ``` Parameters: - `command` ('view' | 'create' | 'str_replace' | 'insert' | 'undo_edit'): The command to run. - `path` (string): Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. - `file_text` (string, optional): Required for `create` command, with the content of the file to be created. - `insert_line` (number, optional): Required for `insert` command. The line number after which to insert the new string. - `new_str` (string, optional): New string for `str_replace` or `insert` commands. - `old_str` (string, optional): Required for `str_replace` command, containing the string to replace. - `view_range` (number[], optional): Optional for `view` command to specify line range to show. #### Computer Tool The Computer Tool enables control of keyboard and mouse actions on a computer: ```ts const computerTool = vertexAnthropic.tools.computer_20241022({ displayWidthPx: 1920, displayHeightPx: 1080, displayNumber: 0, // Optional, for X11 environments execute: async ({ action, coordinate, text }) => { // Implement your computer control logic here // Return the result of the action // Example code: switch (action) { case 'screenshot': { // multipart result: return { type: 'image', data: fs .readFileSync('./data/screenshot-editor.png') .toString('base64'), }; } default: { console.log('Action:', action); console.log('Coordinate:', coordinate); console.log('Text:', text); return `executed ${action}`; } } }, // map to tool result content for LLM consumption: toModelOutput(result) { return typeof result === 'string' ? [{ type: 'text', text: result }] : [{ type: 'image', data: result.data, mediaType: 'image/png' }]; }, }); ``` Parameters: - `action` ('key' | 'type' | 'mouse_move' | 'left_click' | 'left_click_drag' | 'right_click' | 'middle_click' | 'double_click' | 'screenshot' | 'cursor_position'): The action to perform. - `coordinate` (number[], optional): Required for `mouse_move` and `left_click_drag` actions. Specifies the (x, y) coordinates. - `text` (string, optional): Required for `type` and `key` actions. These tools can be used in conjunction with the `claude-3-5-sonnet-v2@20241022` model to enable more complex interactions and tasks. ### Model Capabilities The latest Anthropic model list on Vertex AI is available [here](https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#model-list). See also [Anthropic Model Comparison](https://docs.anthropic.com/en/docs/about-claude/models#model-comparison). | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | Computer Use | | ------------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `claude-3-7-sonnet@20250219` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `claude-3-5-sonnet-v2@20241022` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `claude-3-5-sonnet@20240620` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `claude-3-5-haiku@20241022` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `claude-3-sonnet@20240229` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `claude-3-haiku@20240307` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `claude-3-opus@20240229` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Note> The table above lists popular models. You can also pass any available provider model ID as a string if needed. </Note> --- File: /ai/content/providers/01-ai-sdk-providers/160-revai.mdx --- --- title: Rev.ai description: Learn how to use the Rev.ai provider for the AI SDK. --- # Rev.ai Provider The [Rev.ai](https://www.rev.ai/) provider contains language model support for the Rev.ai transcription API. ## Setup The Rev.ai provider is available in the `@ai-sdk/revai` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/revai" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/revai" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/revai" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `revai` from `@ai-sdk/revai`: ```ts import { revai } from '@ai-sdk/revai'; ``` If you need a customized setup, you can import `createRevai` from `@ai-sdk/revai` and create a provider instance with your settings: ```ts import { createRevai } from '@ai-sdk/revai'; const revai = createRevai({ // custom settings, e.g. fetch: customFetch, }); ``` You can use the following optional settings to customize the Rev.ai provider instance: - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `REVAI_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Transcription Models You can create models that call the [Rev.ai transcription API](https://www.rev.ai/docs/api/transcription) using the `.transcription()` factory method. The first argument is the model id e.g. `machine`. ```ts const model = revai.transcription('machine'); ``` You can also pass additional provider-specific options using the `providerOptions` argument. For example, supplying the input language in ISO-639-1 (e.g. `en`) format can sometimes improve transcription performance if known beforehand. ```ts highlight="6" import { experimental_transcribe as transcribe } from 'ai'; import { revai } from '@ai-sdk/revai'; import { readFile } from 'fs/promises'; const result = await transcribe({ model: revai.transcription('machine'), audio: await readFile('audio.mp3'), providerOptions: { revai: { language: 'en' } }, }); ``` The following provider options are available: - **metadata** _string_ Optional metadata that was provided during job submission. - **notification_config** _object_ Optional configuration for a callback url to invoke when processing is complete. - **url** _string_ - Callback url to invoke when processing is complete. - **auth_headers** _object_ - Optional authorization headers, if needed to invoke the callback. - **Authorization** _string_ - Authorization header value. - **delete_after_seconds** _integer_ Amount of time after job completion when job is auto-deleted. - **verbatim** _boolean_ Configures the transcriber to transcribe every syllable, including all false starts and disfluencies. - **rush** _boolean_ [HIPAA Unsupported] Only available for human transcriber option. When set to true, your job is given higher priority. - **skip_diarization** _boolean_ Specify if speaker diarization will be skipped by the speech engine. - **skip_postprocessing** _boolean_ Only available for English and Spanish languages. User-supplied preference on whether to skip post-processing operations. - **skip_punctuation** _boolean_ Specify if "punct" type elements will be skipped by the speech engine. - **remove_disfluencies** _boolean_ When set to true, disfluencies (like 'ums' and 'uhs') will not appear in the transcript. - **remove_atmospherics** _boolean_ When set to true, atmospherics (like `<laugh>`, `<affirmative>`) will not appear in the transcript. - **filter_profanity** _boolean_ When enabled, profanities will be filtered by replacing characters with asterisks except for the first and last. - **speaker_channels_count** _integer_ Only available for English, Spanish and French languages. Specify the total number of unique speaker channels in the audio. - **speakers_count** _integer_ Only available for English, Spanish and French languages. Specify the total number of unique speakers in the audio. - **diarization_type** _string_ Specify diarization type. Possible values: "standard" (default), "premium". - **custom_vocabulary_id** _string_ Supply the id of a pre-completed custom vocabulary submitted through the Custom Vocabularies API. - **custom_vocabularies** _Array_ Specify a collection of custom vocabulary to be used for this job. - **strict_custom_vocabulary** _boolean_ If true, only exact phrases will be used as custom vocabulary. - **summarization_config** _object_ Specify summarization options. - **model** _string_ - Model type for summarization. Possible values: "standard" (default), "premium". - **type** _string_ - Summarization formatting type. Possible values: "paragraph" (default), "bullets". - **prompt** _string_ - Custom prompt for flexible summaries (mutually exclusive with type). - **translation_config** _object_ Specify translation options. - **target_languages** _Array_ - Array of target languages for translation. - **model** _string_ - Model type for translation. Possible values: "standard" (default), "premium". - **language** _string_ Language is provided as a ISO 639-1 language code. Default is "en". - **forced_alignment** _boolean_ When enabled, provides improved accuracy for per-word timestamps for a transcript. Default is `false`. Currently supported languages: - English (en, en-us, en-gb) - French (fr) - Italian (it) - German (de) - Spanish (es) Note: This option is not available in low-cost environment. ### Model Capabilities | Model | Transcription | Duration | Segments | Language | | ---------- | ------------------- | ------------------- | ------------------- | ------------------- | | `machine` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `low_cost` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `fusion` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | --- File: /ai/content/providers/01-ai-sdk-providers/20-mistral.mdx --- --- title: Mistral AI description: Learn how to use Mistral. --- # Mistral AI Provider The [Mistral AI](https://mistral.ai/) provider contains language model support for the Mistral chat API. ## Setup The Mistral provider is available in the `@ai-sdk/mistral` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/mistral" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/mistral" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/mistral" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `mistral` from `@ai-sdk/mistral`: ```ts import { mistral } from '@ai-sdk/mistral'; ``` If you need a customized setup, you can import `createMistral` from `@ai-sdk/mistral` and create a provider instance with your settings: ```ts import { createMistral } from '@ai-sdk/mistral'; const mistral = createMistral({ // custom settings }); ``` You can use the following optional settings to customize the Mistral provider instance: - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.mistral.ai/v1`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `MISTRAL_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Language Models You can create models that call the [Mistral chat API](https://docs.mistral.ai/api/#operation/createChatCompletion) using a provider instance. The first argument is the model id, e.g. `mistral-large-latest`. Some Mistral chat models support tool calls. ```ts const model = mistral('mistral-large-latest'); ``` Mistral chat models also support additional model settings that are not part of the [standard call settings](/docs/ai-sdk-core/settings). You can pass them as an options argument: ```ts const model = mistral('mistral-large-latest'); await generateText({ model, providerOptions: { mistral: { safePrompt: true, // optional safety prompt injection }, }, }); ``` The following optional provider options are available for Mistral models: - **safePrompt** _boolean_ Whether to inject a safety prompt before all conversations. Defaults to `false`. - **documentImageLimit** _number_ Maximum number of images to process in a document. - **documentPageLimit** _number_ Maximum number of pages to process in a document. ### Document OCR Mistral chat models support document OCR for PDF files. You can optionally set image and page limits using the provider options. ```ts const result = await generateText({ model: mistral('mistral-small-latest'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: new URL( 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/ai.pdf?raw=true', ), mediaType: 'application/pdf', }, ], }, ], // optional settings: providerOptions: { mistral: { documentImageLimit: 8, documentPageLimit: 64, }, }, }); ``` ### Reasoning Models Mistral offers reasoning models that provide step-by-step thinking capabilities: - **magistral-small-2506**: Smaller reasoning model for efficient step-by-step thinking - **magistral-medium-2506**: More powerful reasoning model balancing performance and cost These models return content that includes `<think>...</think>` tags containing the reasoning process. To properly extract and separate the reasoning from the final answer, use the [extract reasoning middleware](/docs/reference/ai-sdk-core/extract-reasoning-middleware): ```ts import { mistral } from '@ai-sdk/mistral'; import { extractReasoningMiddleware, generateText, wrapLanguageModel, } from 'ai'; const result = await generateText({ model: wrapLanguageModel({ model: mistral('magistral-small-2506'), middleware: extractReasoningMiddleware({ tagName: 'think', }), }), prompt: 'What is 15 * 24?', }); console.log('REASONING:', result.reasoningText); // Output: "Let me calculate this step by step..." console.log('ANSWER:', result.text); // Output: "360" ``` The middleware automatically parses the `<think>` tags and provides separate `reasoningText` and `text` properties in the result. ### Example You can use Mistral language models to generate text with the `generateText` function: ```ts import { mistral } from '@ai-sdk/mistral'; import { generateText } from 'ai'; const { text } = await generateText({ model: mistral('mistral-large-latest'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` Mistral language models can also be used in the `streamText`, `generateObject`, and `streamObject` functions (see [AI SDK Core](/docs/ai-sdk-core)). ### Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | ----------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `pixtral-large-latest` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `mistral-large-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `mistral-medium-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `mistral-medium-2505` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `mistral-small-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `magistral-small-2506` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `magistral-medium-2506` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `ministral-3b-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `ministral-8b-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `pixtral-12b-2409` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `open-mistral-7b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `open-mixtral-8x7b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `open-mixtral-8x22b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Note> The table above lists popular models. Please see the [Mistral docs](https://docs.mistral.ai/getting-started/models/models_overview/) for a full list of available models. The table above lists popular models. You can also pass any available provider model ID as a string if needed. </Note> ## Embedding Models You can create models that call the [Mistral embeddings API](https://docs.mistral.ai/api/#operation/createEmbedding) using the `.textEmbedding()` factory method. ```ts const model = mistral.textEmbedding('mistral-embed'); ``` You can use Mistral embedding models to generate embeddings with the `embed` function: ```ts import { mistral } from '@ai-sdk/mistral'; import { embed } from 'ai'; const { embedding } = await embed({ model: mistral.textEmbedding('mistral-embed'), value: 'sunny day at the beach', }); ``` ### Model Capabilities | Model | Default Dimensions | | --------------- | ------------------ | | `mistral-embed` | 1024 | --- File: /ai/content/providers/01-ai-sdk-providers/24-togetherai.mdx --- --- title: Together.ai description: Learn how to use Together.ai's models with the AI SDK. --- # Together.ai Provider The [Together.ai](https://together.ai) provider contains support for 200+ open-source models through the [Together.ai API](https://docs.together.ai/reference). ## Setup The Together.ai provider is available via the `@ai-sdk/togetherai` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/togetherai" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/togetherai" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/togetherai" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `togetherai` from `@ai-sdk/togetherai`: ```ts import { togetherai } from '@ai-sdk/togetherai'; ``` If you need a customized setup, you can import `createTogetherAI` from `@ai-sdk/togetherai` and create a provider instance with your settings: ```ts import { createTogetherAI } from '@ai-sdk/togetherai'; const togetherai = createTogetherAI({ apiKey: process.env.TOGETHER_AI_API_KEY ?? '', }); ``` You can use the following optional settings to customize the Together.ai provider instance: - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.together.xyz/v1`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `TOGETHER_AI_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Language Models You can create [Together.ai models](https://docs.together.ai/docs/serverless-models) using a provider instance. The first argument is the model id, e.g. `google/gemma-2-9b-it`. ```ts const model = togetherai('google/gemma-2-9b-it'); ``` ### Reasoning Models Together.ai exposes the thinking of `deepseek-ai/DeepSeek-R1` in the generated text using the `<think>` tag. You can use the `extractReasoningMiddleware` to extract this reasoning and expose it as a `reasoning` property on the result: ```ts import { togetherai } from '@ai-sdk/togetherai'; import { wrapLanguageModel, extractReasoningMiddleware } from 'ai'; const enhancedModel = wrapLanguageModel({ model: togetherai('deepseek-ai/DeepSeek-R1'), middleware: extractReasoningMiddleware({ tagName: 'think' }), }); ``` You can then use that enhanced model in functions like `generateText` and `streamText`. ### Example You can use Together.ai language models to generate text with the `generateText` function: ```ts import { togetherai } from '@ai-sdk/togetherai'; import { generateText } from 'ai'; const { text } = await generateText({ model: togetherai('meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` Together.ai language models can also be used in the `streamText` function (see [AI SDK Core](/docs/ai-sdk-core)). The Together.ai provider also supports [completion models](https://docs.together.ai/docs/serverless-models#language-models) via (following the above example code) `togetherai.completion()` and [embedding models](https://docs.together.ai/docs/serverless-models#embedding-models) via `togetherai.textEmbedding()`. ## Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | ---------------------------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `meta-llama/Meta-Llama-3.3-70B-Instruct-Turbo` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo` | <Cross size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | `mistralai/Mixtral-8x22B-Instruct-v0.1` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `mistralai/Mistral-7B-Instruct-v0.3` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `deepseek-ai/DeepSeek-V3` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `google/gemma-2b-it` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `Qwen/Qwen2.5-72B-Instruct-Turbo` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `databricks/dbrx-instruct` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Note> The table above lists popular models. Please see the [Together.ai docs](https://docs.together.ai/docs/serverless-models) for a full list of available models. You can also pass any available provider model ID as a string if needed. </Note> ## Image Models You can create Together.ai image models using the `.image()` factory method. For more on image generation with the AI SDK see [generateImage()](/docs/reference/ai-sdk-core/generate-image). ```ts import { togetherai } from '@ai-sdk/togetherai'; import { experimental_generateImage as generateImage } from 'ai'; const { images } = await generateImage({ model: togetherai.image('black-forest-labs/FLUX.1-dev'), prompt: 'A delighted resplendent quetzal mid flight amidst raindrops', }); ``` You can pass optional provider-specific request parameters using the `providerOptions` argument. ```ts import { togetherai } from '@ai-sdk/togetherai'; import { experimental_generateImage as generateImage } from 'ai'; const { images } = await generateImage({ model: togetherai.image('black-forest-labs/FLUX.1-dev'), prompt: 'A delighted resplendent quetzal mid flight amidst raindrops', size: '512x512', // Optional additional provider-specific request parameters providerOptions: { togetherai: { steps: 40, }, }, }); ``` For a complete list of available provider-specific options, see the [Together.ai Image Generation API Reference](https://docs.together.ai/reference/post_images-generations). ### Model Capabilities Together.ai image models support various image dimensions that vary by model. Common sizes include 512x512, 768x768, and 1024x1024, with some models supporting up to 1792x1792. The default size is 1024x1024. | Available Models | | ------------------------------------------ | | `stabilityai/stable-diffusion-xl-base-1.0` | | `black-forest-labs/FLUX.1-dev` | | `black-forest-labs/FLUX.1-dev-lora` | | `black-forest-labs/FLUX.1-schnell` | | `black-forest-labs/FLUX.1-canny` | | `black-forest-labs/FLUX.1-depth` | | `black-forest-labs/FLUX.1-redux` | | `black-forest-labs/FLUX.1.1-pro` | | `black-forest-labs/FLUX.1-pro` | | `black-forest-labs/FLUX.1-schnell-Free` | <Note> Please see the [Together.ai models page](https://docs.together.ai/docs/serverless-models#image-models) for a full list of available image models and their capabilities. </Note> ## Embedding Models You can create Together.ai embedding models using the `.textEmbedding()` factory method. For more on embedding models with the AI SDK see [embed()](/docs/reference/ai-sdk-core/embed). ```ts import { togetherai } from '@ai-sdk/togetherai'; import { embed } from 'ai'; const { embedding } = await embed({ model: togetherai.textEmbedding('togethercomputer/m2-bert-80M-2k-retrieval'), value: 'sunny day at the beach', }); ``` ### Model Capabilities | Model | Dimensions | Max Tokens | | ------------------------------------------------ | ---------- | ---------- | | `togethercomputer/m2-bert-80M-2k-retrieval` | 768 | 2048 | | `togethercomputer/m2-bert-80M-8k-retrieval` | 768 | 8192 | | `togethercomputer/m2-bert-80M-32k-retrieval` | 768 | 32768 | | `WhereIsAI/UAE-Large-V1` | 1024 | 512 | | `BAAI/bge-large-en-v1.5` | 1024 | 512 | | `BAAI/bge-base-en-v1.5` | 768 | 512 | | `sentence-transformers/msmarco-bert-base-dot-v5` | 768 | 512 | | `bert-base-uncased` | 768 | 512 | <Note> For a complete list of available embedding models, see the [Together.ai models page](https://docs.together.ai/docs/serverless-models#embedding-models). </Note> --- File: /ai/content/providers/01-ai-sdk-providers/25-cohere.mdx --- --- title: Cohere description: Learn how to use the Cohere provider for the AI SDK. --- # Cohere Provider The [Cohere](https://cohere.com/) provider contains language and embedding model support for the Cohere chat API. ## Setup The Cohere provider is available in the `@ai-sdk/cohere` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/cohere" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/cohere" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/cohere" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `cohere` from `@ai-sdk/cohere`: ```ts import { cohere } from '@ai-sdk/cohere'; ``` If you need a customized setup, you can import `createCohere` from `@ai-sdk/cohere` and create a provider instance with your settings: ```ts import { createCohere } from '@ai-sdk/cohere'; const cohere = createCohere({ // custom settings }); ``` You can use the following optional settings to customize the Cohere provider instance: - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.cohere.com/v2`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `COHERE_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Language Models You can create models that call the [Cohere chat API](https://docs.cohere.com/v2/docs/chat-api) using a provider instance. The first argument is the model id, e.g. `command-r-plus`. Some Cohere chat models support tool calls. ```ts const model = cohere('command-r-plus'); ``` ### Example You can use Cohere language models to generate text with the `generateText` function: ```ts import { cohere } from '@ai-sdk/cohere'; import { generateText } from 'ai'; const { text } = await generateText({ model: cohere('command-r-plus'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` Cohere language models can also be used in the `streamText`, `generateObject`, and `streamObject` functions (see [AI SDK Core](/docs/ai-sdk-core). ### Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | ------------------------ | ------------------- | ------------------- | ------------------- | ------------------- | | `command-a-03-2025` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `command-r7b-12-2024` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `command-r-plus-04-2024` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `command-r-plus` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `command-r-08-2024` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `command-r-03-2024` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `command-r` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `command` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `command-nightly` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `command-light` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `command-light-nightly` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Note> The table above lists popular models. Please see the [Cohere docs](https://docs.cohere.com/v2/docs/models#command) for a full list of available models. You can also pass any available provider model ID as a string if needed. </Note> ## Embedding Models You can create models that call the [Cohere embed API](https://docs.cohere.com/v2/reference/embed) using the `.textEmbedding()` factory method. ```ts const model = cohere.textEmbedding('embed-english-v3.0'); ``` You can use Cohere embedding models to generate embeddings with the `embed` function: ```ts import { cohere } from '@ai-sdk/cohere'; import { embed } from 'ai'; const { embedding } = await embed({ model: cohere.textEmbedding('embed-english-v3.0'), value: 'sunny day at the beach', providerOptions: { cohere: { inputType: 'search_document', }, }, }); ``` Cohere embedding models support additional provider options that can be passed via `providerOptions.cohere`: ```ts import { cohere } from '@ai-sdk/cohere'; import { embed } from 'ai'; const { embedding } = await embed({ model: cohere.textEmbedding('embed-english-v3.0'), value: 'sunny day at the beach', providerOptions: { cohere: { inputType: 'search_document', truncate: 'END', }, }, }); ``` The following provider options are available: - **inputType** _'search_document' | 'search_query' | 'classification' | 'clustering'_ Specifies the type of input passed to the model. Default is `search_query`. - `search_document`: Used for embeddings stored in a vector database for search use-cases. - `search_query`: Used for embeddings of search queries run against a vector DB to find relevant documents. - `classification`: Used for embeddings passed through a text classifier. - `clustering`: Used for embeddings run through a clustering algorithm. - **truncate** _'NONE' | 'START' | 'END'_ Specifies how the API will handle inputs longer than the maximum token length. Default is `END`. - `NONE`: If selected, when the input exceeds the maximum input token length will return an error. - `START`: Will discard the start of the input until the remaining input is exactly the maximum input token length for the model. - `END`: Will discard the end of the input until the remaining input is exactly the maximum input token length for the model. ### Model Capabilities | Model | Embedding Dimensions | | ------------------------------- | -------------------- | | `embed-english-v3.0` | 1024 | | `embed-multilingual-v3.0` | 1024 | | `embed-english-light-v3.0` | 384 | | `embed-multilingual-light-v3.0` | 384 | | `embed-english-v2.0` | 4096 | | `embed-english-light-v2.0` | 1024 | | `embed-multilingual-v2.0` | 768 | --- File: /ai/content/providers/01-ai-sdk-providers/26-fireworks.mdx --- --- title: Fireworks description: Learn how to use Fireworks models with the AI SDK. --- # Fireworks Provider [Fireworks](https://fireworks.ai/) is a platform for running and testing LLMs through their [API](https://readme.fireworks.ai/). ## Setup The Fireworks provider is available via the `@ai-sdk/fireworks` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/fireworks" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/fireworks" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/fireworks" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `fireworks` from `@ai-sdk/fireworks`: ```ts import { fireworks } from '@ai-sdk/fireworks'; ``` If you need a customized setup, you can import `createFireworks` from `@ai-sdk/fireworks` and create a provider instance with your settings: ```ts import { createFireworks } from '@ai-sdk/fireworks'; const fireworks = createFireworks({ apiKey: process.env.FIREWORKS_API_KEY ?? '', }); ``` You can use the following optional settings to customize the Fireworks provider instance: - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.fireworks.ai/inference/v1`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `FIREWORKS_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. ## Language Models You can create [Fireworks models](https://fireworks.ai/models) using a provider instance. The first argument is the model id, e.g. `accounts/fireworks/models/firefunction-v1`: ```ts const model = fireworks('accounts/fireworks/models/firefunction-v1'); ``` ### Reasoning Models Fireworks exposes the thinking of `deepseek-r1` in the generated text using the `<think>` tag. You can use the `extractReasoningMiddleware` to extract this reasoning and expose it as a `reasoning` property on the result: ```ts import { fireworks } from '@ai-sdk/fireworks'; import { wrapLanguageModel, extractReasoningMiddleware } from 'ai'; const enhancedModel = wrapLanguageModel({ model: fireworks('accounts/fireworks/models/deepseek-r1'), middleware: extractReasoningMiddleware({ tagName: 'think' }), }); ``` You can then use that enhanced model in functions like `generateText` and `streamText`. ### Example You can use Fireworks language models to generate text with the `generateText` function: ```ts import { fireworks } from '@ai-sdk/fireworks'; import { generateText } from 'ai'; const { text } = await generateText({ model: fireworks('accounts/fireworks/models/firefunction-v1'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` Fireworks language models can also be used in the `streamText` function (see [AI SDK Core](/docs/ai-sdk-core)). ### Completion Models You can create models that call the Fireworks completions API using the `.completion()` factory method: ```ts const model = fireworks.completion('accounts/fireworks/models/firefunction-v1'); ``` ### Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | ---------------------------------------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `accounts/fireworks/models/firefunction-v1` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `accounts/fireworks/models/deepseek-r1` | <Cross size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `accounts/fireworks/models/deepseek-v3` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `accounts/fireworks/models/llama-v3p1-405b-instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `accounts/fireworks/models/llama-v3p1-8b-instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `accounts/fireworks/models/llama-v3p2-3b-instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `accounts/fireworks/models/llama-v3p3-70b-instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `accounts/fireworks/models/mixtral-8x7b-instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `accounts/fireworks/models/mixtral-8x7b-instruct-hf` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `accounts/fireworks/models/mixtral-8x22b-instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `accounts/fireworks/models/qwen2p5-coder-32b-instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `accounts/fireworks/models/qwen2p5-72b-instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `accounts/fireworks/models/qwen-qwq-32b-preview` | <Cross size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `accounts/fireworks/models/qwen2-vl-72b-instruct` | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `accounts/fireworks/models/llama-v3p2-11b-vision-instruct` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `accounts/fireworks/models/qwq-32b` | <Cross size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `accounts/fireworks/models/yi-large` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | `accounts/fireworks/models/kimi-k2-instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Note> The table above lists popular models. Please see the [Fireworks models page](https://fireworks.ai/models) for a full list of available models. </Note> ## Embedding Models You can create models that call the Fireworks embeddings API using the `.textEmbedding()` factory method: ```ts const model = fireworks.textEmbedding('nomic-ai/nomic-embed-text-v1.5'); ``` You can use Fireworks embedding models to generate embeddings with the `embed` function: ```ts import { fireworks } from '@ai-sdk/fireworks'; import { embed } from 'ai'; const { embedding } = await embed({ model: fireworks.textEmbedding('nomic-ai/nomic-embed-text-v1.5'), value: 'sunny day at the beach', }); ``` ### Model Capabilities | Model | Dimensions | Max Tokens | | -------------------------------- | ---------- | ---------- | | `nomic-ai/nomic-embed-text-v1.5` | 768 | 8192 | <Note> For more embedding models, see the [Fireworks models page](https://fireworks.ai/models) for a full list of available models. </Note> ## Image Models You can create Fireworks image models using the `.image()` factory method. For more on image generation with the AI SDK see [generateImage()](/docs/reference/ai-sdk-core/generate-image). ```ts import { fireworks } from '@ai-sdk/fireworks'; import { experimental_generateImage as generateImage } from 'ai'; const { image } = await generateImage({ model: fireworks.image('accounts/fireworks/models/flux-1-dev-fp8'), prompt: 'A futuristic cityscape at sunset', aspectRatio: '16:9', }); ``` <Note> Model support for `size` and `aspectRatio` parameters varies. See the [Model Capabilities](#model-capabilities-1) section below for supported dimensions, or check the model's documentation on [Fireworks models page](https://fireworks.ai/models) for more details. </Note> ### Model Capabilities For all models supporting aspect ratios, the following aspect ratios are supported: `1:1 (default), 2:3, 3:2, 4:5, 5:4, 16:9, 9:16, 9:21, 21:9` For all models supporting size, the following sizes are supported: `640 x 1536, 768 x 1344, 832 x 1216, 896 x 1152, 1024x1024 (default), 1152 x 896, 1216 x 832, 1344 x 768, 1536 x 640` | Model | Dimensions Specification | | ------------------------------------------------------------ | ------------------------ | | `accounts/fireworks/models/flux-1-dev-fp8` | Aspect Ratio | | `accounts/fireworks/models/flux-1-schnell-fp8` | Aspect Ratio | | `accounts/fireworks/models/playground-v2-5-1024px-aesthetic` | Size | | `accounts/fireworks/models/japanese-stable-diffusion-xl` | Size | | `accounts/fireworks/models/playground-v2-1024px-aesthetic` | Size | | `accounts/fireworks/models/SSD-1B` | Size | | `accounts/fireworks/models/stable-diffusion-xl-1024-v1-0` | Size | For more details, see the [Fireworks models page](https://fireworks.ai/models). #### Stability AI Models Fireworks also presents several Stability AI models backed by Stability AI API keys and endpoint. The AI SDK Fireworks provider does not currently include support for these models: | Model ID | | -------------------------------------- | | `accounts/stability/models/sd3-turbo` | | `accounts/stability/models/sd3-medium` | | `accounts/stability/models/sd3` | --- File: /ai/content/providers/01-ai-sdk-providers/30-deepseek.mdx --- --- title: DeepSeek description: Learn how to use DeepSeek's models with the AI SDK. --- # DeepSeek Provider The [DeepSeek](https://www.deepseek.com) provider offers access to powerful language models through the DeepSeek API, including their [DeepSeek-V3 model](https://github.com/deepseek-ai/DeepSeek-V3). API keys can be obtained from the [DeepSeek Platform](https://platform.deepseek.com/api_keys). ## Setup The DeepSeek provider is available via the `@ai-sdk/deepseek` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/deepseek" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/deepseek" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/deepseek" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `deepseek` from `@ai-sdk/deepseek`: ```ts import { deepseek } from '@ai-sdk/deepseek'; ``` For custom configuration, you can import `createDeepSeek` and create a provider instance with your settings: ```ts import { createDeepSeek } from '@ai-sdk/deepseek'; const deepseek = createDeepSeek({ apiKey: process.env.DEEPSEEK_API_KEY ?? '', }); ``` You can use the following optional settings to customize the DeepSeek provider instance: - **baseURL** _string_ Use a different URL prefix for API calls. The default prefix is `https://api.deepseek.com/v1`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `DEEPSEEK_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. ## Language Models You can create language models using a provider instance: ```ts import { deepseek } from '@ai-sdk/deepseek'; import { generateText } from 'ai'; const { text } = await generateText({ model: deepseek('deepseek-chat'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` You can also use the `.chat()` or `.languageModel()` factory methods: ```ts const model = deepseek.chat('deepseek-chat'); // or const model = deepseek.languageModel('deepseek-chat'); ``` DeepSeek language models can be used in the `streamText` function (see [AI SDK Core](/docs/ai-sdk-core)). ### Reasoning DeepSeek has reasoning support for the `deepseek-reasoner` model. The reasoning is exposed through streaming: ```ts import { deepseek } from '@ai-sdk/deepseek'; import { streamText } from 'ai'; const result = streamText({ model: deepseek('deepseek-reasoner'), prompt: 'How many "r"s are in the word "strawberry"?', }); for await (const part of result.fullStream) { if (part.type === 'reasoning') { // This is the reasoning text console.log('Reasoning:', part.text); } else if (part.type === 'text') { // This is the final answer console.log('Answer:', part.text); } } ``` See [AI SDK UI: Chatbot](/docs/ai-sdk-ui/chatbot#reasoning) for more details on how to integrate reasoning into your chatbot. ### Cache Token Usage DeepSeek provides context caching on disk technology that can significantly reduce token costs for repeated content. You can access the cache hit/miss metrics through the `providerMetadata` property in the response: ```ts import { deepseek } from '@ai-sdk/deepseek'; import { generateText } from 'ai'; const result = await generateText({ model: deepseek('deepseek-chat'), prompt: 'Your prompt here', }); console.log(result.providerMetadata); // Example output: { deepseek: { promptCacheHitTokens: 1856, promptCacheMissTokens: 5 } } ``` The metrics include: - `promptCacheHitTokens`: Number of input tokens that were cached - `promptCacheMissTokens`: Number of input tokens that were not cached <Note> For more details about DeepSeek's caching system, see the [DeepSeek caching documentation](https://api-docs.deepseek.com/guides/kv_cache#checking-cache-hit-status). </Note> ## Model Capabilities | Model | Text Generation | Object Generation | Image Input | Tool Usage | Tool Streaming | | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `deepseek-chat` | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | `deepseek-reasoner` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Note> Please see the [DeepSeek docs](https://api-docs.deepseek.com) for a full list of available models. You can also pass any available provider model ID as a string if needed. </Note> --- File: /ai/content/providers/01-ai-sdk-providers/40-cerebras.mdx --- --- title: Cerebras description: Learn how to use Cerebras's models with the AI SDK. --- # Cerebras Provider The [Cerebras](https://cerebras.ai) provider offers access to powerful language models through the Cerebras API, including their high-speed inference capabilities powered by Wafer-Scale Engines and CS-3 systems. API keys can be obtained from the [Cerebras Platform](https://cloud.cerebras.ai). ## Setup The Cerebras provider is available via the `@ai-sdk/cerebras` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/cerebras" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/cerebras" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/cerebras" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `cerebras` from `@ai-sdk/cerebras`: ```ts import { cerebras } from '@ai-sdk/cerebras'; ``` For custom configuration, you can import `createCerebras` and create a provider instance with your settings: ```ts import { createCerebras } from '@ai-sdk/cerebras'; const cerebras = createCerebras({ apiKey: process.env.CEREBRAS_API_KEY ?? '', }); ``` You can use the following optional settings to customize the Cerebras provider instance: - **baseURL** _string_ Use a different URL prefix for API calls. The default prefix is `https://api.cerebras.ai/v1`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `CEREBRAS_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. ## Language Models You can create language models using a provider instance: ```ts import { cerebras } from '@ai-sdk/cerebras'; import { generateText } from 'ai'; const { text } = await generateText({ model: cerebras('llama3.1-8b'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` Cerebras language models can be used in the `streamText` function (see [AI SDK Core](/docs/ai-sdk-core)). You can create Cerebras language models using a provider instance. The first argument is the model ID, e.g. `llama-3.3-70b`: ```ts const model = cerebras('llama-3.3-70b'); ``` You can also use the `.languageModel()` and `.chat()` methods: ```ts const model = cerebras.languageModel('llama-3.3-70b'); const model = cerebras.chat('llama-3.3-70b'); ``` ## Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | --------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `llama3.1-8b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `llama3.1-70b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `llama-3.3-70b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Note> Please see the [Cerebras docs](https://inference-docs.cerebras.ai/introduction) for more details about the available models. Note that context windows are temporarily limited to 8192 tokens in the Free Tier. You can also pass any available provider model ID as a string if needed. </Note> --- File: /ai/content/providers/01-ai-sdk-providers/60-replicate.mdx --- --- title: Replicate description: Learn how to use Replicate models with the AI SDK. --- # Replicate Provider [Replicate](https://replicate.com/) is a platform for running open-source AI models. It is a popular choice for running image generation models. ## Setup The Replicate provider is available via the `@ai-sdk/replicate` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/replicate" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/replicate" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/replicate" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `replicate` from `@ai-sdk/replicate`: ```ts import { replicate } from '@ai-sdk/replicate'; ``` If you need a customized setup, you can import `createReplicate` from `@ai-sdk/replicate` and create a provider instance with your settings: ```ts import { createReplicate } from '@ai-sdk/replicate'; const replicate = createReplicate({ apiToken: process.env.REPLICATE_API_TOKEN ?? '', }); ``` You can use the following optional settings to customize the Replicate provider instance: - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.replicate.com/v1`. - **apiToken** _string_ API token that is being sent using the `Authorization` header. It defaults to the `REPLICATE_API_TOKEN` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. ## Image Models You can create Replicate image models using the `.image()` factory method. For more on image generation with the AI SDK see [generateImage()](/docs/reference/ai-sdk-core/generate-image). <Note> Model support for `size` and other parameters varies by model. Check the model's documentation on [Replicate](https://replicate.com/explore) for supported options and additional parameters that can be passed via `providerOptions.replicate`. </Note> ### Supported Image Models The following image models are currently supported by the Replicate provider: - [black-forest-labs/flux-1.1-pro-ultra](https://replicate.com/black-forest-labs/flux-1.1-pro-ultra) - [black-forest-labs/flux-1.1-pro](https://replicate.com/black-forest-labs/flux-1.1-pro) - [black-forest-labs/flux-dev](https://replicate.com/black-forest-labs/flux-dev) - [black-forest-labs/flux-pro](https://replicate.com/black-forest-labs/flux-pro) - [black-forest-labs/flux-schnell](https://replicate.com/black-forest-labs/flux-schnell) - [bytedance/sdxl-lightning-4step](https://replicate.com/bytedance/sdxl-lightning-4step) - [fofr/aura-flow](https://replicate.com/fofr/aura-flow) - [fofr/latent-consistency-model](https://replicate.com/fofr/latent-consistency-model) - [fofr/realvisxl-v3-multi-controlnet-lora](https://replicate.com/fofr/realvisxl-v3-multi-controlnet-lora) - [fofr/sdxl-emoji](https://replicate.com/fofr/sdxl-emoji) - [fofr/sdxl-multi-controlnet-lora](https://replicate.com/fofr/sdxl-multi-controlnet-lora) - [ideogram-ai/ideogram-v2-turbo](https://replicate.com/ideogram-ai/ideogram-v2-turbo) - [ideogram-ai/ideogram-v2](https://replicate.com/ideogram-ai/ideogram-v2) - [lucataco/dreamshaper-xl-turbo](https://replicate.com/lucataco/dreamshaper-xl-turbo) - [lucataco/open-dalle-v1.1](https://replicate.com/lucataco/open-dalle-v1.1) - [lucataco/realvisxl-v2.0](https://replicate.com/lucataco/realvisxl-v2.0) - [lucataco/realvisxl2-lcm](https://replicate.com/lucataco/realvisxl2-lcm) - [luma/photon-flash](https://replicate.com/luma/photon-flash) - [luma/photon](https://replicate.com/luma/photon) - [nvidia/sana](https://replicate.com/nvidia/sana) - [playgroundai/playground-v2.5-1024px-aesthetic](https://replicate.com/playgroundai/playground-v2.5-1024px-aesthetic) - [recraft-ai/recraft-v3-svg](https://replicate.com/recraft-ai/recraft-v3-svg) - [recraft-ai/recraft-v3](https://replicate.com/recraft-ai/recraft-v3) - [stability-ai/stable-diffusion-3.5-large-turbo](https://replicate.com/stability-ai/stable-diffusion-3.5-large-turbo) - [stability-ai/stable-diffusion-3.5-large](https://replicate.com/stability-ai/stable-diffusion-3.5-large) - [stability-ai/stable-diffusion-3.5-medium](https://replicate.com/stability-ai/stable-diffusion-3.5-medium) - [tstramer/material-diffusion](https://replicate.com/tstramer/material-diffusion) You can also use [versioned models](https://replicate.com/docs/topics/models/versions). The id for versioned models is the Replicate model id followed by a colon and the version ID (`$modelId:$versionId`), e.g. `bytedance/sdxl-lightning-4step:5599ed30703defd1d160a25a63321b4dec97101d98b4674bcc56e41f62f35637`. <Note> You can also pass any available Replicate model ID as a string if needed. </Note> ### Basic Usage ```ts import { replicate } from '@ai-sdk/replicate'; import { experimental_generateImage as generateImage } from 'ai'; import { writeFile } from 'node:fs/promises'; const { image } = await generateImage({ model: replicate.image('black-forest-labs/flux-schnell'), prompt: 'The Loch Ness Monster getting a manicure', aspectRatio: '16:9', }); await writeFile('image.webp', image.uint8Array); console.log('Image saved as image.webp'); ``` ### Model-specific options ```ts highlight="9-11" import { replicate } from '@ai-sdk/replicate'; import { experimental_generateImage as generateImage } from 'ai'; const { image } = await generateImage({ model: replicate.image('recraft-ai/recraft-v3'), prompt: 'The Loch Ness Monster getting a manicure', size: '1365x1024', providerOptions: { replicate: { style: 'realistic_image', }, }, }); ``` ### Versioned Models ```ts import { replicate } from '@ai-sdk/replicate'; import { experimental_generateImage as generateImage } from 'ai'; const { image } = await generateImage({ model: replicate.image( 'bytedance/sdxl-lightning-4step:5599ed30703defd1d160a25a63321b4dec97101d98b4674bcc56e41f62f35637', ), prompt: 'The Loch Ness Monster getting a manicure', }); ``` For more details, see the [Replicate models page](https://replicate.com/explore). --- File: /ai/content/providers/01-ai-sdk-providers/70-perplexity.mdx --- --- title: Perplexity description: Learn how to use Perplexity's Sonar API with the AI SDK. --- # Perplexity Provider The [Perplexity](https://sonar.perplexity.ai) provider offers access to Sonar API - a language model that uniquely combines real-time web search with natural language processing. Each response is grounded in current web data and includes detailed citations, making it ideal for research, fact-checking, and obtaining up-to-date information. API keys can be obtained from the [Perplexity Platform](https://docs.perplexity.ai). ## Setup The Perplexity provider is available via the `@ai-sdk/perplexity` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/perplexity" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/perplexity" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/perplexity" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `perplexity` from `@ai-sdk/perplexity`: ```ts import { perplexity } from '@ai-sdk/perplexity'; ``` For custom configuration, you can import `createPerplexity` and create a provider instance with your settings: ```ts import { createPerplexity } from '@ai-sdk/perplexity'; const perplexity = createPerplexity({ apiKey: process.env.PERPLEXITY_API_KEY ?? '', }); ``` You can use the following optional settings to customize the Perplexity provider instance: - **baseURL** _string_ Use a different URL prefix for API calls. The default prefix is `https://api.perplexity.ai`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `PERPLEXITY_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. ## Language Models You can create Perplexity models using a provider instance: ```ts import { perplexity } from '@ai-sdk/perplexity'; import { generateText } from 'ai'; const { text } = await generateText({ model: perplexity('sonar-pro'), prompt: 'What are the latest developments in quantum computing?', }); ``` ### Sources Websites that have been used to generate the response are included in the `sources` property of the result: ```ts import { perplexity } from '@ai-sdk/perplexity'; import { generateText } from 'ai'; const { text, sources } = await generateText({ model: perplexity('sonar-pro'), prompt: 'What are the latest developments in quantum computing?', }); console.log(sources); ``` ### Provider Options & Metadata The Perplexity provider includes additional metadata in the response through `providerMetadata`. Additional configuration options are available through `providerOptions`. ```ts const result = await generateText({ model: perplexity('sonar-pro'), prompt: 'What are the latest developments in quantum computing?', providerOptions: { perplexity: { return_images: true, // Enable image responses (Tier-2 Perplexity users only) }, }, }); console.log(result.providerMetadata); // Example output: // { // perplexity: { // usage: { citationTokens: 5286, numSearchQueries: 1 }, // images: [ // { imageUrl: "https://example.com/image1.jpg", originUrl: "https://elsewhere.com/page1", height: 1280, width: 720 }, // { imageUrl: "https://example.com/image2.jpg", originUrl: "https://elsewhere.com/page2", height: 1280, width: 720 } // ] // }, // } ``` The metadata includes: - `usage`: Object containing `citationTokens` and `numSearchQueries` metrics - `images`: Array of image URLs when `return_images` is enabled (Tier-2 users only) You can enable image responses by setting `return_images: true` in the provider options. This feature is only available to Perplexity Tier-2 users and above. <Note> For more details about Perplexity's capabilities, see the [Perplexity chat completion docs](https://docs.perplexity.ai/api-reference/chat-completions). </Note> ## Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | --------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `sonar-deep-research` | <Cross size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `sonar-reasoning-pro` | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `sonar-reasoning` | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `sonar-pro` | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `sonar` | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Note> Please see the [Perplexity docs](https://docs.perplexity.ai) for detailed API documentation and the latest updates. </Note> --- File: /ai/content/providers/01-ai-sdk-providers/80-luma.mdx --- --- title: Luma description: Learn how to use Luma AI models with the AI SDK. --- # Luma Provider [Luma AI](https://lumalabs.ai/) provides state-of-the-art image generation models through their Dream Machine platform. Their models offer ultra-high quality image generation with superior prompt understanding and unique capabilities like character consistency and multi-image reference support. ## Setup The Luma provider is available via the `@ai-sdk/luma` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/luma" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/luma" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/luma" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `luma` from `@ai-sdk/luma`: ```ts import { luma } from '@ai-sdk/luma'; ``` If you need a customized setup, you can import `createLuma` and create a provider instance with your settings: ```ts import { createLuma } from '@ai-sdk/luma'; const luma = createLuma({ apiKey: 'your-api-key', // optional, defaults to LUMA_API_KEY environment variable baseURL: 'custom-url', // optional headers: { /* custom headers */ }, // optional }); ``` You can use the following optional settings to customize the Luma provider instance: - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.lumalabs.ai`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `LUMA_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Image Models You can create Luma image models using the `.image()` factory method. For more on image generation with the AI SDK see [generateImage()](/docs/reference/ai-sdk-core/generate-image). ### Basic Usage ```ts import { luma } from '@ai-sdk/luma'; import { experimental_generateImage as generateImage } from 'ai'; import fs from 'fs'; const { image } = await generateImage({ model: luma.image('photon-1'), prompt: 'A serene mountain landscape at sunset', aspectRatio: '16:9', }); const filename = `image-${Date.now()}.png`; fs.writeFileSync(filename, image.uint8Array); console.log(`Image saved to ${filename}`); ``` ### Image Model Settings You can customize the generation behavior with optional settings: ```ts const { image } = await generateImage({ model: luma.image('photon-1'), prompt: 'A serene mountain landscape at sunset', aspectRatio: '16:9', maxImagesPerCall: 1, // Maximum number of images to generate per API call providerOptions: { luma: { pollIntervalMillis: 5000, // How often to check for completed images (in ms) maxPollAttempts: 10, // Maximum number of polling attempts before timeout }, }, }); ``` Since Luma processes images through an asynchronous queue system, these settings allow you to tune the polling behavior: - **maxImagesPerCall** _number_ Override the maximum number of images generated per API call. Defaults to 1. - **pollIntervalMillis** _number_ Control how frequently the API is checked for completed images while they are being processed. Defaults to 500ms. - **maxPollAttempts** _number_ Limit how long to wait for results before timing out, since image generation is queued asynchronously. Defaults to 120 attempts. ### Model Capabilities Luma offers two main models: | Model | Description | | ---------------- | ---------------------------------------------------------------- | | `photon-1` | High-quality image generation with superior prompt understanding | | `photon-flash-1` | Faster generation optimized for speed while maintaining quality | Both models support the following aspect ratios: - 1:1 - 3:4 - 4:3 - 9:16 - 16:9 (default) - 9:21 - 21:9 For more details about supported aspect ratios, see the [Luma Image Generation documentation](https://docs.lumalabs.ai/docs/image-generation). Key features of Luma models include: - Ultra-high quality image generation - 10x higher cost efficiency compared to similar models - Superior prompt understanding and adherence - Unique character consistency capabilities from single reference images - Multi-image reference support for precise style matching ### Advanced Options Luma models support several advanced features through the `providerOptions.luma` parameter. #### Image Reference Use up to 4 reference images to guide your generation. Useful for creating variations or visualizing complex concepts. Adjust the `weight` (0-1) to control the influence of reference images. ```ts // Example: Generate a salamander with reference await generateImage({ model: luma.image('photon-1'), prompt: 'A salamander at dusk in a forest pond, in the style of ukiyo-e', providerOptions: { luma: { image_ref: [ { url: 'https://example.com/reference.jpg', weight: 0.85, }, ], }, }, }); ``` #### Style Reference Apply specific visual styles to your generations using reference images. Control the style influence using the `weight` parameter. ```ts // Example: Generate with style reference await generateImage({ model: luma.image('photon-1'), prompt: 'A blue cream Persian cat launching its website on Vercel', providerOptions: { luma: { style_ref: [ { url: 'https://example.com/style.jpg', weight: 0.8, }, ], }, }, }); ``` #### Character Reference Create consistent and personalized characters using up to 4 reference images of the same subject. More reference images improve character representation. ```ts // Example: Generate character-based image await generateImage({ model: luma.image('photon-1'), prompt: 'A woman with a cat riding a broomstick in a forest', providerOptions: { luma: { character_ref: { identity0: { images: ['https://example.com/character.jpg'], }, }, }, }, }); ``` #### Modify Image Transform existing images using text prompts. Use the `weight` parameter to control how closely the result matches the input image (higher weight = closer to input but less creative). <Note> For color changes, it's recommended to use a lower weight value (0.0-0.1). </Note> ```ts // Example: Modify existing image await generateImage({ model: luma.image('photon-1'), prompt: 'transform the bike to a boat', providerOptions: { luma: { modify_image_ref: { url: 'https://example.com/image.jpg', weight: 1.0, }, }, }, }); ``` For more details about Luma's capabilities and features, visit the [Luma Image Generation documentation](https://docs.lumalabs.ai/docs/image-generation). --- File: /ai/content/providers/01-ai-sdk-providers/90-elevenlabs.mdx --- --- title: ElevenLabs description: Learn how to use the ElevenLabs provider for the AI SDK. --- # ElevenLabs Provider The [ElevenLabs](https://elevenlabs.io/) provider contains language model support for the ElevenLabs transcription API. ## Setup The ElevenLabs provider is available in the `@ai-sdk/elevenlabs` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/elevenlabs" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/elevenlabs" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/elevenlabs" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `elevenlabs` from `@ai-sdk/elevenlabs`: ```ts import { elevenlabs } from '@ai-sdk/elevenlabs'; ``` If you need a customized setup, you can import `createElevenLabs` from `@ai-sdk/elevenlabs` and create a provider instance with your settings: ```ts import { createElevenLabs } from '@ai-sdk/elevenlabs'; const elevenlabs = createElevenLabs({ // custom settings, e.g. fetch: customFetch, }); ``` You can use the following optional settings to customize the ElevenLabs provider instance: - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `ELEVENLABS_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Transcription Models You can create models that call the [ElevenLabs transcription API](https://elevenlabs.io/speech-to-text) using the `.transcription()` factory method. The first argument is the model id e.g. `scribe_v1`. ```ts const model = elevenlabs.transcription('scribe_v1'); ``` You can also pass additional provider-specific options using the `providerOptions` argument. For example, supplying the input language in ISO-639-1 (e.g. `en`) format can sometimes improve transcription performance if known beforehand. ```ts highlight="6" import { experimental_transcribe as transcribe } from 'ai'; import { elevenlabs } from '@ai-sdk/elevenlabs'; const result = await transcribe({ model: elevenlabs.transcription('scribe_v1'), audio: new Uint8Array([1, 2, 3, 4]), providerOptions: { elevenlabs: { languageCode: 'en' } }, }); ``` The following provider options are available: - **languageCode** _string_ An ISO-639-1 or ISO-639-3 language code corresponding to the language of the audio file. Can sometimes improve transcription performance if known beforehand. Defaults to `null`, in which case the language is predicted automatically. - **tagAudioEvents** _boolean_ Whether to tag audio events like (laughter), (footsteps), etc. in the transcription. Defaults to `true`. - **numSpeakers** _integer_ The maximum amount of speakers talking in the uploaded file. Can help with predicting who speaks when. The maximum amount of speakers that can be predicted is 32. Defaults to `null`, in which case the amount of speakers is set to the maximum value the model supports. - **timestampsGranularity** _enum_ The granularity of the timestamps in the transcription. Defaults to `'word'`. Allowed values: `'none'`, `'word'`, `'character'`. - **diarize** _boolean_ Whether to annotate which speaker is currently talking in the uploaded file. Defaults to `true`. - **fileFormat** _enum_ The format of input audio. Defaults to `'other'`. Allowed values: `'pcm_s16le_16'`, `'other'`. For `'pcm_s16le_16'`, the input audio must be 16-bit PCM at a 16kHz sample rate, single channel (mono), and little-endian byte order. Latency will be lower than with passing an encoded waveform. ### Model Capabilities | Model | Transcription | Duration | Segments | Language | | ------------------------ | ------------------- | ------------------- | ------------------- | ------------------- | | `scribe_v1` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `scribe_v1_experimental` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | --- File: /ai/content/providers/01-ai-sdk-providers/index.mdx --- --- title: AI SDK Providers description: Learn how to use AI SDK providers. --- # AI SDK Providers The AI SDK comes with several providers that you can use to interact with different language models: <OfficialModelCards /> There are also [community providers](./community-providers) that have been created using the [Language Model Specification](./community-providers/custom-providers). <CommunityModelCards /> ## Provider support Not all providers support all AI SDK features. Here's a quick comparison of the capabilities of popular models: | Provider | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | ------------------------------------------------------------------------ | --------------------------------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-4` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-3` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-3-fast` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-3-mini` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-3-mini-fast` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-2-1212` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-2-vision-1212` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-beta` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-vision-beta` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | [Vercel](/providers/ai-sdk-providers/vercel) | `v0-1.0-md` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4.1` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4.1-mini` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4.1-nano` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4o` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4o-mini` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4.1` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `o1` | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `o1-mini` | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | | [OpenAI](/providers/ai-sdk-providers/openai) | `o1-preview` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-3-7-sonnet-20250219` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-3-5-sonnet-20241022` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-3-5-sonnet-20240620` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-3-5-haiku-20241022` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Groq](/providers/ai-sdk-providers/groq) | `meta-llama/llama-4-scout-17b-16e-instruct` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Groq](/providers/ai-sdk-providers/groq) | `deepseek-r1-distill-llama-70b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Groq](/providers/ai-sdk-providers/groq) | `llama-3.3-70b-versatile` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Groq](/providers/ai-sdk-providers/groq) | `llama-3.1-8b-instant` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Groq](/providers/ai-sdk-providers/groq) | `mistral-saba-24b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Groq](/providers/ai-sdk-providers/groq) | `qwen-qwq-32b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Groq](/providers/ai-sdk-providers/groq) | `mixtral-8x7b-32768` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Groq](/providers/ai-sdk-providers/groq) | `gemma2-9b-it` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Groq](/providers/ai-sdk-providers/groq) | `moonshotai/kimi-k2-instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | [DeepInfra](/providers/ai-sdk-providers/deepinfra) | `meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | [DeepInfra](/providers/ai-sdk-providers/deepinfra) | `meta-llama/Llama-4-Scout-17B-16E-Instruct` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | [DeepInfra](/providers/ai-sdk-providers/deepinfra) | `meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | [DeepInfra](/providers/ai-sdk-providers/deepinfra) | `meta-llama/Llama-3.3-70B-Instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | | [DeepInfra](/providers/ai-sdk-providers/deepinfra) | `deepseek-ai/DeepSeek-V3` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | [DeepInfra](/providers/ai-sdk-providers/deepinfra) | `deepseek-ai/DeepSeek-R1` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | [DeepInfra](/providers/ai-sdk-providers/deepinfra) | `deepseek-ai/DeepSeek-R1-Distill-Llama-70B` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | [DeepInfra](/providers/ai-sdk-providers/deepinfra) | `deepseek-ai/DeepSeek-R1-Turbo` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | [Mistral](/providers/ai-sdk-providers/mistral) | `pixtral-large-latest` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Mistral](/providers/ai-sdk-providers/mistral) | `mistral-large-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Mistral](/providers/ai-sdk-providers/mistral) | `mistral-medium-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Mistral](/providers/ai-sdk-providers/mistral) | `mistral-medium-2505` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Mistral](/providers/ai-sdk-providers/mistral) | `mistral-small-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Mistral](/providers/ai-sdk-providers/mistral) | `pixtral-12b-2409` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai) | `gemini-2.0-flash-exp` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai) | `gemini-1.5-flash` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai) | `gemini-1.5-pro` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Google Vertex](/providers/ai-sdk-providers/google-vertex) | `gemini-2.0-flash-exp` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Google Vertex](/providers/ai-sdk-providers/google-vertex) | `gemini-1.5-flash` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Google Vertex](/providers/ai-sdk-providers/google-vertex) | `gemini-1.5-pro` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [DeepSeek](/providers/ai-sdk-providers/deepseek) | `deepseek-chat` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [DeepSeek](/providers/ai-sdk-providers/deepseek) | `deepseek-reasoner` | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | [Cerebras](/providers/ai-sdk-providers/cerebras) | `llama3.1-8b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Cerebras](/providers/ai-sdk-providers/cerebras) | `llama3.3-70b` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | [Fireworks](/providers/ai-sdk-providers/fireworks) | `kimi-k2-instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Note> This table is not exhaustive. Additional models can be found in the provider documentation pages and on the provider websites. </Note> --- File: /ai/content/providers/02-openai-compatible-providers/01-custom-providers.mdx --- --- title: Writing a Custom Provider description: Create a custom provider package for an OpenAI-compatible provider leveraging the AI SDK OpenAI Compatible package. --- # Writing a Custom Provider You can create your own provider package that leverages the AI SDK's [OpenAI Compatible package](https://www.npmjs.com/package/@ai-sdk/openai-compatible). Publishing your provider package to [npm](https://www.npmjs.com/) can give users an easy way to use the provider's models and stay up to date with any changes you may have. Here's an example structure: ### File Structure ```bash packages/example/ ├── src/ │ ├── example-chat-settings.ts # Chat model types and settings │ ├── example-completion-settings.ts # Completion model types and settings │ ├── example-embedding-settings.ts # Embedding model types and settings │ ├── example-image-settings.ts # Image model types and settings │ ├── example-provider.ts # Main provider implementation │ ├── example-provider.test.ts # Provider tests │ └── index.ts # Public exports ├── package.json ├── tsconfig.json ├── tsup.config.ts # Build configuration └── README.md ``` ### Key Files 1. **example-chat-settings.ts** - Define chat model IDs and settings: ```ts export type ExampleChatModelId = | 'example/chat-model-1' | 'example/chat-model-2' | (string & {}); ``` The completion, embedding, and image settings are implemented similarly to the chat settings. 2. **example-provider.ts** - Main provider implementation: ```ts import { LanguageModelV1, EmbeddingModelV2 } from '@ai-sdk/provider'; import { OpenAICompatibleChatLanguageModel, OpenAICompatibleCompletionLanguageModel, OpenAICompatibleEmbeddingModel, OpenAICompatibleImageModel, } from '@ai-sdk/openai-compatible'; import { FetchFunction, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; // Import your model id and settings here. export interface ExampleProviderSettings { /** Example API key. */ apiKey?: string; /** Base URL for the API calls. */ baseURL?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Optional custom url query parameters to include in request urls. */ queryParams?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } export interface ExampleProvider { /** Creates a model for text generation. */ ( modelId: ExampleChatModelId, settings?: ExampleChatSettings, ): LanguageModelV1; /** Creates a chat model for text generation. */ chatModel( modelId: ExampleChatModelId, settings?: ExampleChatSettings, ): LanguageModelV1; /** Creates a completion model for text generation. */ completionModel( modelId: ExampleCompletionModelId, settings?: ExampleCompletionSettings, ): LanguageModelV1; /** Creates a text embedding model for text generation. */ textEmbeddingModel( modelId: ExampleEmbeddingModelId, settings?: ExampleEmbeddingSettings, ): EmbeddingModelV2<string>; /** Creates an image model for image generation. */ imageModel( modelId: ExampleImageModelId, settings?: ExampleImageSettings, ): ImageModelV2; } export function createExample( options: ExampleProviderSettings = {}, ): ExampleProvider { const baseURL = withoutTrailingSlash( options.baseURL ?? 'https://api.example.com/v1', ); const getHeaders = () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'EXAMPLE_API_KEY', description: 'Example API key', })}`, ...options.headers, }); interface CommonModelConfig { provider: string; url: ({ path }: { path: string }) => string; headers: () => Record<string, string>; fetch?: FetchFunction; } const getCommonModelConfig = (modelType: string): CommonModelConfig => ({ provider: `example.${modelType}`, url: ({ path }) => { const url = new URL(`${baseURL}${path}`); if (options.queryParams) { url.search = new URLSearchParams(options.queryParams).toString(); } return url.toString(); }, headers: getHeaders, fetch: options.fetch, }); const createChatModel = ( modelId: ExampleChatModelId, settings: ExampleChatSettings = {}, ) => { return new OpenAICompatibleChatLanguageModel( modelId, settings, getCommonModelConfig('chat'), ); }; const createCompletionModel = ( modelId: ExampleCompletionModelId, settings: ExampleCompletionSettings = {}, ) => new OpenAICompatibleCompletionLanguageModel( modelId, settings, getCommonModelConfig('completion'), ); const createTextEmbeddingModel = ( modelId: ExampleEmbeddingModelId, settings: ExampleEmbeddingSettings = {}, ) => new OpenAICompatibleEmbeddingModel( modelId, settings, getCommonModelConfig('embedding'), ); const createImageModel = ( modelId: ExampleImageModelId, settings: ExampleImageSettings = {}, ) => new OpenAICompatibleImageModel( modelId, settings, getCommonModelConfig('image'), ); const provider = ( modelId: ExampleChatModelId, settings?: ExampleChatSettings, ) => createChatModel(modelId, settings); provider.completionModel = createCompletionModel; provider.chatModel = createChatModel; provider.textEmbeddingModel = createTextEmbeddingModel; provider.imageModel = createImageModel; return provider; } // Export default instance export const example = createExample(); ``` 3. **index.ts** - Public exports: ```ts export { createExample, example } from './example-provider'; export type { ExampleProvider, ExampleProviderSettings, } from './example-provider'; ``` 4. **package.json** - Package configuration: ```js { "name": "@company-name/example", "version": "0.0.1", "dependencies": { "@ai-sdk/openai-compatible": "^0.0.7", "@ai-sdk/provider": "^1.0.2", "@ai-sdk/provider-utils": "^2.0.4", // ...additional dependencies }, // ...additional scripts and module build configuration } ``` ### Usage Once published, users can use your provider like this: ```ts import { example } from '@company-name/example'; import { generateText } from 'ai'; const { text } = await generateText({ model: example('example/chat-model-1'), prompt: 'Hello, how are you?', }); ``` This structure provides a clean, type-safe implementation that leverages the OpenAI Compatible package while maintaining consistency with the usage of other AI SDK providers. ### Internal API As you work on your provider you may need to use some of the internal API of the OpenAI Compatible package. You can import these from the `@ai-sdk/openai-compatible/internal` package, for example: ```ts import { convertToOpenAICompatibleChatMessages } from '@ai-sdk/openai-compatible/internal'; ``` You can see the latest available exports in the AI SDK [GitHub repository](https://github.com/vercel/ai/blob/main/packages/openai-compatible/src/internal/index.ts). --- File: /ai/content/providers/02-openai-compatible-providers/30-lmstudio.mdx --- --- title: LM Studio description: Use the LM Studio OpenAI compatible API with the AI SDK. --- # LM Studio Provider [LM Studio](https://lmstudio.ai/) is a user interface for running local models. It contains an OpenAI compatible API server that you can use with the AI SDK. You can start the local server under the [Local Server tab](https://lmstudio.ai/docs/basics/server) in the LM Studio UI ("Start Server" button). ## Setup The LM Studio provider is available via the `@ai-sdk/openai-compatible` module as it is compatible with the OpenAI API. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/openai-compatible" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/openai-compatible" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/openai-compatible" dark /> </Tab> </Tabs> ## Provider Instance To use LM Studio, you can create a custom provider instance with the `createOpenAICompatible` function from `@ai-sdk/openai-compatible`: ```ts import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; const lmstudio = createOpenAICompatible({ name: 'lmstudio', baseURL: 'http://localhost:1234/v1', }); ``` <Note> LM Studio uses port `1234` by default, but you can change in the [app's Local Server tab](https://lmstudio.ai/docs/basics/server). </Note> ## Language Models You can interact with local LLMs in [LM Studio](https://lmstudio.ai/docs/basics/server#endpoints-overview) using a provider instance. The first argument is the model id, e.g. `llama-3.2-1b`. ```ts const model = lmstudio('llama-3.2-1b'); ``` ###### To be able to use a model, you need to [download it first](https://lmstudio.ai/docs/basics/download-model). ### Example You can use LM Studio language models to generate text with the `generateText` function: ```ts import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { generateText } from 'ai'; const lmstudio = createOpenAICompatible({ name: 'lmstudio', baseURL: 'https://localhost:1234/v1', }); const { text } = await generateText({ model: lmstudio('llama-3.2-1b'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', maxRetries: 1, // immediately error if the server is not running }); ``` LM Studio language models can also be used with `streamText`. ## Embedding Models You can create models that call the [LM Studio embeddings API](https://lmstudio.ai/docs/basics/server#endpoints-overview) using the `.textEmbeddingModel()` factory method. ```ts const model = lmstudio.textEmbeddingModel( 'text-embedding-nomic-embed-text-v1.5', ); ``` ### Example - Embedding a Single Value ```tsx import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { embed } from 'ai'; const lmstudio = createOpenAICompatible({ name: 'lmstudio', baseURL: 'https://localhost:1234/v1', }); // 'embedding' is a single embedding object (number[]) const { embedding } = await embed({ model: lmstudio.textEmbeddingModel('text-embedding-nomic-embed-text-v1.5'), value: 'sunny day at the beach', }); ``` ### Example - Embedding Many Values When loading data, e.g. when preparing a data store for retrieval-augmented generation (RAG), it is often useful to embed many values at once (batch embedding). The AI SDK provides the [`embedMany`](/docs/reference/ai-sdk-core/embed-many) function for this purpose. Similar to `embed`, you can use it with embeddings models, e.g. `lmstudio.textEmbeddingModel('text-embedding-nomic-embed-text-v1.5')` or `lmstudio.textEmbeddingModel('text-embedding-bge-small-en-v1.5')`. ```tsx import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { embedMany } from 'ai'; const lmstudio = createOpenAICompatible({ name: 'lmstudio', baseURL: 'https://localhost:1234/v1', }); // 'embeddings' is an array of embedding objects (number[][]). // It is sorted in the same order as the input values. const { embeddings } = await embedMany({ model: lmstudio.textEmbeddingModel('text-embedding-nomic-embed-text-v1.5'), values: [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ], }); ``` --- File: /ai/content/providers/02-openai-compatible-providers/35-nim.mdx --- --- title: NVIDIA NIM description: Use NVIDIA NIM OpenAI compatible API with the AI SDK. --- # NVIDIA NIM Provider [NVIDIA NIM](https://www.nvidia.com/en-us/ai/) provides optimized inference microservices for deploying foundation models. It offers an OpenAI-compatible API that you can use with the AI SDK. ## Setup The NVIDIA NIM provider is available via the `@ai-sdk/openai-compatible` module as it is compatible with the OpenAI API. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/openai-compatible" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/openai-compatible" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/openai-compatible" dark /> </Tab> </Tabs> ## Provider Instance To use NVIDIA NIM, you can create a custom provider instance with the `createOpenAICompatible` function from `@ai-sdk/openai-compatible`: ```ts import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; const nim = createOpenAICompatible({ name: 'nim', baseURL: 'https://integrate.api.nvidia.com/v1', headers: { Authorization: `Bearer ${process.env.NIM_API_KEY}`, }, }); ``` <Note> You can obtain an API key and free credits by registering at [NVIDIA Build](https://build.nvidia.com/explore/discover). New users receive 1,000 inference credits to get started. </Note> ## Language Models You can interact with NIM models using a provider instance. For example, to use [DeepSeek-R1](https://build.nvidia.com/deepseek-ai/deepseek-r1), a powerful open-source language model: ```ts const model = nim.chatModel('deepseek-ai/deepseek-r1'); ``` ### Example - Generate Text You can use NIM language models to generate text with the `generateText` function: ```ts import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { generateText } from 'ai'; const nim = createOpenAICompatible({ name: 'nim', baseURL: 'https://integrate.api.nvidia.com/v1', headers: { Authorization: `Bearer ${process.env.NIM_API_KEY}`, }, }); const { text, usage, finishReason } = await generateText({ model: nim.chatModel('deepseek-ai/deepseek-r1'), prompt: 'Tell me the history of the San Francisco Mission-style burrito.', }); console.log(text); console.log('Token usage:', usage); console.log('Finish reason:', finishReason); ``` ### Example - Stream Text NIM language models can also generate text in a streaming fashion with the `streamText` function: ```ts import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { streamText } from 'ai'; const nim = createOpenAICompatible({ name: 'nim', baseURL: 'https://integrate.api.nvidia.com/v1', headers: { Authorization: `Bearer ${process.env.NIM_API_KEY}`, }, }); const result = streamText({ model: nim.chatModel('deepseek-ai/deepseek-r1'), prompt: 'Tell me the history of the Northern White Rhino.', }); 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); ``` NIM language models can also be used with other AI SDK functions like `generateObject` and `streamObject`. <Note> Model support for tool calls and structured object generation varies. For example, the [`meta/llama-3.3-70b-instruct`](https://build.nvidia.com/meta/llama-3_3-70b-instruct) model supports object generation capabilities. Check each model's documentation on NVIDIA Build for specific supported features. </Note> --- File: /ai/content/providers/02-openai-compatible-providers/40-baseten.mdx --- --- title: Baseten description: Use a Baseten OpenAI compatible API with the AI SDK. --- # Baseten Provider [Baseten](https://baseten.co/) is a platform for running and testing LLMs. It allows you to deploy models that are OpenAI API compatible that you can use with the AI SDK. ## Setup The Baseten provider is available via the `@ai-sdk/openai-compatible` module as it is compatible with the OpenAI API. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/openai-compatible" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/openai-compatible" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/openai-compatible" dark /> </Tab> </Tabs> ## Provider Instance To use Baseten, you can create a custom provider instance with the `createOpenAICompatible` function from `@ai-sdk/openai-compatible`: ```ts import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; const BASETEN_MODEL_ID = '<model-id>'; // e.g. 5q3z8xcw const BASETEN_MODEL_URL = `https://model-${BASETEN_MODEL_ID}.api.baseten.co/environments/production/sync/v1`; const baseten = createOpenAICompatible({ name: 'baseten', baseURL: BASETEN_MODEL_URL, headers: { Authorization: `Bearer ${process.env.BASETEN_API_KEY ?? ''}`, }, }); ``` Be sure to have your `BASETEN_API_KEY` set in your environment and the model `<model-id>` ready. The `<model-id>` will be given after you have deployed the model on Baseten. ## Language Models You can create [Baseten models](https://www.baseten.co/library/) using a provider instance. The first argument is the served model name, e.g. `llama`. ```ts const model = baseten('llama'); ``` ### Example You can use Baseten language models to generate text with the `generateText` function: ```ts import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { generateText } from 'ai'; const BASETEN_MODEL_ID = '<model-id>'; // e.g. 5q3z8xcw const BASETEN_MODEL_URL = `https://model-${BASETEN_MODEL_ID}.api.baseten.co/environments/production/sync/v1`; const baseten = createOpenAICompatible({ name: 'baseten', baseURL: BASETEN_MODEL_URL, headers: { Authorization: `Bearer ${process.env.BASETEN_API_KEY ?? ''}`, }, }); const { text } = await generateText({ model: baseten('llama'), prompt: 'Tell me about yourself in one sentence', }); console.log(text); ``` Baseten language models are also able to generate text in a streaming fashion with the `streamText` function: ```ts import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { streamText } from 'ai'; const BASETEN_MODEL_ID = '<model-id>'; // e.g. 5q3z8xcw const BASETEN_MODEL_URL = `https://model-${BASETEN_MODEL_ID}.api.baseten.co/environments/production/sync/v1`; const baseten = createOpenAICompatible({ name: 'baseten', baseURL: BASETEN_MODEL_URL, headers: { Authorization: `Bearer ${process.env.BASETEN_API_KEY ?? ''}`, }, }); const result = streamText({ model: baseten('llama'), prompt: 'Tell me about yourself in one sentence', }); for await (const message of result.textStream) { console.log(message); } ``` Baseten language models can also be used in the `generateObject`, and `streamObject` functions. --- File: /ai/content/providers/02-openai-compatible-providers/index.mdx --- --- title: OpenAI Compatible Providers description: Use OpenAI compatible providers with the AI SDK. --- # OpenAI Compatible Providers You can use the [OpenAI Compatible Provider](https://www.npmjs.com/package/@ai-sdk/openai-compatible) package to use language model providers that implement the OpenAI API. Below we focus on the general setup and provider instance creation. You can also [write a custom provider package leveraging the OpenAI Compatible package](/providers/openai-compatible-providers/custom-providers). We provide detailed documentation for the following OpenAI compatible providers: - [LM Studio](/providers/openai-compatible-providers/lmstudio) - [NIM](/providers/openai-compatible-providers/nim) - [Baseten](/providers/openai-compatible-providers/baseten) The general setup and provider instance creation is the same for all of these providers. ## Setup The OpenAI Compatible provider is available via the `@ai-sdk/openai-compatible` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/openai-compatible" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/openai-compatible" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/openai-compatible" dark /> </Tab> </Tabs> ## Provider Instance To use an OpenAI compatible provider, you can create a custom provider instance with the `createOpenAICompatible` function from `@ai-sdk/openai-compatible`: ```ts import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; const provider = createOpenAICompatible({ name: 'provider-name', apiKey: process.env.PROVIDER_API_KEY, baseURL: 'https://api.provider.com/v1', includeUsage: true, // Include usage information in streaming responses }); ``` You can use the following optional settings to customize the provider instance: - **baseURL** _string_ Set the URL prefix for API calls. - **apiKey** _string_ API key for authenticating requests. If specified, adds an `Authorization` header to request headers with the value `Bearer <apiKey>`. This will be added before any headers potentially specified in the `headers` option. - **headers** _Record&lt;string,string&gt;_ Optional custom headers to include in requests. These will be added to request headers after any headers potentially added by use of the `apiKey` option. - **queryParams** _Record&lt;string,string&gt;_ Optional custom url query parameters to include in request urls. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. - **includeUsage** _boolean_ Include usage information in streaming responses. When enabled, usage data will be included in the response metadata for streaming requests. Defaults to `undefined` (`false`). ## Language Models You can create provider models using a provider instance. The first argument is the model id, e.g. `model-id`. ```ts const model = provider('model-id'); ``` ### Example You can use provider language models to generate text with the `generateText` function: ```ts import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { generateText } from 'ai'; const provider = createOpenAICompatible({ name: 'provider-name', apiKey: process.env.PROVIDER_API_KEY, baseURL: 'https://api.provider.com/v1', }); const { text } = await generateText({ model: provider('model-id'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` ### Including model ids for auto-completion ```ts import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { generateText } from 'ai'; type ExampleChatModelIds = | 'meta-llama/Llama-3-70b-chat-hf' | 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo' | (string & {}); type ExampleCompletionModelIds = | 'codellama/CodeLlama-34b-Instruct-hf' | 'Qwen/Qwen2.5-Coder-32B-Instruct' | (string & {}); type ExampleEmbeddingModelIds = | 'BAAI/bge-large-en-v1.5' | 'bert-base-uncased' | (string & {}); const model = createOpenAICompatible< ExampleChatModelIds, ExampleCompletionModelIds, ExampleEmbeddingModelIds >({ name: 'example', apiKey: process.env.PROVIDER_API_KEY, baseURL: 'https://api.example.com/v1', }); // Subsequent calls to e.g. `model.chatModel` will auto-complete the model id // from the list of `ExampleChatModelIds` while still allowing free-form // strings as well. const { text } = await generateText({ model: model.chatModel('meta-llama/Llama-3-70b-chat-hf'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` ### Custom query parameters Some providers may require custom query parameters. An example is the [Azure AI Model Inference API](https://learn.microsoft.com/en-us/azure/machine-learning/reference-model-inference-chat-completions?view=azureml-api-2) which requires an `api-version` query parameter. You can set these via the optional `queryParams` provider setting. These will be added to all requests made by the provider. ```ts highlight="7-9" import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; const provider = createOpenAICompatible({ name: 'provider-name', apiKey: process.env.PROVIDER_API_KEY, baseURL: 'https://api.provider.com/v1', queryParams: { 'api-version': '1.0.0', }, }); ``` For example, with the above configuration, API requests would include the query parameter in the URL like: `https://api.provider.com/v1/chat/completions?api-version=1.0.0`. ## Provider-specific options The OpenAI Compatible provider supports adding provider-specific options to the request body. These are specified with the `providerOptions` field in the request body. For example, if you create a provider instance with the name `provider-name`, you can add a `custom-option` field to the request body like this: ```ts const provider = createOpenAICompatible({ name: 'provider-name', apiKey: process.env.PROVIDER_API_KEY, baseURL: 'https://api.provider.com/v1', }); const { text } = await generateText({ model: provider('model-id'), prompt: 'Hello', providerOptions: { 'provider-name': { customOption: 'magic-value' }, }, }); ``` The request body sent to the provider will include the `customOption` field with the value `magic-value`. This gives you an easy way to add provider-specific options to requests without having to modify the provider or AI SDK code. ## Custom Metadata Extraction The OpenAI Compatible provider supports extracting provider-specific metadata from API responses through metadata extractors. These extractors allow you to capture additional information returned by the provider beyond the standard response format. Metadata extractors receive the raw, unprocessed response data from the provider, giving you complete flexibility to extract any custom fields or experimental features that the provider may include. This is particularly useful when: - Working with providers that include non-standard response fields - Experimenting with beta or preview features - Capturing provider-specific metrics or debugging information - Supporting rapid provider API evolution without SDK changes Metadata extractors work with both streaming and non-streaming chat completions and consist of two main components: 1. A function to extract metadata from complete responses 2. A streaming extractor that can accumulate metadata across chunks in a streaming response Here's an example metadata extractor that captures both standard and custom provider data: ```typescript const myMetadataExtractor: MetadataExtractor = { // Process complete, non-streaming responses extractMetadata: ({ parsedBody }) => { // You have access to the complete raw response // Extract any fields the provider includes return { myProvider: { standardUsage: parsedBody.usage, experimentalFeatures: parsedBody.beta_features, customMetrics: { processingTime: parsedBody.server_timing?.total_ms, modelVersion: parsedBody.model_version, // ... any other provider-specific data }, }, }; }, // Process streaming responses createStreamExtractor: () => { let accumulatedData = { timing: [], customFields: {}, }; return { // Process each chunk's raw data processChunk: parsedChunk => { if (parsedChunk.server_timing) { accumulatedData.timing.push(parsedChunk.server_timing); } if (parsedChunk.custom_data) { Object.assign(accumulatedData.customFields, parsedChunk.custom_data); } }, // Build final metadata from accumulated data buildMetadata: () => ({ myProvider: { streamTiming: accumulatedData.timing, customData: accumulatedData.customFields, }, }), }; }, }; ``` You can provide a metadata extractor when creating your provider instance: ```typescript const provider = createOpenAICompatible({ name: 'my-provider', apiKey: process.env.PROVIDER_API_KEY, baseURL: 'https://api.provider.com/v1', metadataExtractor: myMetadataExtractor, }); ``` The extracted metadata will be included in the response under the `providerMetadata` field: ```typescript const { text, providerMetadata } = await generateText({ model: provider('model-id'), prompt: 'Hello', }); console.log(providerMetadata.myProvider.customMetric); ``` This allows you to access provider-specific information while maintaining a consistent interface across different providers. --- File: /ai/content/providers/03-community-providers/01-custom-providers.mdx --- --- title: Writing a Custom Provider description: Learn how to write a custom provider for the AI SDK --- # Writing a Custom Provider The AI SDK provides a [Language Model Specification](https://github.com/vercel/ai/tree/main/packages/provider/src/language-model/v2) that enables you to create custom providers compatible with the AI SDK. This specification ensures consistency across different providers. ## Publishing Your Provider Please publish your custom provider in your own GitHub repository and as an NPM package. You are responsible for hosting and maintaining your provider. Once published, you can submit a PR to the AI SDK repository to add your provider to the [Community Providers](/providers/community-providers) documentation section. Use the [OpenRouter provider documentation](https://github.com/vercel/ai/blob/main/content/providers/03-community-providers/13-openrouter.mdx) as a template for your documentation. ## Why the Language Model Specification? The Language Model Specification V2 is a standardized specification for interacting with language models that provides a unified abstraction layer across all AI providers. This specification creates a consistent interface that works seamlessly with different language models, ensuring that developers can interact with any provider using the same patterns and methods. It enables: <Note> If you open-source a provider, we'd love to promote it here. Please send us a PR to add it to the [Community Providers](/providers/community-providers) section. </Note> ## Understanding the V2 Specification The Language Model Specification V2 creates a robust abstraction layer that works across all current and future AI providers. By establishing a standardized interface, it provides the flexibility to support emerging LLM capabilities while ensuring your application code remains provider-agnostic and future-ready. ### Architecture At its heart, the V2 specification defines three main interfaces: 1. **ProviderV2**: The top-level interface that serves as a factory for different model types 2. **LanguageModelV2**: The primary interface for text generation models 3. **EmbeddingModelV2** and **ImageModelV2**: Interfaces for embeddings and image generation ### `ProviderV2` The `ProviderV2` interface acts as the entry point: ```ts interface ProviderV2 { languageModel(modelId: string): LanguageModelV2; textEmbeddingModel(modelId: string): EmbeddingModelV2<string>; imageModel(modelId: string): ImageModelV2; } ``` ### `LanguageModelV2` The `LanguageModelV2` interface defines the methods your provider must implement: ```ts interface LanguageModelV2 { specificationVersion: 'V2'; provider: string; modelId: string; supportedUrls: Record<string, RegExp[]>; doGenerate(options: LanguageModelV2CallOptions): Promise<GenerateResult>; doStream(options: LanguageModelV2CallOptions): Promise<StreamResult>; } ``` Key aspects: - **specificationVersion**: Must be 'V2' - **supportedUrls**: Declares which URLs (for file parts) the provider can handle natively - **doGenerate/doStream**: methods for non-streaming and streaming generation ### Understanding Input vs Output Before diving into the details, it's important to understand the distinction between two key concepts in the V2 specification: 1. **LanguageModelV2Content**: The specification for what the models generate 2. **LanguageModelV2Prompt**: The specification for what you send to the model ### `LanguageModelV2Content` The V2 specification supports five distinct content types that models can generate, each designed for specific use cases: #### Text Content The fundamental building block for all text generation: ```ts type LanguageModelV2Text = { type: 'text'; text: string; }; ``` This is used for standard model responses, system messages, and any plain text output. #### Tool Calls Enable models to invoke functions with structured arguments: ```ts type LanguageModelV2ToolCall = { type: 'tool-call'; toolCallType: 'function'; toolCallId: string; toolName: string; args: string; }; ``` The `toolCallId` is crucial for correlating tool results back to their calls, especially in streaming scenarios. #### File Generation Support for multimodal output generation: ```ts type LanguageModelV2File = { type: 'file'; mediaType: string; // IANA media type (e.g., 'image/png', 'audio/mp3') data: string | Uint8Array; // Generated file data as base64 encoded strings or binary data }; ``` This enables models to generate images, audio, documents, and other file types directly. #### Reasoning Dedicated support for chain-of-thought reasoning (essential for models like OpenAI's o1): ```ts type LanguageModelV2Reasoning = { type: 'reasoning'; text: string; /** * Optional provider-specific metadata for the reasoning part. */ providerMetadata?: SharedV2ProviderMetadata; }; ``` Reasoning content is tracked separately from regular text, allowing for proper token accounting and UI presentation. #### Sources ```ts type LanguageModelV2Source = { type: 'source'; sourceType: 'url'; id: string; url: string; title?: string; providerMetadata?: SharedV2ProviderMetadata; }; ``` ### `LanguageModelV2Prompt` The V2 prompt format (`LanguageModelV2Prompt`) is designed as a flexible message array that supports multimodal inputs: #### Message Roles Each message has a specific role with allowed content types: - **System**: Model instructions (text only) ```ts { role: 'system', content: string } ``` - **User**: Human inputs supporting text and files ```ts { role: 'user', content: Array<LanguageModelV2TextPart | LanguageModelV2FilePart> } ``` - **Assistant**: Model outputs with full content type support ```ts { role: 'assistant', content: Array<LanguageModelV2TextPart | LanguageModelV2FilePart | LanguageModelV2ReasoningPart | LanguageModelV2ToolCallPart> } ``` - **Tool**: Results from tool executions ```ts { role: 'tool', content: Array<LanguageModelV2ToolResultPart> } ``` #### Prompt Parts Prompt parts are the building blocks of messages in the prompt structure. While `LanguageModelV2Content` represents the model's output content, prompt parts are specifically designed for constructing input messages. Each message role supports different types of prompt parts: - **System messages**: Only support text content - **User messages**: Support text and file parts - **Assistant messages**: Support text, file, reasoning, and tool call parts - **Tool messages**: Only support tool result parts Let's explore each prompt part type: ##### Text Parts The most basic prompt part, containing plain text content: ```ts interface LanguageModelV2TextPart { type: 'text'; text: string; providerOptions?: SharedV2ProviderOptions; } ``` ##### Reasoning Parts Used in assistant messages to capture the model's reasoning process: ```ts interface LanguageModelV2ReasoningPart { type: 'reasoning'; text: string; providerOptions?: SharedV2ProviderOptions; } ``` ##### File Parts Enable multimodal inputs by including files in prompts: ```ts interface LanguageModelV2FilePart { type: 'file'; filename?: string; data: LanguageModelV2DataContent; mediaType: string; providerOptions?: SharedV2ProviderOptions; } ``` The `data` field offers flexibility: - **Uint8Array**: Direct binary data - **string**: Base64-encoded data - **URL**: Reference to external content (if supported by provider via `supportedUrls`) ##### Tool Call Parts Represent tool calls made by the assistant: ```ts interface LanguageModelV2ToolCallPart { type: 'tool-call'; toolCallId: string; toolName: string; args: unknown; providerOptions?: SharedV2ProviderOptions; } ``` ##### Tool Result Parts Contain the results of executed tool calls: ```ts interface LanguageModelV2ToolResultPart { type: 'tool-result'; toolCallId: string; toolName: string; result: unknown; isError?: boolean; content?: Array<{ type: 'text' | 'image'; text?: string; data?: string; // base64 encoded image data mediaType?: string; }>; providerOptions?: SharedV2ProviderOptions; } ``` The optional `content` field enables rich tool results including images, providing more flexibility than the basic `result` field. ### Streaming #### Stream Parts The streaming system uses typed events for different stages: 1. **Stream Lifecycle Events**: - `stream-start`: Initial event with any warnings about unsupported features - `response-metadata`: Model information and response headers - `finish`: Final event with usage statistics and finish reason - `error`: Error events that can occur at any point 2. **Content Events**: - All content types (`text`, `file`, `reasoning`, `source`, `tool-call`) stream directly - `tool-call-delta`: Incremental updates for tool call arguments - `reasoning-part-finish`: Explicit marker for reasoning section completion Example stream sequence: ```ts { type: 'stream-start', warnings: [] } { type: 'text', text: 'Hello' } { type: 'text', text: ' world' } { type: 'tool-call', toolCallId: '1', toolName: 'search', args: {...} } { type: 'response-metadata', modelId: 'gpt-4.1', ... } { type: 'finish', usage: { inputTokens: 10, outputTokens: 20 }, finishReason: 'stop' } ``` #### Usage Tracking Enhanced usage information: ```ts type LanguageModelV2Usage = { inputTokens: number | undefined; outputTokens: number | undefined; totalTokens: number | undefined; reasoningTokens?: number | undefined; cachedInputTokens?: number | undefined; }; ``` ### Tools The V2 specification supports two types of tools: #### Function Tools Standard user-defined functions with JSON Schema validation: ```ts type LanguageModelV2FunctionTool = { type: 'function'; name: string; description?: string; parameters: JSONSchema7; // Full JSON Schema support }; ``` #### Provider-Defined Client Tools Native provider capabilities exposed as tools: ```ts export type LanguageModelV2ProviderClientDefinedTool = { type: 'provider-defined-client'; id: string; // e.g., 'anthropic.computer-use' name: string; // Human-readable name args: Record<string, unknown>; }; ``` Tool choice can be controlled via: ```ts toolChoice: 'auto' | 'none' | 'required' | { type: 'tool', toolName: string }; ``` ### Native URL Support Providers can declare URLs they can access directly: ```ts supportedUrls: { 'image/*': [/^https:\/\/cdn\.example\.com\/.*/], 'application/pdf': [/^https:\/\/docs\.example\.com\/.*/], 'audio/*': [/^https:\/\/media\.example\.com\/.*/] } ``` The AI SDK checks these patterns before downloading any URL-based content. ### Provider Options The specification includes a flexible system for provider-specific features without breaking the standard interface: ```ts providerOptions: { anthropic: { cacheControl: true, maxTokens: 4096 }, openai: { parallelToolCalls: false, responseFormat: { type: 'json_object' } } } ``` Provider options can be specified at multiple levels: - **Call level**: In `LanguageModelV2CallOptions` - **Message level**: On individual messages - **Part level**: On specific content parts (text, file, etc.) This layered approach allows fine-grained control while maintaining compatibility. ### Error Handling The V2 specification emphasizes robust error handling: 1. **Streaming Errors**: Can be emitted at any point via `{ type: 'error', error: unknown }` 2. **Warnings**: Non-fatal issues reported in `stream-start` and response objects 3. **Finish Reasons**: Clear indication of why generation stopped: - `'stop'`: Natural completion - `'length'`: Hit max tokens - `'content-filter'`: Safety filtering - `'tool-calls'`: Stopped to execute tools - `'error'`: Generation failed - `'other'`: Provider-specific reasons ## Provider Implementation Guide To implement a custom language model provider, you'll need to install the required packages: ```bash npm install @ai-sdk/provider @ai-sdk/provider-utils ``` Implementing a custom language model provider involves several steps: - Creating an entry point - Adding a language model implementation - Mapping the input (prompt, tools, settings) - Processing the results (generate, streaming, tool calls) - Supporting object generation <Note> The best way to get started is to use the [Mistral provider](https://github.com/vercel/ai/tree/main/packages/mistral) as a reference implementation. </Note> ### Step 1: Create the Provider Entry Point Start by creating a `provider.ts` file that exports a factory function and a default instance: ```ts filename="provider.ts" import { generateId, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { ProviderV2 } from '@ai-sdk/provider'; import { CustomChatLanguageModel } from './custom-chat-language-model'; // Define your provider interface extending ProviderV2 interface CustomProvider extends ProviderV2 { (modelId: string, settings?: CustomChatSettings): CustomChatLanguageModel; // Add specific methods for different model types languageModel( modelId: string, settings?: CustomChatSettings, ): CustomChatLanguageModel; } // Provider settings interface CustomProviderSettings { /** * Base URL for API calls */ baseURL?: string; /** * API key for authentication */ apiKey?: string; /** * Custom headers for requests */ headers?: Record<string, string>; } // Factory function to create provider instance function createCustom(options: CustomProviderSettings = {}): CustomProvider { const createChatModel = ( modelId: string, settings: CustomChatSettings = {}, ) => new CustomChatLanguageModel(modelId, settings, { provider: 'custom', baseURL: withoutTrailingSlash(options.baseURL) ?? 'https://api.custom.ai/v1', headers: () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'CUSTOM_API_KEY', description: 'Custom Provider', })}`, ...options.headers, }), generateId: options.generateId ?? generateId, }); const provider = function (modelId: string, settings?: CustomChatSettings) { if (new.target) { throw new Error( 'The model factory function cannot be called with the new keyword.', ); } return createChatModel(modelId, settings); }; provider.languageModel = createChatModel; return provider as CustomProvider; } // Export default provider instance const custom = createCustom(); ``` ### Step 2: Implement the Language Model Create a `custom-chat-language-model.ts` file that implements `LanguageModelV2`: ```ts filename="custom-chat-language-model.ts" import { LanguageModelV2, LanguageModelV2CallOptions } from '@ai-sdk/provider'; import { postJsonToApi } from '@ai-sdk/provider-utils'; class CustomChatLanguageModel implements LanguageModelV2 { readonly specificationVersion = 'V2'; readonly provider: string; readonly modelId: string; constructor( modelId: string, settings: CustomChatSettings, config: CustomChatConfig, ) { this.provider = config.provider; this.modelId = modelId; // Initialize with settings and config } // Convert AI SDK prompt to provider format private getArgs(options: LanguageModelV2CallOptions) { const warnings: LanguageModelV2CallWarning[] = []; // Map messages to provider format const messages = this.convertToProviderMessages(options.prompt); // Handle tools if provided const tools = options.tools ? this.prepareTools(options.tools, options.toolChoice) : undefined; // Build request body const body = { model: this.modelId, messages, temperature: options.temperature, max_tokens: options.maxOutputTokens, stop: options.stopSequences, tools, // ... other parameters }; return { args: body, warnings }; } async doGenerate(options: LanguageModelV2CallOptions) { const { args, warnings } = this.getArgs(options); // Make API call const response = await postJsonToApi({ url: `${this.config.baseURL}/chat/completions`, headers: this.config.headers(), body: args, abortSignal: options.abortSignal, }); // Convert provider response to AI SDK format const content: LanguageModelV2Content[] = []; // Extract text content if (response.choices[0].message.content) { content.push({ type: 'text', text: response.choices[0].message.content, }); } // Extract tool calls if (response.choices[0].message.tool_calls) { for (const toolCall of response.choices[0].message.tool_calls) { content.push({ type: 'tool-call', toolCallType: 'function', toolCallId: toolCall.id, toolName: toolCall.function.name, args: JSON.stringify(toolCall.function.arguments), }); } } return { content, finishReason: this.mapFinishReason(response.choices[0].finish_reason), usage: { inputTokens: response.usage?.prompt_tokens, outputTokens: response.usage?.completion_tokens, totalTokens: response.usage?.total_tokens, }, request: { body: args }, response: { body: response }, warnings, }; } async doStream(options: LanguageModelV2CallOptions) { const { args, warnings } = this.getArgs(options); // Create streaming response const response = await fetch(`${this.config.baseURL}/chat/completions`, { method: 'POST', headers: { ...this.config.headers(), 'Content-Type': 'application/json', }, body: JSON.stringify({ ...args, stream: true }), signal: options.abortSignal, }); // Transform stream to AI SDK format const stream = response .body!.pipeThrough(new TextDecoderStream()) .pipeThrough(this.createParser()) .pipeThrough(this.createTransformer(warnings)); return { stream, warnings }; } // Supported URL patterns for native file handling get supportedUrls() { return { 'image/*': [/^https:\/\/example\.com\/images\/.*/], }; } } ``` ### Step 3: Implement Message Conversion Map AI SDK messages to your provider's format: ```ts filename="custom-chat-language-model.ts#L50-100" private convertToProviderMessages(prompt: LanguageModelV2Prompt) { return prompt.map((message) => { switch (message.role) { case 'system': return { role: 'system', content: message.content }; case 'user': return { role: 'user', content: message.content.map((part) => { switch (part.type) { case 'text': return { type: 'text', text: part.text }; case 'file': return { type: 'image_url', image_url: { url: this.convertFileToUrl(part.data), }, }; default: throw new Error(`Unsupported part type: ${part.type}`); } }), }; case 'assistant': // Handle assistant messages with text, tool calls, etc. return this.convertAssistantMessage(message); case 'tool': // Handle tool results return this.convertToolMessage(message); default: throw new Error(`Unsupported message role: ${message.role}`); } }); } ``` ### Step 4: Implement Streaming Create a streaming transformer that converts provider chunks to AI SDK stream parts: ```ts filename="custom-chat-language-model.ts#L150-200" private createTransformer(warnings: LanguageModelV2CallWarning[]) { let isFirstChunk = true; return new TransformStream<ParsedChunk, LanguageModelV2StreamPart>({ async transform(chunk, controller) { // Send warnings with first chunk if (isFirstChunk) { controller.enqueue({ type: 'stream-start', warnings }); isFirstChunk = false; } // Handle different chunk types if (chunk.choices?.[0]?.delta?.content) { controller.enqueue({ type: 'text', text: chunk.choices[0].delta.content, }); } if (chunk.choices?.[0]?.delta?.tool_calls) { for (const toolCall of chunk.choices[0].delta.tool_calls) { controller.enqueue({ type: 'tool-call-delta', toolCallType: 'function', toolCallId: toolCall.id, toolName: toolCall.function.name, argsTextDelta: toolCall.function.arguments, }); } } // Handle finish reason if (chunk.choices?.[0]?.finish_reason) { controller.enqueue({ type: 'finish', finishReason: this.mapFinishReason(chunk.choices[0].finish_reason), usage: { inputTokens: chunk.usage?.prompt_tokens, outputTokens: chunk.usage?.completion_tokens, totalTokens: chunk.usage?.total_tokens, }, }); } }, }); } ``` ### Step 5: Handle Errors Use standardized AI SDK errors for consistent error handling: ```ts filename="custom-chat-language-model.ts#L250-280" import { APICallError, InvalidResponseDataError, TooManyRequestsError, } from '@ai-sdk/provider'; private handleError(error: unknown): never { if (error instanceof Response) { const status = error.status; if (status === 429) { throw new TooManyRequestsError({ cause: error, retryAfter: this.getRetryAfter(error), }); } throw new APICallError({ statusCode: status, statusText: error.statusText, cause: error, isRetryable: status >= 500 && status < 600, }); } throw error; } ``` ## Next Steps - Dig into the [Language Model Specification V2](https://github.com/vercel/ai/tree/main/packages/provider/src/language-model/V2) - Check out the [Mistral provider](https://github.com/vercel/ai/tree/main/packages/mistral) reference implementation - Check out [provider utilities](https://github.com/vercel/ai/tree/main/packages/provider-utils) for helpful functions - Test your provider with the AI SDK's built-in examples - Explore the V2 types in detail at [`@ai-sdk/provider`](https://github.com/vercel/ai/tree/main/packages/provider/src/language-model/V2) --- File: /ai/content/providers/03-community-providers/02-qwen.mdx --- --- title: Qwen description: Learn how to use the Qwen provider. --- # Qwen Provider <Note type="warning"> This community provider is not yet compatible with AI SDK 5. Please wait for the provider to be updated or consider using an [AI SDK 5 compatible provider](/providers/ai-sdk-providers). </Note> [younis-ahmed/qwen-ai-provider](https://github.com/younis-ahmed/qwen-ai-provider) is a community provider that uses [Qwen](https://www.alibabacloud.com/en/solutions/generative-ai/qwen) to provide language model support for the AI SDK. ## Setup The Qwen provider is available in the `qwen-ai-provider` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add qwen-ai-provider" dark /> </Tab> <Tab> <Snippet text="npm install qwen-ai-provider" dark /> </Tab> <Tab> <Snippet text="yarn add qwen-ai-provider" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `qwen` from `qwen-ai-provider`: ```ts import { qwen } from 'qwen-ai-provider'; ``` If you need a customized setup, you can import `createQwen` from `qwen-ai-provider` and create a provider instance with your settings: ```ts import { createQwen } from 'qwen-ai-provider'; const qwen = createQwen({ // optional settings, e.g. // baseURL: 'https://qwen/api/v1', }); ``` You can use the following optional settings to customize the Qwen provider instance: - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://dashscope-intl.aliyuncs.com/compatible-mode/v1`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `DASHSCOPE_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Language Models You can create models that call the [Qwen chat API](https://www.alibabacloud.com/help/en/model-studio/developer-reference/use-qwen-by-calling-api) using a provider instance. The first argument is the model id, e.g. `qwen-plus`. Some Qwen chat models support tool calls. ```ts const model = qwen('qwen-plus'); ``` ### Example You can use Qwen language models to generate text with the `generateText` function: ```ts import { qwen } from 'qwen-ai-provider'; import { generateText } from 'ai'; const { text } = await generateText({ model: qwen('qwen-plus'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` Qwen language models can also be used in the `streamText`, `generateObject`, and `streamObject` functions (see [AI SDK Core](/docs/ai-sdk-core)). ### Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | ------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `qwen-vl-max` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `qwen-plus-latest` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `qwen-max` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `qwen2.5-72b-instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `qwen2.5-14b-instruct-1m` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `qwen2.5-vl-72b-instruct` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Note> The table above lists popular models. Please see the [Qwen docs](https://www.alibabacloud.com/help/en/model-studio/getting-started/models) for a full list of available models. The table above lists popular models. You can also pass any available provider model ID as a string if needed. </Note> ## Embedding Models You can create models that call the [Qwen embeddings API](https://www.alibabacloud.com/help/en/model-studio/getting-started/models#cff6607866tsg) using the `.textEmbeddingModel()` factory method. ```ts const model = qwen.textEmbeddingModel('text-embedding-v3'); ``` ### Model Capabilities | Model | Default Dimensions | Maximum number of rows | Maximum tokens per row | | ------------------- | ------------------ | ---------------------- | ---------------------- | | `text-embedding-v3` | 1024 | 6 | 8,192 | --- File: /ai/content/providers/03-community-providers/03-ollama.mdx --- --- title: Ollama description: Learn how to use the Ollama provider. --- # Ollama Provider <Note type="warning"> This community provider is not yet compatible with AI SDK 5. It uses the deprecated `.embedding()` method instead of the standard `.textEmbeddingModel()` method. Please wait for the provider to be updated or consider using an [AI SDK 5 compatible provider](/providers/ai-sdk-providers). </Note> [sgomez/ollama-ai-provider](https://github.com/sgomez/ollama-ai-provider) is a community provider that uses [Ollama](https://ollama.com/) to provide language model support for the AI SDK. ## Setup The Ollama provider is available in the `ollama-ai-provider` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add ollama-ai-provider" dark /> </Tab> <Tab> <Snippet text="npm install ollama-ai-provider" dark /> </Tab> <Tab> <Snippet text="yarn add ollama-ai-provider" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `ollama` from `ollama-ai-provider`: ```ts import { ollama } from 'ollama-ai-provider'; ``` If you need a customized setup, you can import `createOllama` from `ollama-ai-provider` and create a provider instance with your settings: ```ts import { createOllama } from 'ollama-ai-provider'; const ollama = createOllama({ // optional settings, e.g. baseURL: 'https://api.ollama.com', }); ``` You can use the following optional settings to customize the Ollama provider instance: - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `http://localhost:11434/api`. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. ## Language Models You can create models that call the [Ollama Chat Completion API](https://github.com/ollama/ollama/blob/main/docs/api.md#generate-a-chat-completion) using the provider instance. The first argument is the model id, e.g. `phi3`. Some models have multi-modal capabilities. ```ts const model = ollama('phi3'); ``` You can find more models on the [Ollama Library](https://ollama.com/library) homepage. ### Model Capabilities This provider is capable of generating and streaming text and objects. Object generation may fail depending on the model used and the schema used. The following models have been tested with image inputs: - llava - llava-llama3 - llava-phi3 - moondream ## Embedding Models You can create models that call the [Ollama embeddings API](https://github.com/ollama/ollama/blob/main/docs/api.md#generate-embeddings) using the `.embedding()` factory method. ```ts const model = ollama.embedding('nomic-embed-text'); ``` --- File: /ai/content/providers/03-community-providers/05-a2a.mdx --- --- title: A2A description: A2A Protocol Provider for the AI SDK --- # A2A The [dracoblue/a2a-ai-provider](https://github.com/dracoblue/a2a-ai-provider) is a community provider enables the use of [A2A protocol](https://a2aproject.github.io/A2A/specification/) compliant agents with the [AI SDK](https://ai-sdk.dev/). This allows developers to stream, send, and receive text, tool calls, and artifacts using a standardized JSON-RPC interface over HTTP. <Note type="warning"> The `a2a-ai-provider` package is under constant development. </Note> The provider supports (by using the official a2a-js sdk [@a2a-js/sdk](https://github.com/a2aproject/a2a-js)): - **Streaming Text Responses** via `sendSubscribe` and SSE - **File & Artifact Uploads** to the A2A server - **Multi-modal Messaging** with support for text and file parts - **Full JSON-RPC 2.0 Compliance** for A2A-compatible LLM agents Learn more about A2A at the [A2A Project Site](https://a2aproject.github.io/A2A/). ## Setup Install the `a2a-ai-provider` from npm: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add a2a-ai-provider" dark /> </Tab> <Tab> <Snippet text="npm install a2a-ai-provider" dark /> </Tab> <Tab> <Snippet text="yarn add a2a-ai-provider" dark /> </Tab> </Tabs> ## Provider Instance To create a provider instance for an A2A server: ```ts import { a2a } from 'a2a-ai-provider'; ``` ## Examples You can now use the provider with the AI SDK like this: ### `generateText` ```ts import { a2a } from 'a2a-ai-provider'; import { generateText } from 'ai'; const result = await generateText({ model: a2a('https://your-a2a-server.example.com'), prompt: 'What is love?', }); console.log(result.text); ``` ### `streamText` ```ts import { a2a } from 'a2a-ai-provider'; import { streamText } from 'ai'; const chatId = 'unique-chat-id'; // for each conversation to keep history in a2a server const streamResult = streamText({ model: a2a('https://your-a2a-server.example.com'), prompt: 'What is love?', providerOptions: { a2a: { contextId: chatId, }, }, }); await streamResult.consumeStream(); console.log(await streamResult.content); ``` ## Features - **Text Streaming**: Streams token-by-token output from the A2A server - **File Uploads**: Send files as part of your prompts - **Artifact Handling**: Receives file artifacts in streamed or final results ## Additional Resources - [GitHub Repository](https://github.com/DracoBlue/a2a-ai-provider) - [A2A Protocol Spec](https://a2aproject.github.io/A2A/specification/) - License: MIT --- File: /ai/content/providers/03-community-providers/08-friendliai.mdx --- --- title: FriendliAI description: Learn how to use the FriendliAI Provider for the AI SDK. --- # FriendliAI Provider The [FriendliAI](https://friendli.ai/) provider supports both open-source LLMs via [Friendli Serverless Endpoints](https://friendli.ai/products/serverless-endpoints) and custom models via [Dedicated Endpoints](https://friendli.ai/products/dedicated-endpoints). It creates language model objects that can be used with the `generateText`, `streamText`, `generateObject`, and `streamObject` functions. ## Setup The Friendli provider is available via the `@friendliai/ai-provider` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @friendliai/ai-provider" dark /> </Tab> <Tab> <Snippet text="npm install @friendliai/ai-provider" dark /> </Tab> <Tab> <Snippet text="yarn add @friendliai/ai-provider" dark /> </Tab> </Tabs> ### Credentials The tokens required for model usage can be obtained from the [Friendli suite](https://suite.friendli.ai/). To use the provider, you need to set the `FRIENDLI_TOKEN` environment variable with your personal access token. ```bash export FRIENDLI_TOKEN="YOUR_FRIENDLI_TOKEN" ``` Check the [FriendliAI documentation](https://friendli.ai/docs/guides/personal_access_tokens) for more information. ## Provider Instance You can import the default provider instance `friendliai` from `@friendliai/ai-provider`: ```ts import { friendli } from '@friendliai/ai-provider'; ``` ## Language Models You can create [FriendliAI models](https://friendli.ai/docs/guides/serverless_endpoints/text_generation#model-supports) using a provider instance. The first argument is the model id, e.g. `meta-llama-3.1-8b-instruct`. ```ts const model = friendli('meta-llama-3.1-8b-instruct'); ``` ### Example: Generating text You can use FriendliAI language models to generate text with the `generateText` function: ```ts import { friendli } from '@friendliai/ai-provider'; import { generateText } from 'ai'; const { text } = await generateText({ model: friendli('meta-llama-3.1-8b-instruct'), prompt: 'What is the meaning of life?', }); console.log(text); ``` ### Example: Reasoning FriendliAI exposes the thinking of `deepseek-r1` in the generated text using the `<think>` tag. You can use the `extractReasoningMiddleware` to extract this reasoning and expose it as a `reasoning` property on the result: ```ts import { friendli } from '@friendliai/ai-provider'; import { wrapLanguageModel, extractReasoningMiddleware } from 'ai'; const enhancedModel = wrapLanguageModel({ model: friendli('deepseek-r1'), middleware: extractReasoningMiddleware({ tagName: 'think' }), }); const { text, reasoning } = await generateText({ model: enhancedModel, prompt: 'Explain quantum entanglement.', }); ``` ### Example: Structured Outputs (regex) The regex option allows you to control the format of your LLM's output by specifying patterns. This can be particularly useful when you need: - Specific formats like CSV - Restrict output to specific characters such as Korean or Japanese This feature is available with both `generateText` and `streamText` functions. For a deeper understanding of how to effectively use regex patterns with LLMs, check out our detailed guide in the [Structured Output LLM Agents](https://friendli.ai/blog/structured-output-llm-agents) blog post. ```ts highlight="6" import { friendli } from '@friendliai/ai-provider'; import { generateText } from 'ai'; const { text } = await generateText({ model: friendli('meta-llama-3.1-8b-instruct', { regex: new RegExp('[\n ,.?!0-9\uac00-\ud7af]*'), }), prompt: 'Who is the first king of the Joseon Dynasty?', }); console.log(text); ``` ### Example: Structured Outputs (json) Structured outputs are a form of guided generation. The JSON schema is used as a grammar and the outputs will always conform to the schema. ```ts import { friendli } from '@friendliai/ai-provider'; import { generateObject } from 'ai'; import { z } from 'zod'; const { object } = await generateObject({ model: friendli('meta-llama-3.3-70b-instruct'), schemaName: 'CalendarEvent', schema: z.object({ name: z.string(), date: z.string(), participants: z.array(z.string()), }), system: 'Extract the event information.', prompt: 'Alice and Bob are going to a science fair on Friday.', }); console.log(object); ``` ### Example: Using built-in tools <Note type="warning">Built-in tools are currently in beta.</Note> If you use `@friendliai/ai-provider`, you can use the [built-in tools](https://friendli.ai/docs/guides/serverless_endpoints/tools/built_in_tools) via the `tools` option. Built-in tools allow models to use tools to generate better answers. For example, a `web:search` tool can provide up-to-date answers to current questions. ```ts highlight="1,5,6,7" import { friendli } from '@friendliai/ai-provider'; import { streamText } from 'ai'; const result = streamText({ model: friendli('meta-llama-3.3-70b-instruct', { tools: [{ type: 'web:search' }, { type: 'math:calculator' }], }), prompt: 'Find the current USD to CAD exchange rate and calculate how much $5,000 USD would be in Canadian dollars.', }); for await (const textPart of result.textStream) { console.log(textPart); } ``` ### Example: Generating text with Dedicated Endpoints To use a custom model via a dedicated endpoint, you can use the `friendli` instance with the endpoint id, e.g. `zbimjgovmlcb` ```ts import { friendli } from '@friendliai/ai-provider'; import { generateText } from 'ai'; const { text } = await generateText({ model: friendli('YOUR_ENDPOINT_ID'), prompt: 'What is the meaning of life?', }); console.log(text); ``` You can use the code below to force requests to dedicated endpoints. By default, they are auto-detected. ```ts highlight="5,6,7" import { friendli } from '@friendliai/ai-provider'; import { generateText } from 'ai'; const { text } = await generateText({ model: friendli("YOUR_ENDPOINT_ID", { endpoint: "dedicated", }); prompt: 'What is the meaning of life?', }); console.log(text); ``` FriendliAI language models can also be used in the `streamText`, `generateObject`, and `streamObject` functions. (see [AI SDK Core](/docs/ai-sdk-core)). ### Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | ----------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `deepseek-r1` | <Cross size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `meta-llama-3.3-70b-instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `meta-llama-3.1-8b-instruct` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Note> To access [more models](https://friendli.ai/models), visit the [Friendli Dedicated Endpoints documentation](https://friendli.ai/docs/guides/dedicated_endpoints/quickstart) to deploy your custom models. </Note> ### OpenAI Compatibility You can also use `@ai-sdk/openai` as the APIs are OpenAI-compatible. ```ts import { createOpenAI } from '@ai-sdk/openai'; const friendli = createOpenAI({ baseURL: 'https://api.friendli.ai/serverless/v1', apiKey: process.env.FRIENDLI_TOKEN, }); ``` If you are using dedicated endpoints ```ts import { createOpenAI } from '@ai-sdk/openai'; const friendli = createOpenAI({ baseURL: 'https://api.friendli.ai/dedicated/v1', apiKey: process.env.FRIENDLI_TOKEN, }); ``` --- File: /ai/content/providers/03-community-providers/10-portkey.mdx --- --- title: Portkey description: Learn how to use the Portkey provider for the AI SDK. --- # Portkey Provider [Portkey](https://portkey.ai/?utm_source=vercel&utm_medium=docs&utm_campaign=integration) natively integrates with the AI SDK to make your apps production-ready and reliable. Import Portkey's Vercel package and use it as a provider in your Vercel AI app to enable all of Portkey's features: - Full-stack **observability** and **tracing** for all requests - Interoperability across **250+ LLMs** - Built-in **50+** state-of-the-art guardrails - Simple & semantic **caching** to save costs & time - Conditional request routing with fallbacks, load-balancing, automatic retries, and more - Continuous improvement based on user feedback Learn more at [Portkey docs for the AI SDK](https://docs.portkey.ai/docs/integrations/libraries/vercel) ## Setup The Portkey provider is available in the `@portkey-ai/vercel-provider` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @portkey-ai/vercel-provider" dark /> </Tab> <Tab> <Snippet text="npm install @portkey-ai/vercel-provider" dark /> </Tab> <Tab> <Snippet text="yarn add @portkey-ai/vercel-provider" dark /> </Tab> </Tabs> ## Provider Instance To create a Portkey provider instance, use the `createPortkey` function: ```typescript import { createPortkey } from '@portkey-ai/vercel-provider'; const portkeyConfig = { provider: 'openai', //enter provider of choice api_key: 'OPENAI_API_KEY', //enter the respective provider's api key override_params: { model: 'gpt-4', //choose from 250+ LLMs }, }; const portkey = createPortkey({ apiKey: 'YOUR_PORTKEY_API_KEY', config: portkeyConfig, }); ``` You can find your Portkey API key in the [Portkey Dashboard](https://app.portkey.ai). ## Language Models Portkey supports both chat and completion models. Use `portkey.chatModel()` for chat models and `portkey.completionModel()` for completion models: ```typescript const chatModel = portkey.chatModel(''); const completionModel = portkey.completionModel(''); ``` Note: You can provide an empty string as the model name if you've defined it in the `portkeyConfig`. ## Examples You can use Portkey language models with the `generateText` or `streamText` function: ### `generateText` ```javascript import { createPortkey } from '@portkey-ai/vercel-provider'; import { generateText } from 'ai'; const portkey = createPortkey({ apiKey: 'YOUR_PORTKEY_API_KEY', config: portkeyConfig, }); const { text } = await generateText({ model: portkey.chatModel(''), prompt: 'What is Portkey?', }); console.log(text); ``` ### `streamText` ```javascript import { createPortkey } from '@portkey-ai/vercel-provider'; import { streamText } from 'ai'; const portkey = createPortkey({ apiKey: 'YOUR_PORTKEY_API_KEY', config: portkeyConfig, }); const result = streamText({ model: portkey.completionModel(''), prompt: 'Invent a new holiday and describe its traditions.', }); for await (const chunk of result) { console.log(chunk); } ``` Note: - Portkey supports `Tool` use with the AI SDK - `generatObject` and `streamObject` are currently not supported. ## Advanced Features Portkey offers several advanced features to enhance your AI applications: 1. **Interoperability**: Easily switch between 250+ AI models by changing the provider and model name in your configuration. 2. **Observability**: Access comprehensive analytics and logs for all your requests. 3. **Reliability**: Implement caching, fallbacks, load balancing, and conditional routing. 4. **Guardrails**: Enforce LLM behavior in real-time with input and output checks. 5. **Security and Compliance**: Set budget limits and implement fine-grained user roles and permissions. For detailed information on these features and advanced configuration options, please refer to the [Portkey documentation](https://docs.portkey.ai/docs/integrations/libraries/vercel). ## Additional Resources - [Portkey Documentation](https://docs.portkey.ai/docs/integrations/libraries/vercel) - [Twitter](https://twitter.com/portkeyai) - [Discord Community](https://discord.gg/JHPt4C7r) - [Portkey Dashboard](https://app.portkey.ai) --- File: /ai/content/providers/03-community-providers/100-built-in-ai.mdx --- --- title: Built-in AI description: Learn how to use the Built-in AI provider (browser models) for the AI SDK. --- # Built-in AI [jakobhoeg/built-in-ai](https://github.com/jakobhoeg/built-in-ai) is a community provider that serves as the base AI SDK provider for client side in-browser AI models. It currently provides a model provider for Chrome & Edge's native browser AI models via the JavaScript [Prompt API](https://github.com/webmachinelearning/prompt-api), as well as a model provider for using open-source in-browser models with [WebLLM](https://github.com/mlc-ai/web-llm). > We are also working hard to include other browser-based AI frameworks such as [transformers.js](https://huggingface.co/docs/transformers.js/en/index). <Note type="warning"> The `@built-in-ai/core` package is under constant development as the Prompt API matures, and may contain errors and breaking changes. However, this module will also mature with it as new implementations arise. </Note> ## Setup ### Installation The `@built-in-ai/core` package is the AI SDK provider for Chrome and Edge browser's built-in AI models. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @built-in-ai/core" dark /> </Tab> <Tab> <Snippet text="npm install @built-in-ai/core" dark /> </Tab> <Tab> <Snippet text="yarn add @built-in-ai/core" dark /> </Tab> </Tabs> The `@built-in-ai/web-llm` package is the AI SDK provider for popular open-source models using the WebLLM inference engine. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @built-in-ai/web-llm" dark /> </Tab> <Tab> <Snippet text="npm install @built-in-ai/web-llm" dark /> </Tab> <Tab> <Snippet text="yarn add @built-in-ai/web-llm" dark /> </Tab> </Tabs> ### Browser-specific setup (@built-in-ai/core) <Note type="warning"> The Prompt API (built-in AI) is currently experimental and might change as it matures. The following enablement guide for the API might also change in the future. </Note> 1. You need Chrome (v. 128 or higher) or Edge Dev/Canary (v. 138.0.3309.2 or higher) 2. Enable these experimental flags: - If you're using Chrome: 1. Go to `chrome://flags/`, search for _'Prompt API for Gemini Nano with Multimodal Input'_ and set it to Enabled 2. Go to `chrome://components` and click Check for Update on Optimization Guide On Device Model - If you're using Edge: 1. Go to `edge://flags/#prompt-api-for-phi-mini` and set it to Enabled For more information, check out [this guide](https://developer.chrome.com/docs/extensions/ai/prompt-api) ## Provider Instances ### `@built-in-ai/core` You can import the default provider instance `builtInAI` from `@built-in-ai/core`: ```ts import { builtInAI } from '@built-in-ai/core'; const model = builtInAI(); ``` You can use the following optional settings to customize the model: - **temperature** _number_ Controls randomness in the model's responses. For most models, `0` means almost deterministic results, and higher values mean more randomness. - **topK** _number_ Control the diversity and coherence of generated text by limiting the selection of the next token. ### `@built-in-ai/web-llm` You can import the default provider instance `webLLM` from `@built-in-ai/web-llm`: ```ts import { webLLM } from '@built-in-ai/web-llm'; const model = webLLM(); ``` ## Language Models ### `@built-in-ai/core` The provider will automatically work in all browsers that support the Prompt API since the browser handles model orchestration. For instance, if your client uses Edge, it will use [Phi4-mini](https://learn.microsoft.com/en-us/microsoft-edge/web-platform/prompt-api#the-phi-4-mini-model), and for Chrome it will use [Gemini Nano](https://developer.chrome.com/docs/ai/prompt-api#model_download). ### `@built-in-ai/web-llm` The provider allows using a ton of popular open-source models such as Llama3 and Qwen3. To see a complete list, please refer to the official [WebLLM documentation](https://github.com/mlc-ai/web-llm) ### Example usage #### `@built-in-ai/core` ```ts import { streamText } from 'ai'; import { builtInAI } from '@built-in-ai/core'; const result = streamText({ model: builtInAI(), // will default to the specific browser model prompt: 'Hello, how are you', }); for await (const chunk of result.textStream) { console.log(chunk); } ``` #### `@built-in-ai/web-llm` ```ts import { streamText } from 'ai'; import { webLLM } from '@built-in-ai/web-llm'; const result = streamText({ model: webLLM('Qwen3-0.6B-q0f16-MLC'), prompt: 'Hello, how are you', }); for await (const chunk of result.textStream) { console.log(chunk); } ``` For more examples and API reference, check out the [documentation](https://github.com/jakobhoeg/built-in-ai) --- File: /ai/content/providers/03-community-providers/100-gemini-cli.mdx --- --- title: Gemini CLI description: Learn how to use the Gemini CLI community provider to access Google's Gemini models through the official CLI/SDK. --- # Gemini CLI Provider The [ai-sdk-provider-gemini-cli](https://github.com/ben-vargas/ai-sdk-provider-gemini-cli) community provider enables using Google's Gemini models through the [@google/gemini-cli-core](https://www.npmjs.com/package/@google/gemini-cli-core) library and Google Cloud Code endpoints. While it works with both Gemini Code Assist (GCA) licenses and API key authentication, it's particularly useful for developers who want to use their existing GCA subscription rather than paid use API keys. ## Version Compatibility The Gemini CLI provider supports both AI SDK v4 and v5-beta: | Provider Version | AI SDK Version | Status | Branch | | ---------------- | -------------- | ------ | -------------------------------------------------------------------------------------- | | 0.x | v4 | Stable | [`ai-sdk-v4`](https://github.com/ben-vargas/ai-sdk-provider-gemini-cli/tree/ai-sdk-v4) | | 1.x-beta | v5-beta | Beta | [`main`](https://github.com/ben-vargas/ai-sdk-provider-gemini-cli/tree/main) | ## Setup The Gemini CLI provider is available in the `ai-sdk-provider-gemini-cli` module. Install the version that matches your AI SDK version: ### For AI SDK v5-beta (latest) <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add ai-sdk-provider-gemini-cli ai" dark /> </Tab> <Tab> <Snippet text="npm install ai-sdk-provider-gemini-cli ai" dark /> </Tab> <Tab> <Snippet text="yarn add ai-sdk-provider-gemini-cli ai" dark /> </Tab> </Tabs> ### For AI SDK v4 (stable) <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add ai-sdk-provider-gemini-cli@^0 ai@^4" dark /> </Tab> <Tab> <Snippet text="npm install ai-sdk-provider-gemini-cli@^0 ai@^4" dark /> </Tab> <Tab> <Snippet text="yarn add ai-sdk-provider-gemini-cli@^0 ai@^4" dark /> </Tab> </Tabs> ## Provider Instance You can import `createGeminiProvider` from `ai-sdk-provider-gemini-cli` and create a provider instance with your settings: ```ts import { createGeminiProvider } from 'ai-sdk-provider-gemini-cli'; // OAuth authentication (recommended) const gemini = createGeminiProvider({ authType: 'oauth-personal', }); // API key authentication const gemini = createGeminiProvider({ authType: 'api-key', apiKey: process.env.GEMINI_API_KEY, }); ``` You can use the following settings to customize the Gemini CLI provider instance: - **authType** _'oauth-personal' | 'api-key' | 'gemini-api-key'_ Required. The authentication method to use. - `'oauth-personal'`: Uses existing Gemini CLI credentials from `~/.gemini/oauth_creds.json` - `'api-key'`: Standard AI SDK API key authentication (recommended) - `'gemini-api-key'`: Gemini-specific API key authentication (identical to `'api-key'`) Note: `'api-key'` and `'gemini-api-key'` are functionally identical. We recommend using `'api-key'` for consistency with AI SDK standards, but both options map to the same Gemini authentication method internally. - **apiKey** _string_ Required when using API key authentication. Your Gemini API key from [Google AI Studio](https://aistudio.google.com/apikey). ## Language Models You can create models that call Gemini through the CLI using the provider instance. The first argument is the model ID: ```ts const model = gemini('gemini-2.5-pro'); ``` Gemini CLI supports the following models: - **gemini-2.5-pro**: Most capable model for complex tasks (64K output tokens) - **gemini-2.5-flash**: Faster model for simpler tasks (64K output tokens) ### Example: Generate Text You can use Gemini CLI language models to generate text with the `generateText` function: ```ts import { createGeminiProvider } from 'ai-sdk-provider-gemini-cli'; import { generateText } from 'ai'; const gemini = createGeminiProvider({ authType: 'oauth-personal', }); // AI SDK v4 const { text } = await generateText({ model: gemini('gemini-2.5-pro'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); // AI SDK v5-beta const result = await generateText({ model: gemini('gemini-2.5-pro'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); console.log(result.content[0].text); ``` Gemini CLI language models can also be used in the `streamText`, `generateObject`, and `streamObject` functions (see [AI SDK Core](/docs/ai-sdk-core) for more information). <Note> The response format differs between AI SDK v4 and v5-beta. In v4, text is accessed directly via `result.text`. In v5-beta, it's accessed via `result.content[0].text`. Make sure to use the appropriate format for your AI SDK version. </Note> ### Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | ------------------ | ------------------- | ------------------- | ------------------- | ------------------- | | `gemini-2.5-pro` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `gemini-2.5-flash` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Note> Images must be provided as base64-encoded data. Image URLs are not supported due to the Google Cloud Code endpoint requirements. </Note> ## Authentication The Gemini CLI provider supports two authentication methods: ### OAuth Authentication (Recommended) First, install and authenticate the Gemini CLI globally: ```bash npm install -g @google/gemini-cli gemini # Follow the interactive authentication setup ``` Then use OAuth authentication in your code: ```ts const gemini = createGeminiProvider({ authType: 'oauth-personal', }); ``` This uses your existing Gemini CLI credentials from `~/.gemini/oauth_creds.json`. ### API Key Authentication 1. Generate an API key from [Google AI Studio](https://aistudio.google.com/apikey). 2. Set it as an environment variable in your terminal: ```bash export GEMINI_API_KEY="YOUR_API_KEY" ``` Replace `YOUR_API_KEY` with your generated key. 3. Use API key authentication in your code: ```ts const gemini = createGeminiProvider({ authType: 'api-key', apiKey: process.env.GEMINI_API_KEY, }); ``` <Note> The Gemini API provides a free tier with 100 requests per day using Gemini 2.5 Pro. You can upgrade to a paid plan for higher rate limits on the [API key page](https://aistudio.google.com/apikey). </Note> ## Features ### Structured Object Generation Generate structured data using Zod schemas: ```ts import { generateObject } from 'ai'; import { createGeminiProvider } from 'ai-sdk-provider-gemini-cli'; import { z } from 'zod'; const gemini = createGeminiProvider({ authType: 'oauth-personal', }); const result = await generateObject({ model: gemini('gemini-2.5-pro'), schema: z.object({ name: z.string().describe('Product name'), price: z.number().describe('Price in USD'), features: z.array(z.string()).describe('Key features'), }), prompt: 'Generate a laptop product listing', }); console.log(result.object); ``` ### Streaming Responses Stream text for real-time output: ```ts import { streamText } from 'ai'; import { createGeminiProvider } from 'ai-sdk-provider-gemini-cli'; const gemini = createGeminiProvider({ authType: 'oauth-personal', }); const result = await streamText({ model: gemini('gemini-2.5-pro'), prompt: 'Write a story about a robot learning to paint', }); // Both v4 and v5 use the same streaming API for await (const chunk of result.textStream) { process.stdout.write(chunk); } ``` For more examples and features, including tool usage and multimodal input, see the [provider documentation](https://github.com/ben-vargas/ai-sdk-provider-gemini-cli). ## Model Parameters You can configure model behavior with standard AI SDK parameters: ```ts // AI SDK v4 const model = gemini('gemini-2.5-pro', { temperature: 0.7, // Controls randomness (0-2) maxTokens: 1000, // Maximum output tokens (defaults to 65536) topP: 0.95, // Nucleus sampling threshold }); // AI SDK v5-beta const model = gemini('gemini-2.5-pro', { temperature: 0.7, // Controls randomness (0-2) maxOutputTokens: 1000, // Maximum output tokens (defaults to 65536) topP: 0.95, // Nucleus sampling threshold }); ``` <Note> In AI SDK v5-beta, the `maxTokens` parameter has been renamed to `maxOutputTokens`. Make sure to use the correct parameter name for your version. </Note> ## Limitations - Requires Node.js ≥ 18 - OAuth authentication requires the Gemini CLI to be installed globally - Image URLs not supported (use base64-encoded images) - Very strict character length constraints in schemas may be challenging - Some AI SDK parameters not supported: `frequencyPenalty`, `presencePenalty`, `seed` - Only function tools supported (no provider-defined tools) ## Requirements - Node.js 18 or higher - Gemini CLI installed globally for OAuth authentication (`npm install -g @google/gemini-cli`) - Valid Google account or Gemini API key --- File: /ai/content/providers/03-community-providers/11-cloudflare-workers-ai.mdx --- --- title: Cloudflare Workers AI description: Learn how to use the Cloudflare Workers AI provider for the AI SDK. --- # Cloudflare Workers AI [workers-ai-provider](https://github.com/cloudflare/ai/tree/main/packages/workers-ai-provider) is a community provider that allows you to use Cloudflare's [Workers AI](https://ai.cloudflare.com/) models with the AI SDK. ## Setup The Cloudflare Workers AI provider is available in the `workers-ai-provider` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add workers-ai-provider" /> </Tab> <Tab> <Snippet text="npm install workers-ai-provider" /> </Tab> <Tab> <Snippet text="yarn add workers-ai-provider" /> </Tab> </Tabs> Then, setup an AI binding in your Cloudflare Workers project `wrangler.toml` file: ```bash filename="wrangler.toml" [ai] binding = "AI" ``` ## Provider Instance To create a `workersai` provider instance, use the `createWorkersAI` function, passing in the AI binding as an option: ```typescript import { createWorkersAI } from 'workers-ai-provider'; const workersai = createWorkersAI({ binding: env.AI }); ``` ## Language Models To create a model instance, call the provider instance and specify the model you would like to use as the first argument. You can also pass additional settings in the second argument: ```typescript highlight="4-7" import { createWorkersAI } from 'workers-ai-provider'; const workersai = createWorkersAI({ binding: env.AI }); const model = workersai('@cf/meta/llama-3.1-8b-instruct', { // additional settings safePrompt: true, }); ``` You can use the following optional settings to customize: - **safePrompt** _boolean_ Whether to inject a safety prompt before all conversations. Defaults to `false` ### Examples You can use Cloudflare Workers AI language models to generate text with the **`generateText`** or **`streamText`** function: #### `generateText` ```typescript import { createWorkersAI } from 'workers-ai-provider'; import { generateText } from 'ai'; type Env = { AI: Ai; }; export default { async fetch(_: Request, env: Env) { const workersai = createWorkersAI({ binding: env.AI }); const result = await generateText({ model: workersai('@cf/meta/llama-2-7b-chat-int8'), prompt: 'Write a 50-word essay about hello world.', }); return new Response(result.text); }, }; ``` #### `streamText` ```typescript import { createWorkersAI } from 'workers-ai-provider'; import { streamText } from 'ai'; type Env = { AI: Ai; }; export default { async fetch(_: Request, env: Env) { const workersai = createWorkersAI({ binding: env.AI }); const result = streamText({ model: workersai('@cf/meta/llama-2-7b-chat-int8'), prompt: 'Write a 50-word essay about hello world.', }); return result.toTextStreamResponse({ headers: { // add these headers to ensure that the // response is chunked and streamed 'Content-Type': 'text/x-unknown', 'content-encoding': 'identity', 'transfer-encoding': 'chunked', }, }); }, }; ``` #### `generateObject` Some Cloudflare Workers AI language models can also be used with the `generateObject` function: ```typescript import { createWorkersAI } from 'workers-ai-provider'; import { generateObject } from 'ai'; import { z } from 'zod'; type Env = { AI: Ai; }; export default { async fetch(_: Request, env: Env) { const workersai = createWorkersAI({ binding: env.AI }); const result = await generateObject({ model: workersai('@cf/meta/llama-3.1-8b-instruct'), prompt: 'Generate a Lasagna recipe', schema: z.object({ recipe: z.object({ ingredients: z.array(z.string()), description: z.string(), }), }), }); return Response.json(result.object); }, }; ``` --- File: /ai/content/providers/03-community-providers/12-cloudflare-ai-gateway.mdx --- --- title: Cloudflare AI Gateway description: Learn how to use the Cloudflare AI Gateway provider for the AI SDK. --- # AI Gateway Provider The AI Gateway Provider is a library that integrates Cloudflare's AI Gateway with the Vercel AI SDK. It enables seamless access to multiple AI models from various providers through a unified interface, with automatic fallback for high availability. ## Features - **Runtime Agnostic**: Compatible with Node.js, Edge Runtime, and other JavaScript runtimes supported by the Vercel AI SDK. - **Automatic Fallback**: Automatically switches to the next available model if one fails, ensuring resilience. - **Multi-Provider Support**: Supports models from OpenAI, Anthropic, DeepSeek, Google AI Studio, Grok, Mistral, Perplexity AI, Replicate, and Groq. - **AI Gateway Integration**: Leverages Cloudflare's AI Gateway for request management, caching, and rate limiting. - **Simplified Configuration**: Easy setup with support for API key authentication or Cloudflare AI bindings. ## Setup The AI Gateway Provider is available in the `ai-gateway-provider` module. Install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add ai-gateway-provider" /> </Tab> <Tab> <Snippet text="npm install ai-gateway-provider" /> </Tab> <Tab> <Snippet text="yarn add ai-gateway-provider" /> </Tab> </Tabs> ## Provider Instance Create an `aigateway` provider instance using the `createAiGateway` function. You can authenticate using an API key or a Cloudflare AI binding. ### API Key Authentication ```typescript import { createAiGateway } from 'ai-gateway-provider'; const aigateway = createAiGateway({ accountId: 'your-cloudflare-account-id', gateway: 'your-gateway-name', apiKey: 'your-cloudflare-api-key', // Only required if your gateway has authentication enabled options: { skipCache: true, // Optional request-level settings }, }); ``` ### Cloudflare AI Binding This method is only available inside Cloudflare Workers. Configure an AI binding in your `wrangler.toml`: ```bash [AI] binding = "AI" ``` In your worker, create a new instance using the binding: ```typescript import { createAiGateway } from 'ai-gateway-provider'; const aigateway = createAiGateway({ binding: env.AI.gateway('my-gateway'), options: { skipCache: true, // Optional request-level settings }, }); ``` ## Language Models Create a model instance by passing an array of models to the `aigateway` provider. The provider will attempt to use the models in order, falling back to the next if one fails. ```typescript import { createAiGateway } from 'ai-gateway-provider'; import { createOpenAI } from '@ai-sdk/openai'; import { createAnthropic } from '@ai-sdk/anthropic'; const aigateway = createAiGateway({ accountId: 'your-cloudflare-account-id', gateway: 'your-gateway-name', apiKey: 'your-cloudflare-api-key', }); const openai = createOpenAI({ apiKey: 'openai-api-key' }); const anthropic = createAnthropic({ apiKey: 'anthropic-api-key' }); const model = aigateway([ anthropic('claude-3-5-haiku-20241022'), // Primary model openai('gpt-4o-mini'), // Fallback model ]); ``` ### Request Options Customize AI Gateway settings per request: - `cacheKey`: Custom cache key for the request. - `cacheTtl`: Cache time-to-live in seconds. - `skipCache`: Bypass caching. - `metadata`: Custom metadata for the request. - `collectLog`: Enable/disable log collection. - `eventId`: Custom event identifier. - `requestTimeoutMs`: Request timeout in milliseconds. - `retries`: - `maxAttempts`: Number of retry attempts (1-5). - `retryDelayMs`: Delay between retries. - `backoff`: Retry strategy (`constant`, `linear`, `exponential`). Example: ```typescript const aigateway = createAiGateway({ accountId: 'your-cloudflare-account-id', gateway: 'your-gateway-name', apiKey: 'your-cloudflare-api-key', options: { cacheTtl: 3600, // Cache for 1 hour metadata: { userId: 'user123' }, retries: { maxAttempts: 3, retryDelayMs: 1000, backoff: 'exponential', }, }, }); ``` ## Examples ### `generateText` Generate non-streaming text using the AI Gateway Provider: ```typescript import { createAiGateway } from 'ai-gateway-provider'; import { createOpenAI } from '@ai-sdk/openai'; import { generateText } from 'ai'; const aigateway = createAiGateway({ accountId: 'your-cloudflare-account-id', gateway: 'your-gateway-name', apiKey: 'your-cloudflare-api-key', }); const openai = createOpenAI({ apiKey: 'openai-api-key' }); const { text } = await generateText({ model: aigateway([openai('gpt-4o-mini')]), prompt: 'Write a greeting.', }); console.log(text); // Output: "Hello" ``` ### `streamText` Stream text responses using the AI Gateway Provider: ```typescript import { createAiGateway } from 'ai-gateway-provider'; import { createOpenAI } from '@ai-sdk/openai'; import { streamText } from 'ai'; const aigateway = createAiGateway({ accountId: 'your-cloudflare-account-id', gateway: 'your-gateway-name', apiKey: 'your-cloudflare-api-key', }); const openai = createOpenAI({ apiKey: 'openai-api-key' }); const result = await streamText({ model: aigateway([openai('gpt-4o-mini')]), prompt: 'Write a multi-part greeting.', }); let accumulatedText = ''; for await (const chunk of result.textStream) { accumulatedText += chunk; } console.log(accumulatedText); // Output: "Hello world!" ``` ## Supported Providers - OpenAI - Anthropic - DeepSeek - Google AI Studio - Grok - Mistral - Perplexity AI - Replicate - Groq ## Error Handling The provider throws the following custom errors: - `AiGatewayUnauthorizedError`: Invalid or missing API key when authentication is enabled. - `AiGatewayDoesNotExist`: Specified AI Gateway does not exist. --- File: /ai/content/providers/03-community-providers/13-openrouter.mdx --- --- title: OpenRouter description: OpenRouter Provider for the AI SDK --- # OpenRouter [OpenRouter](https://openrouter.ai/) is a unified API gateway that provides access to hundreds of AI models from leading providers like Anthropic, Google, Meta, Mistral, and more. The OpenRouter provider for the AI SDK enables seamless integration with all these models while offering unique advantages: - **Universal Model Access**: One API key for hundreds of models from multiple providers - **Cost-Effective**: Pay-as-you-go pricing with no monthly fees or commitments - **Transparent Pricing**: Clear per-token costs for all models - **High Availability**: Enterprise-grade infrastructure with automatic failover - **Simple Integration**: Standardized API across all models - **Latest Models**: Immediate access to new models as they're released Learn more about OpenRouter's capabilities in the [OpenRouter Documentation](https://openrouter.ai/docs). ## Setup The OpenRouter provider is available in the `@openrouter/ai-sdk-provider` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @openrouter/ai-sdk-provider" dark /> </Tab> <Tab> <Snippet text="npm install @openrouter/ai-sdk-provider" dark /> </Tab> <Tab> <Snippet text="yarn add @openrouter/ai-sdk-provider" dark /> </Tab> </Tabs> ## Provider Instance To create an OpenRouter provider instance, use the `createOpenRouter` function: ```typescript import { createOpenRouter } from '@openrouter/ai-sdk-provider'; const openrouter = createOpenRouter({ apiKey: 'YOUR_OPENROUTER_API_KEY', }); ``` You can obtain your OpenRouter API key from the [OpenRouter Dashboard](https://openrouter.ai/keys). ## Language Models OpenRouter supports both chat and completion models. Use `openrouter.chat()` for chat models and `openrouter.completion()` for completion models: ```typescript // Chat models (recommended) const chatModel = openrouter.chat('anthropic/claude-3.5-sonnet'); // Completion models const completionModel = openrouter.completion( 'meta-llama/llama-3.1-405b-instruct', ); ``` You can find the full list of available models in the [OpenRouter Models documentation](https://openrouter.ai/docs#models). ## Examples Here are examples of using OpenRouter with the AI SDK: ### `generateText` ```javascript import { createOpenRouter } from '@openrouter/ai-sdk-provider'; import { generateText } from 'ai'; const openrouter = createOpenRouter({ apiKey: 'YOUR_OPENROUTER_API_KEY', }); const { text } = await generateText({ model: openrouter.chat('anthropic/claude-3.5-sonnet'), prompt: 'What is OpenRouter?', }); console.log(text); ``` ### `streamText` ```javascript import { createOpenRouter } from '@openrouter/ai-sdk-provider'; import { streamText } from 'ai'; const openrouter = createOpenRouter({ apiKey: 'YOUR_OPENROUTER_API_KEY', }); const result = streamText({ model: openrouter.chat('meta-llama/llama-3.1-405b-instruct'), prompt: 'Write a short story about AI.', }); for await (const chunk of result) { console.log(chunk); } ``` ## Advanced Features OpenRouter offers several advanced features to enhance your AI applications: 1. **Model Flexibility**: Switch between hundreds of models without changing your code or managing multiple API keys. 2. **Cost Management**: Track usage and costs per model in real-time through the dashboard. 3. **Enterprise Support**: Available for high-volume users with custom SLAs and dedicated support. 4. **Cross-Provider Compatibility**: Use the same code structure across different model providers. 5. **Regular Updates**: Automatic access to new models and features as they become available. For more information about these features and advanced configuration options, visit the [OpenRouter Documentation](https://openrouter.ai/docs). ## Additional Resources - [OpenRouter Provider Repository](https://github.com/OpenRouterTeam/ai-sdk-provider) - [OpenRouter Documentation](https://openrouter.ai/docs) - [OpenRouter Dashboard](https://openrouter.ai/dashboard) - [OpenRouter Discord Community](https://discord.gg/openrouter) - [OpenRouter Status Page](https://status.openrouter.ai) --- File: /ai/content/providers/03-community-providers/14-azure-ai.mdx --- --- title: Azure AI description: Learn how to use the @quail-ai/azure-ai-provider for the AI SDK. --- # Azure Custom Provider for AI SDK <Note type="warning"> This community provider is not yet compatible with AI SDK 5. Please wait for the provider to be updated or consider using an [AI SDK 5 compatible provider](/providers/ai-sdk-providers). </Note> The **[Quail-AI/azure-ai-provider](https://github.com/QuailAI/azure-ai-provider)** enables unofficial integration with Azure-hosted language models that use Azure's native APIs instead of the standard OpenAI API format. ## Language Models This provider works with any model in the Azure AI Foundry that is compatible with the Azure-Rest AI-inference API. **Note:** This provider is not compatible with the Azure OpenAI models. For those, please use the [Azure OpenAI Provider](/providers/ai-sdk-providers/azure). ### Models Tested: - DeepSeek-R1 - LLama 3.3-70B Instruct - Cohere-command-r-08-2024 ## Setup ### Installation Install the provider via npm: ```bash npm i @quail-ai/azure-ai-provider ``` ## Provider Instance Create an Azure AI resource and set up your endpoint URL and API key. Add the following to your `.env` file: ```bash AZURE_API_ENDPOINT=https://<your-resource>.services.ai.azure.com/models AZURE_API_KEY=<your-api-key> ``` Import `createAzure` from the package to create your provider instance: ```ts import { createAzure } from '@quail-ai/azure-ai-provider'; const azure = createAzure({ endpoint: process.env.AZURE_API_ENDPOINT, apiKey: process.env.AZURE_API_KEY, }); ``` ## Basic Usage Generate text using the Azure custom provider: ```ts import { generateText } from 'ai'; const { text } = await generateText({ model: azure('your-deployment-name'), prompt: 'Write a story about a robot.', }); ``` ## Status > ✅ Chat Completions: Working with both streaming and non-streaming responses\ > ⚠️ Tool Calling: Functionality highly dependent on model choice\ > ⚠️ Embeddings: Implementation present but untested --- File: /ai/content/providers/03-community-providers/20-sap-ai.mdx --- --- title: SAP AI Core description: SAP AI Core Provider for the AI SDK --- # SAP AI Core ## Important Note > **Third-Party Provider**: This SAP AI Core provider (`@mymediset/sap-ai-provider`) is developed and maintained by Mymediset, not by SAP SE. While it integrates with official SAP AI Core services, it is not an official SAP product. For official SAP AI solutions, please refer to the [SAP AI Core Documentation](https://help.sap.com/docs/ai-core). [SAP AI Core](https://help.sap.com/docs/ai-core) is SAP's enterprise-grade AI platform that provides access to leading AI models from OpenAI, Anthropic, Google, Amazon, and more through a unified, secure, and scalable infrastructure. The SAP AI Core provider for the AI SDK enables seamless integration with enterprise AI capabilities while offering unique advantages: - **Multi-Model Access**: Support for 40+ models including GPT-4, Claude, Gemini, and Amazon Nova - **OAuth Integration**: Automatic authentication handling with SAP BTP - **Cost Management**: Enterprise billing and usage tracking through SAP BTP - **High Availability**: Enterprise-grade infrastructure with SLA guarantees - **Hybrid Deployment**: Support for both cloud and on-premise deployments - **Tool Calling**: Full function calling capabilities for compatible models - **Multi-modal Support**: Text and image inputs for compatible models Learn more about SAP AI Core's capabilities in the [SAP AI Core Documentation](https://help.sap.com/docs/ai-core). ## Setup The SAP AI Core provider is available in the `@mymediset/sap-ai-provider` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @mymediset/sap-ai-provider" dark /> </Tab> <Tab> <Snippet text="npm install @mymediset/sap-ai-provider" dark /> </Tab> <Tab> <Snippet text="yarn add @mymediset/sap-ai-provider" dark /> </Tab> </Tabs> ## Provider Instance To create an SAP AI Core provider instance, use the `createSAPAIProvider` function: ```typescript import { createSAPAIProvider } from '@mymediset/sap-ai-provider'; const sapai = await createSAPAIProvider({ serviceKey: 'YOUR_SAP_AI_CORE_SERVICE_KEY', }); ``` You can obtain your SAP AI Core service key from your SAP BTP Cockpit by creating a service key for your AI Core instance. ## Language Models You can create SAP AI Core models using the provider instance and model name: ```typescript // OpenAI models const gpt4Model = sapai('gpt-4o'); // Anthropic models const claudeModel = sapai('anthropic--claude-3-sonnet'); // Google models const geminiModel = sapai('gemini-1.5-pro'); // Amazon models const novaModel = sapai('amazon--nova-pro'); ``` ## Supported Models The provider supports a wide range of models available in your SAP AI Core deployment: ### OpenAI Models - `gpt-4`, `gpt-4o`, `gpt-4o-mini` - `o1`, `o1-mini` ### Anthropic Models - `anthropic--claude-3-haiku`, `anthropic--claude-3-sonnet`, `anthropic--claude-3-opus` - `anthropic--claude-3.5-sonnet` ### Google Models - `gemini-1.5-pro`, `gemini-1.5-flash` - `gemini-2.0-pro`, `gemini-2.0-flash` ### Amazon Models - `amazon--nova-premier`, `amazon--nova-pro`, `amazon--nova-lite`, `amazon--nova-micro` ### Other Models - `mistralai--mistral-large-instruct` - `meta--llama3-70b-instruct`, `meta--llama3.1-70b-instruct` Note: Model availability may vary based on your SAP AI Core subscription and region. Some models may require additional configuration or permissions. ## Examples Here are examples of using SAP AI Core with the AI SDK: ### generateText ```typescript import { createSAPAIProvider } from '@mymediset/sap-ai-provider'; import { generateText } from 'ai'; const sapai = await createSAPAIProvider({ serviceKey: process.env.SAP_AI_SERVICE_KEY, }); const { text } = await generateText({ model: sapai('gpt-4o'), prompt: 'What are the benefits of enterprise AI platforms?', }); console.log(text); ``` ### streamText ```typescript import { createSAPAIProvider } from '@mymediset/sap-ai-provider'; import { streamText } from 'ai'; const sapai = await createSAPAIProvider({ serviceKey: process.env.SAP_AI_SERVICE_KEY, }); const result = streamText({ model: sapai('anthropic--claude-3-sonnet'), prompt: 'Write a short story about AI.', }); for await (const textPart of result.textStream) { console.log(textPart); } ``` ### Tool Calling ```typescript import { createSAPAIProvider } from '@mymediset/sap-ai-provider'; import { generateText, tool } from 'ai'; import { z } from 'zod'; const sapai = await createSAPAIProvider({ serviceKey: process.env.SAP_AI_SERVICE_KEY, }); const result = await generateText({ model: sapai('gpt-4o'), prompt: 'What is the current status of our inventory system?', tools: { checkInventory: tool({ description: 'Check current inventory levels', inputSchema: z.object({ item: z.string().describe('Item to check'), location: z.string().describe('Warehouse location'), }), execute: async ({ item, location }) => { // Your inventory system integration return { item, location, quantity: 150, status: 'In Stock' }; }, }), }, }); console.log(result.text); ``` ### Multi-modal Input ```typescript import { createSAPAIProvider } from '@mymediset/sap-ai-provider'; import { generateText } from 'ai'; const sapai = await createSAPAIProvider({ serviceKey: process.env.SAP_AI_SERVICE_KEY, }); const result = await generateText({ model: sapai('gpt-4o'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Analyze this business process diagram.' }, { type: 'image', image: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ...', }, ], }, ], }); console.log(result.text); ``` ## Configuration ### Provider Settings ```typescript interface SAPAIProviderSettings { serviceKey?: string; // SAP AI Core service key JSON token?: string; // Direct access token (alternative to serviceKey) baseURL?: string; // Custom base URL for API calls } ``` ### Model Settings ```typescript interface SAPAIModelSettings { modelParams?: { maxTokens?: number; // Maximum tokens to generate temperature?: number; // Sampling temperature (0-2) topP?: number; // Nucleus sampling parameter frequencyPenalty?: number; // Frequency penalty (-2 to 2) presencePenalty?: number; // Presence penalty (-2 to 2) }; safePrompt?: boolean; // Enable safe prompt filtering structuredOutputs?: boolean; // Enable structured outputs } ``` ## Environment Variables ### Required Your SAP AI Core service key: ```bash SAP_AI_SERVICE_KEY='{"serviceurls":{"AI_API_URL":"..."},"clientid":"...","clientsecret":"..."}' ``` ### Optional Direct access token (alternative to service key): ```bash SAP_AI_TOKEN='your-access-token' ``` Custom base URL: ```bash SAP_AI_BASE_URL='https://api.ai.prod.eu-central-1.aws.ml.hana.ondemand.com' ``` ## Enterprise Features SAP AI Core offers several enterprise-grade features: - **Multi-Tenant Architecture**: Isolated environments for different business units - **Cost Allocation**: Detailed usage tracking and cost center allocation - **Custom Models**: Deploy and manage your own fine-tuned models - **Hybrid Deployment**: Support for both cloud and on-premise installations - **Integration Ready**: Native integration with SAP S/4HANA, SuccessFactors, and other SAP solutions For more information about these features and advanced configuration options, visit the [SAP AI Core Documentation](https://help.sap.com/docs/ai-core). ## Additional Resources - [SAP AI Provider Repository](https://github.com/BITASIA/sap-ai-provider) - [SAP AI Core Documentation](https://help.sap.com/docs/ai-core) - [SAP BTP Documentation](https://help.sap.com/docs/btp) - [SAP Community](https://community.sap.com/t5/c-khhcw49343/SAP+AI+Core/pd-p/73555000100800003283) - [SAP AI Core Service Guide](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide) --- File: /ai/content/providers/03-community-providers/21-crosshatch.mdx --- --- title: Crosshatch description: Learn how to use the Crosshatch provider for the AI SDK. --- # Crosshatch Provider <Note type="warning"> This community provider is not yet compatible with AI SDK 5. Please wait for the provider to be updated or consider using an [AI SDK 5 compatible provider](/providers/ai-sdk-providers). </Note> The [Crosshatch](https://crosshatch.io) provider supports secure inference from popular language models with permissioned access to data users share, giving responses personalized with complete user context. It creates language model objects that can be used with the `generateText`, `streamText`, `generateObject` and `streamObject` functions. ## Setup The Crosshatch provider is available via the `@crosshatch/ai-provider` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @crosshatch/ai-provider" dark /> </Tab> <Tab> <Snippet text="npm install @crosshatch/ai-provider" dark /> </Tab> <Tab> <Snippet text="yarn add @crosshatch/ai-provider" dark /> </Tab> </Tabs> The [Crosshatch](https://crosshatch.io/) provider supports all of their available models such as OpenAI's GPT and Anthropic's Claude. This provider also supports the querying interface for controlling Crosshatch's custom data integration behaviors. This provider wraps the existing underlying providers ([@ai-sdk/openai](/providers/ai-sdk-providers/openai), [@ai-sdk/anthropic](/providers/ai-sdk-providers/openai). ### Credentials The Crosshatch provider is authenticated by user-specific tokens, enabling permissioned access to personalized inference. You can obtain synthetic and test user tokens from the [your Crosshatch developer dashboard](https://platform.crosshatch.io/). Production user tokens are provisioned and accessed with the [Link SDK](https://www.npmjs.com/package/@crosshatch/link) using your Crosshatch developer client id. ## Provider Instance To create a Crosshatch provider instance, use the `createCrosshatch` function: ```ts import createCrosshatch from '@crosshatch/ai-provider'; ``` ## Language Models You can create [Crosshatch models](https://docs.crosshatch.io/endpoints/ai#supported-model-providers) using a provider instance. ```ts import { createCrosshatch } from '@crosshatch/ai-provider'; const crosshatch = createCrosshatch(); ``` To create a model instance, call the provider instance and specify the model you would like to use in the first argument. In the second argument, specify the user auth token, desired context, and model arguments. You can use Crosshatch to get generated text based on permissioned user context and your favorite language model. ### Example: Generate Text with Context This example uses `gpt-4o-mini` to generate text. ```ts import { generateText } from 'ai'; import createCrosshatch from '@crosshatch/ai-provider': const crosshatch = createCrosshatch(); const { text } = await generateText({ model: crosshatch.languageModel("gpt-4o-mini", { token: 'YOUR_ACCESS_TOKEN', replace: { restaurants: { select: ["entity_name", "entity_city", "entity_region"], from: "personalTimeline", where: [ { field: "event", op: "=", value: "confirmed" }, { field: "entity_subtype2", op: "=", value: "RESTAURANTS" } ], groupby: ["entity_name", "entity_city", "entity_region"], orderby: "count DESC", limit: 5 } } }), system: `The user recently ate at these restaurants: {restaurants}`, messages: [{role: "user", content: "Where should I stay in Paris?"}] }); ``` ### Example: Recommend Items based on Context Use crosshatch to re-rank items based on recent user purchases. ```ts import { streamObject } from 'ai'; import createCrosshatch from `@crosshatch/ai-provider` const crosshatch = createCrosshatch(); const itemSummaries = [...]; // list of items const ids = (itemSummaries?.map(({ itemId }) => itemId) ?? []) as string[]; const { elementStream } = streamObject({ output: "array", mode: "json", model: crosshatch.languageModel("gpt-4o-mini", { token, replace: { "orders": { select: ["originalTimestamp", "entity_name", "order_total", "order_summary"], from: "personalTimeline", where: [{ field: "event", op: "=", value: "purchased" }], orderBy: [{ field: "originalTimestamp", dir: "desc" }], limit: 5, }, }, }), system: `Rerank the following items based on alignment with users recent purchases {orders}`, messages: [{role: "user", content: "Heres a list of item: ${JSON.stringify(itemSummaries)"},], schema: jsonSchema<{ id: string; reason: string }>({ type: "object", properties: { id: { type: "string", enum: ids }, reason: { type: "string", description: "Explain your ranking." }, }, }), }) ``` --- File: /ai/content/providers/03-community-providers/5-requesty.mdx --- --- title: Requesty description: Requesty Provider for the AI SDK --- # Requesty [Requesty](https://requesty.ai/) is a unified LLM gateway that provides access to over 300 large language models from leading providers like OpenAI, Anthropic, Google, Mistral, AWS, and more. The Requesty provider for the AI SDK enables seamless integration with all these models while offering enterprise-grade advantages: - **Universal Model Access**: One API key for 300+ models from multiple providers - **99.99% Uptime SLA**: Enterprise-grade infrastructure with intelligent failover and load balancing - **Cost Optimization**: Pay-as-you-go pricing with intelligent routing and prompt caching to reduce costs by up to 80% - **Advanced Security**: Prompt injection detection, end-to-end encryption, and GDPR compliance - **Real-time Observability**: Built-in monitoring, tracing, and analytics - **Intelligent Routing**: Automatic failover and performance-based routing - **Reasoning Support**: Advanced reasoning capabilities with configurable effort levels Learn more about Requesty's capabilities in the [Requesty Documentation](https://docs.requesty.ai). ## Setup The Requesty provider is available in the `@requesty/ai-sdk` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @requesty/ai-sdk" dark /> </Tab> <Tab> <Snippet text="npm install @requesty/ai-sdk" dark /> </Tab> <Tab> <Snippet text="yarn add @requesty/ai-sdk" dark /> </Tab> </Tabs> ## API Key Setup For security, you should set your API key as an environment variable named exactly `REQUESTY_API_KEY`: ```bash # Linux/Mac export REQUESTY_API_KEY=your_api_key_here # Windows Command Prompt set REQUESTY_API_KEY=your_api_key_here # Windows PowerShell $env:REQUESTY_API_KEY="your_api_key_here" ``` You can obtain your Requesty API key from the [Requesty Dashboard](https://app.requesty.ai/api-keys). ## Provider Instance You can import the default provider instance `requesty` from `@requesty/ai-sdk`: ```typescript import { requesty } from '@requesty/ai-sdk'; ``` Alternatively, you can create a custom provider instance using `createRequesty`: ```typescript import { createRequesty } from '@requesty/ai-sdk'; const customRequesty = createRequesty({ apiKey: 'YOUR_REQUESTY_API_KEY', }); ``` ## Language Models Requesty supports both chat and completion models with a simple, unified interface: ```typescript // Using the default provider instance const model = requesty('openai/gpt-4o'); // Using a custom provider instance const customModel = customRequesty('anthropic/claude-3.5-sonnet'); ``` You can find the full list of available models in the [Requesty Models documentation](https://requesty.ai/models). ## Examples Here are examples of using Requesty with the AI SDK: ### `generateText` ```javascript import { requesty } from '@requesty/ai-sdk'; import { generateText } from 'ai'; const { text } = await generateText({ model: requesty('openai/gpt-4o'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); console.log(text); ``` ### `streamText` ```javascript import { requesty } from '@requesty/ai-sdk'; import { streamText } from 'ai'; const result = streamText({ model: requesty('anthropic/claude-3.5-sonnet'), prompt: 'Write a short story about AI.', }); for await (const chunk of result.textStream) { console.log(chunk); } ``` ### Tool Usage ```javascript import { requesty } from '@requesty/ai-sdk'; import { generateObject } from 'ai'; import { z } from 'zod'; const { object } = await generateObject({ model: requesty('openai/gpt-4o'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.string()), steps: z.array(z.string()), }), }), prompt: 'Generate a recipe for chocolate chip cookies.', }); console.log(object.recipe); ``` ## Advanced Features ### Reasoning Support Requesty provides advanced reasoning capabilities with configurable effort levels for supported models: ```javascript import { createRequesty } from '@requesty/ai-sdk'; import { generateText } from 'ai'; const requesty = createRequesty({ apiKey: process.env.REQUESTY_API_KEY }); // Using reasoning effort const { text, reasoning } = await generateText({ model: requesty('openai/o3-mini', { reasoningEffort: 'medium', }), prompt: 'Solve this complex problem step by step...', }); console.log('Response:', text); console.log('Reasoning:', reasoning); ``` #### Reasoning Effort Values - `'low'` - Minimal reasoning effort - `'medium'` - Moderate reasoning effort - `'high'` - High reasoning effort - `'max'` - Maximum reasoning effort (Requesty-specific) - Budget strings (e.g., `"10000"`) - Specific token budget for reasoning #### Supported Reasoning Models - **OpenAI**: `openai/o3-mini`, `openai/o3` - **Anthropic**: `anthropic/claude-sonnet-4-0`, other Claude reasoning models - **Deepseek**: All Deepseek reasoning models (automatic reasoning) ### Custom Configuration Configure Requesty with custom settings: ```javascript import { createRequesty } from '@requesty/ai-sdk'; const requesty = createRequesty({ apiKey: process.env.REQUESTY_API_KEY, baseURL: 'https://router.requesty.ai/v1', headers: { 'Custom-Header': 'custom-value', }, extraBody: { custom_field: 'value', }, }); ``` ### Passing Extra Body Parameters There are three ways to pass extra body parameters to Requesty: #### 1. Via Provider Options ```javascript await streamText({ model: requesty('anthropic/claude-3.5-sonnet'), messages: [{ role: 'user', content: 'Hello' }], providerOptions: { requesty: { custom_field: 'value', reasoning_effort: 'high', }, }, }); ``` #### 2. Via Model Settings ```javascript const model = requesty('anthropic/claude-3.5-sonnet', { extraBody: { custom_field: 'value', }, }); ``` #### 3. Via Provider Factory ```javascript const requesty = createRequesty({ apiKey: process.env.REQUESTY_API_KEY, extraBody: { custom_field: 'value', }, }); ``` ## Enterprise Features Requesty offers several enterprise-grade features: 1. **99.99% Uptime SLA**: Advanced routing and failover mechanisms keep your AI application online when other services fail. 2. **Intelligent Load Balancing**: Real-time performance-based routing automatically selects the best-performing providers. 3. **Cost Optimization**: Intelligent routing can reduce API costs by up to 40% while maintaining response quality. 4. **Advanced Security**: Built-in prompt injection detection, end-to-end encryption, and GDPR compliance. 5. **Real-time Observability**: Comprehensive monitoring, tracing, and analytics for all requests. 6. **Geographic Restrictions**: Comply with regional regulations through configurable geographic controls. 7. **Model Access Control**: Fine-grained control over which models and providers can be accessed. ## Key Benefits - **Zero Downtime**: Automatic failover with \<50ms switching time - **Multi-Provider Redundancy**: Seamless switching between healthy providers - **Intelligent Queuing**: Retry logic with exponential backoff - **Developer-Friendly**: Straightforward setup with unified API - **Flexibility**: Switch between models and providers without code changes - **Enterprise Support**: Available for high-volume users with custom SLAs ## Additional Resources - [Requesty Provider Repository](https://github.com/requestyai/ai-sdk-requesty) - [Requesty Documentation](https://docs.requesty.ai/) - [Requesty Dashboard](https://app.requesty.ai/analytics) - [Requesty Discord Community](https://discord.com/invite/Td3rwAHgt4) - [Requesty Status Page](https://status.requesty.ai) --- File: /ai/content/providers/03-community-providers/60-mixedbread.mdx --- --- title: Mixedbread description: Learn how to use the Mixedbread provider. --- # Mixedbread Provider <Note type="warning"> This community provider is not yet compatible with AI SDK 5. It uses the deprecated `.embedding()` method instead of the standard `.textEmbeddingModel()` method. Please wait for the provider to be updated or consider using an [AI SDK 5 compatible provider](/providers/ai-sdk-providers). </Note> [patelvivekdev/mixedbread-ai-provider](https://github.com/patelvivekdev/mixedbread-ai-provider) is a community provider that uses [Mixedbread](https://www.mixedbread.ai/) to provide Embedding support for the AI SDK. ## Setup The Mixedbread provider is available in the `mixedbread-ai-provider` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add mixedbread-ai-provider" dark /> </Tab> <Tab> <Snippet text="npm install mixedbread-ai-provider" dark /> </Tab> <Tab> <Snippet text="yarn add mixedbread-ai-provider" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `mixedbread ` from `mixedbread-ai-provider`: ```ts import { mixedbread } from 'mixedbread-ai-provider'; ``` If you need a customized setup, you can import `createMixedbread` from `mixedbread-ai-provider` and create a provider instance with your settings: ```ts import { createMixedbread } from 'mixedbread-ai-provider'; const mixedbread = createMixedbread({ baseURL: 'https://api.mixedbread.ai/v1', apiKey: process.env.MIXEDBREAD_API_KEY, }); ``` You can use the following optional settings to customize the Mixedbread provider instance: - **baseURL** _string_ The base URL of the Mixedbread API - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. ## Embedding Models You can create models that call the [Mixedbread embeddings API](https://www.mixedbread.ai/api-reference/endpoints/embeddings) using the `.embedding()` factory method. ```ts import { mixedbread } from 'mixedbread-ai-provider'; const embeddingModel = mixedbread.embedding( 'mixedbread-ai/mxbai-embed-large-v1', ); ``` ### Model Capabilities | Model | Default Dimensions | Context Length | Custom Dimensions | | --------------------------------- | ------------------ | -------------- | ------------------- | | `mxbai-embed-large-v1` | 1024 | 512 | <Check size={18} /> | | `deepset-mxbai-embed-de-large-v1` | 1024 | 512 | <Check size={18} /> | <Note> The table above lists popular models. Please see the [Mixedbread docs](https://www.mixedbread.ai/docs/embeddings/models) for a full list of available models. </Note> ### Add settings to the model The settings object should contain the settings you want to add to the model. ```ts import { mixedbread } from 'mixedbread-ai-provider'; const embeddingModel = mixedbread.embedding( 'mixedbread-ai/mxbai-embed-large-v1', { prompt: 'Generate embeddings for text', // Max 256 characters dimensions: 512, // Max 1024 for embed-large-v1 }, ); ``` --- File: /ai/content/providers/03-community-providers/61-voyage-ai.mdx --- --- title: Voyage AI description: Learn how to use the Voyage AI provider. --- # Voyage AI Provider <Note type="warning"> This community provider is not yet compatible with AI SDK 5. It uses the deprecated `.embedding()` method instead of the standard `.textEmbeddingModel()` method. Please wait for the provider to be updated or consider using an [AI SDK 5 compatible provider](/providers/ai-sdk-providers). </Note> [patelvivekdev/voyage-ai-provider](https://github.com/patelvivekdev/voyageai-ai-provider) is a community provider that uses [Voyage AI](https://www.voyageai.com) to provide Embedding support for the AI SDK. ## Setup The Voyage provider is available in the `voyage-ai-provider` module. You can install it with <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add voyage-ai-provider" dark /> </Tab> <Tab> <Snippet text="npm install voyage-ai-provider" dark /> </Tab> <Tab> <Snippet text="yarn add voyage-ai-provider" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `voyage ` from `voyage-ai-provider`: ```ts import { voyage } from 'voyage-ai-provider'; ``` If you need a customized setup, you can import `createVoyage` from `voyage-ai-provider` and create a provider instance with your settings: ```ts import { createVoyage } from 'voyage-ai-provider'; const voyage = createVoyage({ baseURL: 'https://api.voyageai.com/v1', apiKey: process.env.VOYAGE_API_KEY, }); ``` You can use the following optional settings to customize the Voyage provider instance: - **baseURL** _string_ The base URL of the voyage API - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. ## Embedding Models You can create models that call the [Voyage embeddings API](https://docs.voyageai.com/reference/embeddings-api) using the `.embedding()` factory method. ```ts import { voyage } from 'voyage-ai-provider'; const embeddingModel = voyage.embedding('voyage-3'); ``` You can find more models on the [Voyage Library](https://docs.voyageai.com/docs/embeddings) homepage. ### Model Capabilities | Model | Default Dimensions | Context Length | | ----------------------- | ------------------------------ | -------------- | | `voyage-3-large` | 1024 (default), 256, 512, 2048 | 32,000 | | `voyage-3` | 1024 | 32000 | | `voyage-code-3` | 1024 (default), 256, 512, 2048 | 32000 | | `voyage-3-lite` | 512 | 32000 | | `voyage-finance-2` | 1024 | 32000 | | `voyage-multilingual-2` | 1024 | 32000 | | `voyage-law-2` | 1024 | 32000 | | `voyage-code-2` | 1024 | 16000 | <Note> The table above lists popular models. Please see the [Voyage docs](https://docs.voyageai.com/docs/embeddings) for a full list of available models. </Note> ### Add settings to the model The settings object should contain the settings you want to add to the model. ```ts import { voyage } from 'voyage-ai-provider'; const embeddingModel = voyage.embedding('voyage-3', { inputType: 'document', // 'document' or 'query' truncation: false, outputDimension: 1024, // the new model voyage-code-3, voyage-3-large has 4 different output dimensions: 256, 512, 1024 (default), 2048 outputDtype: 'float', // output data types - int8, uint8, binary, ubinary are supported by the new model voyage-code-3, voyage-3-large }); ``` Learn more about the [output data types.](https://docs.voyageai.com/docs/faq#what-is-quantization-and-output-data-types) --- File: /ai/content/providers/03-community-providers/70-mem0.mdx --- --- title: 'Mem0' description: 'Learn how to use the Mem0 AI SDK provider for the AI SDK.' --- # Mem0 Provider The [Mem0 Provider](https://github.com/mem0ai/mem0/tree/main/vercel-ai-sdk) is a library developed by [**Mem0**](https://mem0.ai) to integrate with the AI SDK. This library brings enhanced AI interaction capabilities to your applications by introducing persistent memory functionality. <Note type="info"> 🎉 Exciting news! Mem0 AI SDK now supports <strong>Tools Call</strong>. </Note> ## Setup The Mem0 provider is available in the `@mem0/vercel-ai-provider` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @mem0/vercel-ai-provider" dark /> </Tab> <Tab> <Snippet text="npm install @mem0/vercel-ai-provider" dark /> </Tab> <Tab> <Snippet text="yarn add @mem0/vercel-ai-provider" dark /> </Tab> </Tabs> ## Provider Instance First, get your **Mem0 API Key** from the [Mem0 Dashboard](https://app.mem0.ai/dashboard/api-keys). Then initialize the `Mem0 Client` in your application: ```ts import { createMem0 } from '@mem0/vercel-ai-provider'; const mem0 = createMem0({ provider: 'openai', mem0ApiKey: 'm0-xxx', apiKey: 'provider-api-key', config: { // Configure the LLM Provider here }, // Optional Mem0 Global Config mem0Config: { user_id: 'mem0-user-id', enable_graph: true, }, }); ``` <Note> The `openai` provider is set as default. Consider using `MEM0_API_KEY` and `OPENAI_API_KEY` as environment variables for security. </Note> <Note> The `mem0Config` is optional. It is used to set the global config for the Mem0 Client (eg. `user_id`, `agent_id`, `app_id`, `run_id`, `org_id`, `project_id` etc). </Note> - Add Memories to Enhance Context: ```ts import { LanguageModelV2Prompt } from '@ai-sdk/provider'; import { addMemories } from '@mem0/vercel-ai-provider'; const messages: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'I love red cars.' }] }, ]; await addMemories(messages, { user_id: 'borat' }); ``` ## Features ### Adding and Retrieving Memories - `retrieveMemories()`: Retrieves memory context for prompts. - `getMemories()`: Get memories from your profile in array format. - `addMemories()`: Adds user memories to enhance contextual responses. ```ts await addMemories(messages, { user_id: 'borat', mem0ApiKey: 'm0-xxx', }); await retrieveMemories(prompt, { user_id: 'borat', mem0ApiKey: 'm0-xxx', }); await getMemories(prompt, { user_id: 'borat', mem0ApiKey: 'm0-xxx', }); ``` <Note> For standalone features, such as `addMemories`, `retrieveMemories`, and `getMemories`, you must either set `MEM0_API_KEY` as an environment variable or pass it directly in the function call. </Note> <Note> `getMemories` will return raw memories in the form of an array of objects, while `retrieveMemories` will return a response in string format with a system prompt ingested with the retrieved memories. </Note> ### Generate Text with Memory Context You can use language models from **OpenAI**, **Anthropic**, **Cohere**, and **Groq** to generate text with the `generateText` function: ```ts import { generateText } from 'ai'; import { createMem0 } from '@mem0/vercel-ai-provider'; const mem0 = createMem0(); const { text } = await generateText({ model: mem0('gpt-4.1', { user_id: 'borat' }), prompt: 'Suggest me a good car to buy!', }); ``` ### Structured Message Format with Memory ```ts import { generateText } from 'ai'; import { createMem0 } from '@mem0/vercel-ai-provider'; const mem0 = createMem0(); const { text } = await generateText({ model: mem0('gpt-4.1', { user_id: 'borat' }), messages: [ { role: 'user', content: [ { type: 'text', text: 'Suggest me a good car to buy.' }, { type: 'text', text: 'Why is it better than the other cars for me?' }, ], }, ], }); ``` ### Streaming Responses with Memory Context ```ts import { streamText } from 'ai'; import { createMem0 } from '@mem0/vercel-ai-provider'; const mem0 = createMem0(); const { textStream } = streamText({ model: mem0('gpt-4.1', { user_id: 'borat', }), prompt: 'Suggest me a good car to buy! Why is it better than the other cars for me? Give options for every price range.', }); for await (const textPart of textStream) { process.stdout.write(textPart); } ``` ### Generate Responses with Tools Call ```ts import { generateText } from 'ai'; import { createMem0 } from '@mem0/vercel-ai-provider'; import { z } from 'zod'; const mem0 = createMem0({ provider: 'anthropic', apiKey: 'anthropic-api-key', mem0Config: { // Global User ID user_id: 'borat', }, }); const prompt = 'What the temperature in the city that I live in?'; const result = await generateText({ model: mem0('claude-3-5-sonnet-20240620'), tools: { weather: tool({ description: 'Get the weather in a location', parameters: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, prompt: prompt, }); console.log(result); ``` ### Get sources from memory ```ts const { text, sources } = await generateText({ model: mem0('gpt-4.1'), prompt: 'Suggest me a good car to buy!', }); console.log(sources); ``` This same functionality is available in the `streamText` function. ## Supported LLM Providers The Mem0 provider supports the following LLM providers: | Provider | Configuration Value | | --------- | ------------------- | | OpenAI | `openai` | | Anthropic | `anthropic` | | Google | `google` | | Groq | `groq` | | Cohere | `cohere` | ## Best Practices - **User Identification**: Use a unique `user_id` for consistent memory retrieval. - **Memory Cleanup**: Regularly clean up unused memory data. <Note> We also have support for `agent_id`, `app_id`, and `run_id`. Refer [Docs](https://docs.mem0.ai/api-reference/memory/add-memories). </Note> ## Help - For more details on Vercel AI SDK, visit the [Vercel AI SDK documentation](/docs/introduction). - For Mem0 documentation, refer to the [Mem0 Platform](https://app.mem0.ai/). - If you need further assistance, please feel free to reach out to us through following methods: ## References - [Mem0 AI SDK Docs](https://docs.mem0.ai/integrations/vercel-ai-sdk#getting-started) - [Mem0 documentation](https://docs.mem0.ai) --- File: /ai/content/providers/03-community-providers/71-letta.mdx --- --- title: 'Letta' description: 'Learn how to use the Letta AI SDK provider for the AI SDK.' --- # Letta Provider <Note type="warning"> This community provider is not yet compatible with AI SDK 5. Please wait for the provider to be updated or consider using an [AI SDK 5 compatible provider](/providers/ai-sdk-providers). </Note> The [Letta AI-SDK provider](https://github.com/letta-ai/vercel-ai-sdk-provider) is the official provider for the [Letta](https://docs.letta.com) platform. It allows you to integrate Letta's AI capabilities into your applications using the Vercel AI SDK. ## Setup The Letta provider is available in the `@letta-ai/vercel-ai-sdk-provider` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @letta-ai/vercel-ai-sdk-provider" dark /> </Tab> <Tab> <Snippet text="npm install @letta-ai/vercel-ai-sdk-provider" dark /> </Tab> <Tab> <Snippet text="yarn add @letta-ai/vercel-ai-sdk-provider" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `letta` from `@letta-ai/vercel-ai-sdk-provider`: ```ts import { letta } from '@letta-ai/vercel-ai-sdk-provider'; ``` ## Quick Start ### Using Letta Cloud (https://api.letta.com) Create a file called `.env.local` and add your [API Key](https://app.letta.com/api-keys) ```text LETTA_API_KEY=<your_api_key> ``` ```ts import { lettaCloud } from '@letta-ai/vercel-ai-sdk-provider'; import { generateText } from 'ai'; const { text } = await generateText({ model: lettaCloud('your_agent_id'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` ### Local instances (http://localhost:8283) ```ts import { lettaLocal } from '@letta-ai/vercel-ai-sdk-provider'; import { generateText } from 'ai'; const { text } = await generateText({ model: lettaLocal('your_agent_id'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` ### Custom setups ```ts import { createLetta } from '@letta-ai/vercel-ai-sdk-provider'; import { generateText } from 'ai'; const letta = createLetta({ baseUrl: '<your_base_url>', token: '<your_access_token>', }); const { text } = await generateText({ model: letta('your_agent_id'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` ### Using other Letta Client Functions The `vercel-ai-sdk-provider` extends the [@letta-ai/letta-client](https://www.npmjs.com/package/@letta-ai/letta-client), you can access the operations directly by using `lettaCloud.client` or `lettaLocal.client` or your custom generated `letta.client` ```ts // with Letta Cloud import { lettaCloud } from '@letta-ai/vercel-ai-sdk-provider'; lettaCloud.client.agents.list(); // with Letta Local import { lettaLocal } from '@letta-ai/vercel-ai-sdk-provider'; lettaLocal.client.agents.list(); ``` ### More Information For more information on the Letta API, please refer to the [Letta API documentation](https://docs.letta.com/api). --- File: /ai/content/providers/03-community-providers/91-anthropic-vertex-ai.mdx --- --- title: Anthropic Vertex description: Learn how to use the Anthropic Vertex provider for the AI SDK. --- # AnthropicVertex Provider <Note> Anthropic for Google Vertex is also support by the [AI SDK Google Vertex provider](/providers/ai-sdk-providers/google-vertex). </Note> <Note type="warning"> This community provider is not yet compatible with AI SDK 5. Please wait for the provider to be updated or consider using an [AI SDK 5 compatible provider](/providers/ai-sdk-providers). </Note> [nalaso/anthropic-vertex-ai](https://github.com/nalaso/anthropic-vertex-ai) is a community provider that uses Anthropic models through Vertex AI to provide language model support for the AI SDK. ## Setup The AnthropicVertex provider is available in the `anthropic-vertex-ai` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add anthropic-vertex-ai" dark /> </Tab> <Tab> <Snippet text="npm install anthropic-vertex-ai" dark /> </Tab> <Tab> <Snippet text="yarn add anthropic-vertex-ai" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `anthropicVertex` from `anthropic-vertex-ai`: ```ts import { anthropicVertex } from 'anthropic-vertex-ai'; ``` If you need a customized setup, you can import `createAnthropicVertex` from `anthropic-vertex-ai` and create a provider instance with your settings: ```ts import { createAnthropicVertex } from 'anthropic-vertex-ai'; const anthropicVertex = createAnthropicVertex({ region: 'us-central1', projectId: 'your-project-id', // other options }); ``` You can use the following optional settings to customize the AnthropicVertex provider instance: - **region** _string_ Your Google Vertex region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable. - **projectId** _string_ Your Google Vertex project ID. Defaults to the `GOOGLE_VERTEX_PROJECT_ID` environment variable. - **googleAuth** _GoogleAuth_ Optional. The Authentication options provided by google-auth-library. - **baseURL** _string_ Use a different URL prefix for API calls, e.g., to use proxy servers. The default prefix is `https://{region}-aiplatform.googleapis.com/v1`. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g., testing. ## Language Models You can create models that call the Anthropic API through Vertex AI using the provider instance. The first argument is the model ID, e.g., `claude-3-sonnet@20240229`: ```ts const model = anthropicVertex('claude-3-sonnet@20240229'); ``` ### Example: Generate Text You can use AnthropicVertex language models to generate text with the `generateText` function: ```ts import { anthropicVertex } from 'anthropic-vertex-ai'; import { generateText } from 'ai'; const { text } = await generateText({ model: anthropicVertex('claude-3-sonnet@20240229'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` AnthropicVertex language models can also be used in the `streamText`, `generateObject`, and `streamObject` functions (see [AI SDK Core](/docs/ai-sdk-core) for more information). ### Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | ---------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | | `claude-3-5-sonnet@20240620` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `claude-3-opus@20240229` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `claude-3-sonnet@20240229` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | | `claude-3-haiku@20240307` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | ## Environment Variables To use the AnthropicVertex provider, you need to set up the following environment variables: - `GOOGLE_VERTEX_REGION`: Your Google Vertex region (e.g., 'us-central1') - `GOOGLE_VERTEX_PROJECT_ID`: Your Google Cloud project ID Make sure to set these variables in your environment or in a `.env` file in your project root. ## Authentication The AnthropicVertex provider uses Google Cloud authentication. Make sure you have set up your Google Cloud credentials properly. You can either use a service account key file or default application credentials. For more information on setting up authentication, refer to the [Google Cloud Authentication guide](https://cloud.google.com/docs/authentication). --- File: /ai/content/providers/03-community-providers/92-spark.mdx --- --- title: Spark description: Learn how to use the Spark provider for the AI SDK. --- # Spark Provider <Note type="warning"> This community provider is not yet compatible with AI SDK 5. Please wait for the provider to be updated or consider using an [AI SDK 5 compatible provider](/providers/ai-sdk-providers). </Note> The **[Spark provider](https://github.com/klren0312/spark-ai-provider)** contains language model support for the Spark API, giving you access to models like lite, generalv3, pro-128k, generalv3.5, max-32k and 4.0Ultra. ## Setup The Spark provider is available in the `spark-ai-provider` module. You can install it with ```bash npm i spark-ai-provider ``` ## Provider Instance You can import `createSparkProvider` from `spark-ai-provider` to create a provider instance: ```ts import { createSparkProvider } from 'spark-ai-provider'; ``` ## Example ```ts import { createSparkProvider } from './index.mjs'; import { generateText } from 'ai'; const spark = createSparkProvider({ apiKey: '', }); const { text } = await generateText({ model: spark('lite'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); ``` ## Documentation Please check out the **[Spark provider documentation](https://github.com/klren0312/spark-ai-provider)** for more information. --- File: /ai/content/providers/03-community-providers/93-inflection-ai.mdx --- --- title: Inflection AI description: Learn how to use the unofficial Inflection AI provider for the AI SDK. --- # Unofficial Community Provider for AI SDK - Inflection AI <Note type="warning"> This community provider is not yet compatible with AI SDK 5. Please wait for the provider to be updated or consider using an [AI SDK 5 compatible provider](/providers/ai-sdk-providers). </Note> The **[unofficial Inflection AI provider](https://www.npmjs.com/package/inflection-ai-sdk-provider)** for the [AI SDK](/docs) contains language model support for the [Inflection AI API](https://developers.inflection.ai/). ## Setup The Inflection AI provider is available in the [`inflection-ai-sdk-provider`](https://www.npmjs.com/package/inflection-ai-sdk-provider) module on npm. You can install it with ```bash npm i inflection-ai-sdk-provider ``` ## Provider Instance You can import the default provider instance `inflection` from `inflection-ai-sdk-provider`: ```ts import { inflection } from 'inflection-ai-sdk-provider'; ``` ## Example ```ts import { inflection } from 'inflection-ai-sdk-provider'; import { generateText } from 'ai'; const { text } = await generateText({ model: inflection('inflection_3_with_tools'), prompt: 'how can I make quick chicken pho?', }); ``` ## Models The following models are supported: - `inflection_3_pi` - "the model powering our Pi experience, including a backstory, emotional intelligence, productivity, and safety. It excels in scenarios such as customer support chatbots." - `inflection_3_productivity`- "the model optimized for following instructions. It is better for tasks requiring JSON output or precise adherence to provided guidelines." - `inflection_3_with_tools` - This model seems to be in preview and it lacks an official description as of the writing of this README in 1.0.0. | Model | Text Generation | Streaming | Image Input | Object Generation | Tool Usage | Tool Streaming | | --------------------------- | --------------- | --------- | ----------- | ----------------- | ---------- | -------------- | | `inflection_3_pi` | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | | `inflection_3_productivity` | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | | `inflection_3_with_tools` | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | There is limited API support for features other than text generation and streaming text at this time. Should that change, the table above will be updated and support will be added to this unofficial provider. ## Documentation Please check out Inflection AI's [API Documentation](https://developers.inflection.ai/docs/api-reference) for more information. You can find the source code for this provider [here on GitHub](https://github.com/Umbrage-Studios/inflection-ai-sdk-provider). --- File: /ai/content/providers/03-community-providers/94-langdb.mdx --- --- title: LangDB description: Learn how to use LangDB with the AI SDK --- # LangDB <Note type="warning"> This community provider is not yet compatible with AI SDK 5. Please wait for the provider to be updated or consider using an [AI SDK 5 compatible provider](/providers/ai-sdk-providers). </Note> [LangDB](https://langdb.ai) is a high-performance enterprise AI gateway built in Rust, designed to govern, secure, and optimize AI traffic. LangDB provides OpenAI-compatible APIs, enabling developers to connect with multiple LLMs by changing just two lines of code. With LangDB, you can: - Provide access to all major LLMs - Enable plug-and-play functionality using any framework like Langchain, Vercel AI SDK, CrewAI, etc., for easy adoption. - Simplify implementation of tracing and cost optimization features, ensuring streamlined operations. - Dynamically route requests to the most suitable LLM based on predefined parameters. ## Setup The LangDB provider is available via the `@langdb/vercel-provider` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @langdb/vercel-provider" dark /> </Tab> <Tab> <Snippet text="npm install @langdb/vercel-provider" dark /> </Tab> <Tab> <Snippet text="yarn add @langdb/vercel-provider" dark /> </Tab> </Tabs> ## Provider Instance To create a LangDB provider instance, use the `createLangDB` function: ```tsx import { createLangDB } from '@langdb/vercel-provider'; const langdb = createLangDB({ apiKey: process.env.LANGDB_API_KEY, // Required projectId: 'your-project-id', // Required threadId: uuidv4(), // Optional runId: uuidv4(), // Optional label: 'code-agent', // Optional headers: { 'Custom-Header': 'value' }, // Optional }); ``` You can find your LangDB API key in the [LangDB dashboard](https://app.langdb.ai). ## Examples You can use LangDB with the `generateText` or `streamText` function: ### `generateText` ```tsx import { createLangDB } from '@langdb/vercel-provider'; import { generateText } from 'ai'; const langdb = createLangDB({ apiKey: process.env.LANGDB_API_KEY, projectId: 'your-project-id', }); export async function generateTextExample() { const { text } = await generateText({ model: langdb('openai/gpt-4o-mini'), prompt: 'Write a Python function that sorts a list:', }); console.log(text); } ``` ### generateImage ```tsx import { createLangDB } from '@langdb/vercel-provider'; import { experimental_generateImage as generateImage } from 'ai'; import fs from 'fs'; import path from 'path'; const langdb = createLangDB({ apiKey: process.env.LANGDB_API_KEY, projectId: 'your-project-id', }); export async function generateImageExample() { const { images } = await generateImage({ model: langdb.image('openai/dall-e-3'), prompt: 'A delighted resplendent quetzal mid-flight amidst raindrops', }); const imagePath = path.join(__dirname, 'generated-image.png'); fs.writeFileSync(imagePath, images[0].uint8Array); console.log(`Image saved to: ${imagePath}`); } ``` ### embed ```tsx import { createLangDB } from '@langdb/vercel-provider'; import { embed } from 'ai'; const langdb = createLangDB({ apiKey: process.env.LANGDB_API_KEY, projectId: 'your-project-id', }); export async function generateEmbeddings() { const { embedding } = await embed({ model: langdb.textEmbeddingModel('text-embedding-3-small'), value: 'sunny day at the beach', }); console.log('Embedding:', embedding); } ``` ## Supported Models LangDB supports over 250+ models, enabling seamless interaction with a wide range of AI capabilities. Checkout the [model list](https://app.langdb.ai/models) for more information. For more information, visit the [LangDB documentation](https://docs.langdb.ai/). --- File: /ai/content/providers/03-community-providers/95-zhipu.mdx --- --- title: Zhipu AI description: Learn how to use the Zhipu provider. --- # Zhipu AI Provider <Note type="warning"> This community provider is not yet compatible with AI SDK 5. Please wait for the provider to be updated or consider using an [AI SDK 5 compatible provider](/providers/ai-sdk-providers). </Note> [Zhipu AI Provider](https://github.com/Xiang-CH/zhipu-ai-provider) is a community provider for the [AI SDK](/). It enables seamless integration with **GLM** and Embedding Models provided on [bigmodel.cn](https://bigmodel.cn/) by [ZhipuAI](https://www.zhipuai.cn/). ## Setup <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add zhipu-ai-provider" dark /> </Tab> <Tab> <Snippet text="npm i zhipu-ai-provider" dark /> </Tab> <Tab> <Snippet text="yarn add zhipu-ai-provider" dark /> </Tab> </Tabs> Set up your `.env` file / environment with your API key. ```bash ZHIPU_API_KEY=<your-api-key> ``` ## Provider Instance You can import the default provider instance `zhipu` from `zhipu-ai-provider` (This automatically reads the API key from the environment variable `ZHIPU_API_KEY`): ```ts import { zhipu } from 'zhipu-ai-provider'; ``` Alternatively, you can create a provider instance with custom configuration with `createZhipu`: ```ts import { createZhipu } from 'zhipu-ai-provider'; const zhipu = createZhipu({ baseURL: 'https://open.bigmodel.cn/api/paas/v4', apiKey: 'your-api-key', }); ``` You can use the following optional settings to customize the Zhipu provider instance: - **baseURL**: _string_ - Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://open.bigmodel.cn/api/paas/v4`. - **apiKey**: _string_ - Your API key for Zhipu [BigModel Platform](https://bigmodel.cn/). If not provided, the provider will attempt to read the API key from the environment variable `ZHIPU_API_KEY`. - **headers**: _Record\<string, string\>_ - Custom headers to include in the requests. ## Example ```ts import { zhipu } from 'zhipu-ai-provider'; const { text } = await generateText({ model: zhipu('glm-4-plus'), prompt: 'Why is the sky blue?', }); console.log(result); ``` ## Documentation - **[Zhipu documentation](https://bigmodel.cn/dev/welcome)** --- File: /ai/content/providers/03-community-providers/96-sambanova.mdx --- --- title: SambaNova description: Learn how to use the SambaNova provider for the AI SDK. --- # SambaNova Provider [sambanova-ai-provider](https://github.com/sambanova/sambanova-ai-provider) contains language model support for the SambaNova API. API keys can be obtained from the [SambaNova Cloud Platform](https://cloud.sambanova.ai/apis). ## Setup The SambaNova provider is available via the `sambanova-ai-provider` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add sambanova-ai-provider" dark /> </Tab> <Tab> <Snippet text="npm install sambanova-ai-provider" dark /> </Tab> <Tab> <Snippet text="yarn add sambanova-ai-provider" dark /> </Tab> </Tabs> ### Environment variables Create a `.env` file with a `SAMBANOVA_API_KEY` variable. ## Provider Instance You can import the default provider instance `sambanova` from `sambanova-ai-provider`: ```ts import { sambanova } from 'sambanova-ai-provider'; ``` If you need a customized setup, you can import `createSambaNova` from `sambanova-ai-provider` and create a provider instance with your settings: ```ts import { createSambaNova } from 'sambanova-ai-provider'; const sambanova = createSambaNova({ // Optional settings }); ``` You can use the following optional settings to customize the SambaNova provider instance: - **baseURL** _string_ Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.sambanova.ai/v1`. - **apiKey** _string_ API key that is being sent using the `Authorization` header. It defaults to the `SAMBANOVA_API_KEY` environment variable. - **headers** _Record&lt;string,string&gt;_ Custom headers to include in the requests. - **fetch** _(input: RequestInfo, init?: RequestInit) => Promise&lt;Response&gt;_ Custom [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) implementation. Defaults to the global `fetch` function. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. ## Models You can use [SambaNova models](https://docs.sambanova.ai/cloud/docs/get-started/supported-models) on a provider instance. The first argument is the model id, e.g. `Meta-Llama-3.1-70B-Instruct`. ```ts const model = sambanova('Meta-Llama-3.1-70B-Instruct'); ``` ## Tested models and capabilities This provider is capable of generating and streaming text, and interpreting image inputs. At least it has been tested with the following features (which use the `/chat/completion` endpoint): | Chat completion | Image input | | ------------------- | ------------------- | | <Check size={18} /> | <Check size={18} /> | ### Image input You need to use any of the following models for visual understanding: - Llama3.2-11B-Vision-Instruct - Llama3.2-90B-Vision-Instruct SambaNova does not support URLs, but the ai-sdk is able to download the file and send it to the model. ## Example Usage Basic demonstration of text generation using the SambaNova provider. ```ts import { createSambaNova } from 'sambanova-ai-provider'; import { generateText } from 'ai'; const sambanova = createSambaNova({ apiKey: 'YOUR_API_KEY', }); const model = sambanova('Meta-Llama-3.1-70B-Instruct'); const { text } = await generateText({ model, prompt: 'Hello, nice to meet you.', }); console.log(text); ``` You will get an output text similar to this one: ``` Hello. Nice to meet you too. Is there something I can help you with or would you like to chat? ``` ## Intercepting Fetch Requests This provider supports [Intercepting Fetch Requests](/examples/providers/intercepting-fetch-requests). ### Example ```ts import { createSambaNova } from 'sambanova-ai-provider'; import { generateText } from 'ai'; const sambanovaProvider = createSambaNova({ apiKey: 'YOUR_API_KEY', fetch: async (url, options) => { console.log('URL', url); console.log('Headers', JSON.stringify(options.headers, null, 2)); console.log(`Body ${JSON.stringify(JSON.parse(options.body), null, 2)}`); return await fetch(url, options); }, }); const model = sambanovaProvider('Meta-Llama-3.1-70B-Instruct'); const { text } = await generateText({ model, prompt: 'Hello, nice to meet you.', }); ``` And you will get an output like this: ```bash URL https://api.sambanova.ai/v1/chat/completions Headers { "Content-Type": "application/json", "Authorization": "Bearer YOUR_API_KEY" } Body { "model": "Meta-Llama-3.1-70B-Instruct", "temperature": 0, "messages": [ { "role": "user", "content": "Hello, nice to meet you." } ] } ``` --- File: /ai/content/providers/03-community-providers/97-dify.mdx --- --- title: Dify description: Learn how to use the Dify provider for the AI SDK. --- # Dify Provider The **[Dify provider](https://github.com/warmwind/dify-ai-provider)** allows you to easily integrate Dify's application workflow with your applications using the AI SDK. ## Setup The Dify provider is available in the `dify-ai-provider` module. You can install it with: ```bash npm install dify-ai-provider # pnpm pnpm add dify-ai-provider # yarn yarn add dify-ai-provider ``` ## Provider Instance You can import `difyProvider` from `dify-ai-provider` to create a provider instance: ```ts import { difyProvider } from 'dify-ai-provider'; ``` ## Example ### Use dify.ai ```ts import { generateText } from 'ai'; import { difyProvider } from 'dify-ai-provider'; const dify = difyProvider('dify-application-id', { responseMode: 'blocking', apiKey: 'dify-api-key', }); const { text, providerMetadata } = await generateText({ model: dify, messages: [{ role: 'user', content: 'Hello, how are you today?' }], headers: { 'user-id': 'test-user' }, }); const { conversationId, messageId } = providerMetadata.difyWorkflowData; console.log(text); console.log('conversationId', conversationId); console.log('messageId', messageId); ``` ### Use self-hosted Dify ```typescript import { createDifyProvider } from 'dify-ai-provider'; const difyProvider = createDifyProvider({ baseURL: 'your-base-url', }); const dify = difyProvider('dify-application-id', { responseMode: 'blocking', apiKey: 'dify-api-key', }); ``` ## Documentation Please refer to the **[Dify provider documentation](https://github.com/warmwind/dify-ai-provider)** for more detailed information. --- File: /ai/content/providers/03-community-providers/97-sarvam.mdx --- --- title: 'Sarvam' description: 'Learn how to use the Sarvam AI provider for the AI SDK.' --- # Sarvam Provider The Sarvam AI Provider is a library developed to integrate with the AI SDK. This library brings Speech to Text (STT) capabilities to your applications, allowing for seamless interaction with audio and text data. ## Setup The Sarvam provider is available in the `sarvam-ai-provider` module. You can install it with: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add sarvam-ai-provider" dark /> </Tab> <Tab> <Snippet text="npm install sarvam-ai-provider" dark /> </Tab> <Tab> <Snippet text="yarn add sarvam-ai-provider" dark /> </Tab> </Tabs> ## Provider Instance First, get your **Sarvam API Key** from the [Sarvam Dashboard](https://dashboard.sarvam.ai/auth/signin). Then initialize `Sarvam` in your application: ```ts import { createSarvam } from 'sarvam-ai-provider'; const sarvam = createSarvam({ headers: { 'api-subscription-key': 'YOUR_API_KEY', }, }); ``` <Note> The `api-subscription-key` needs to be passed in headers. Consider using `YOUR_API_KEY` as environment variables for security. </Note> - Transcribe speech to text ```ts import { experimental_transcribe as transcribe } from 'ai'; import { readFile } from 'fs/promises'; await transcribe({ model: sarvam.transcription('saarika:v2'), audio: await readFile('./src/transcript-test.mp3'), providerOptions: { sarvam: { language_code: 'en-IN', }, }, }); ``` ## Features ### Changing parameters - Change language_code ```ts providerOptions: { sarvam: { language_code: 'en-IN', }, }, ``` <Note> `language_code` specifies the language of the input audio and is required for accurate transcription. • It is mandatory for the `saarika:v1` model (this model does not support `unknown`). • It is optional for the `saarika:v2` model. • Use `unknown` when the language is not known; in that case, the API will auto‑detect it. Available options: `unknown`, `hi-IN`, `bn-IN`, `kn-IN`, `ml-IN`, `mr-IN`, `od-IN`, `pa-IN`, `ta-IN`, `te-IN`, `en-IN`, `gu-IN`. </Note> - with_timestamps? ```ts providerOptions: { sarvam: { with_timestamps: true, }, }, ``` <Note> `with_timestamps` specifies whether to include start/end timestamps for each word/token. • Type: boolean • When true, each word/token will include start/end timestamps. • Default: false </Note> - with_diarization? ```ts providerOptions: { sarvam: { with_diarization: true, }, }, ``` <Note> `with_diarization` enables speaker diarization (Beta). • Type: boolean • When true, enables speaker diarization. • Default: false </Note> - num_speakers? ```ts providerOptions: { sarvam: { with_diarization: true, num_speakers: 2, }, }, ``` <Note> `num_speakers` sets the number of distinct speakers to detect (only when `with_diarization` is true). • Type: number | null • Number of distinct speakers to detect. • Default: null </Note> ## References - [Sarvam API Docs](https://docs.sarvam.ai/api-reference-docs/endpoints/speech-to-text) --- File: /ai/content/providers/03-community-providers/99-claude-code.mdx --- --- title: Claude Code description: Learn how to use the Claude Code community provider to access Claude through your Pro/Max subscription. --- # Claude Code Provider The [ai-sdk-provider-claude-code](https://github.com/ben-vargas/ai-sdk-provider-claude-code) community provider allows you to access Claude models through the official Claude Code SDK/CLI. While it works with both Claude Pro/Max subscriptions and API key authentication, it's particularly useful for developers who want to use their existing Claude subscription without managing API keys. ## Version Compatibility The Claude Code provider supports both AI SDK v4 and v5-beta: | Provider Version | AI SDK Version | Status | Branch | | ---------------- | -------------- | ------ | --------------------------------------------------------------------------------------- | | 0.x | v4 | Stable | [`ai-sdk-v4`](https://github.com/ben-vargas/ai-sdk-provider-claude-code/tree/ai-sdk-v4) | | 1.x-beta | v5-beta | Beta | [`main`](https://github.com/ben-vargas/ai-sdk-provider-claude-code/tree/main) | ## Setup The Claude Code provider is available in the `ai-sdk-provider-claude-code` module. Install the version that matches your AI SDK version: ### For AI SDK v5-beta (latest) <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add ai-sdk-provider-claude-code ai" dark /> </Tab> <Tab> <Snippet text="npm install ai-sdk-provider-claude-code ai" dark /> </Tab> <Tab> <Snippet text="yarn add ai-sdk-provider-claude-code ai" dark /> </Tab> </Tabs> ### For AI SDK v4 (stable) <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add ai-sdk-provider-claude-code@^0 ai@^4" dark /> </Tab> <Tab> <Snippet text="npm install ai-sdk-provider-claude-code@^0 ai@^4" dark /> </Tab> <Tab> <Snippet text="yarn add ai-sdk-provider-claude-code@^0 ai@^4" dark /> </Tab> </Tabs> ## Provider Instance You can import the default provider instance `claudeCode` from `ai-sdk-provider-claude-code`: ```ts import { claudeCode } from 'ai-sdk-provider-claude-code'; ``` If you need a customized setup, you can import `createClaudeCode` from `ai-sdk-provider-claude-code` and create a provider instance with your settings: ```ts import { createClaudeCode } from 'ai-sdk-provider-claude-code'; const claudeCode = createClaudeCode({ allowedTools: ['Read', 'Write', 'Edit'], disallowedTools: ['Bash'], // other options }); ``` You can use the following optional settings to customize the Claude Code provider instance: - **anthropicDir** _string_ Optional. Directory for Claude Code CLI data. Defaults to `~/.claude/claude_code`. - **allowedTools** _string[]_ Optional. List of allowed tools. When specified, only these tools will be available. - **disallowedTools** _string[]_ Optional. List of disallowed tools. These tools will be blocked even if enabled in settings. - **mcpServers** _string[]_ Optional. List of MCP server names to use for this session. ## Language Models You can create models that call Claude through the Claude Code CLI using the provider instance. The first argument is the model ID: ```ts const model = claudeCode('opus'); ``` Claude Code supports the following models: - **opus**: Claude 4 Opus (most capable) - **sonnet**: Claude 4 Sonnet (balanced performance) ### Example: Generate Text You can use Claude Code language models to generate text with the `generateText` function: ```ts import { claudeCode } from 'ai-sdk-provider-claude-code'; import { generateText } from 'ai'; // AI SDK v4 const { text } = await generateText({ model: claudeCode('opus'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); // AI SDK v5-beta const result = await generateText({ model: claudeCode('opus'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', }); const text = await result.text; ``` Claude Code language models can also be used in the `streamText`, `generateObject`, and `streamObject` functions (see [AI SDK Core](/docs/ai-sdk-core) for more information). <Note> The response format differs between AI SDK v4 and v5-beta. In v4, text is accessed directly via `result.text`. In v5-beta, it's accessed as a promise via `await result.text`. Make sure to use the appropriate format for your AI SDK version. </Note> ### Model Capabilities | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | | -------- | ------------------- | ------------------- | ------------------- | ------------------- | | `opus` | <Cross size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | | `sonnet` | <Cross size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Note> The ❌ for "Tool Usage" and "Tool Streaming" refers specifically to the AI SDK's standardized tool interface, which allows defining custom functions with schemas that models can call. The Claude Code provider uses a different architecture where Claude's built-in tools (Bash, Edit, Read, Write, etc.) and MCP servers are managed directly by the Claude Code CLI. While you cannot define custom tools using the AI SDK's conventions, Claude can still effectively use its comprehensive set of built-in tools to perform tasks like file manipulation, web fetching, and command execution. </Note> ## Authentication The Claude Code provider uses your existing Claude Pro or Max subscription through the Claude Code CLI. You need to authenticate once using: ```bash claude login ``` This will open a browser window for authentication. Once authenticated, the provider will use your subscription automatically. ## Built-in Tools One of the unique features of the Claude Code provider is access to Claude's built-in tools: - **Bash**: Execute shell commands - **Edit**: Edit files with precise replacements - **Read**: Read file contents - **Write**: Write new files - **LS**: List directory contents - **Grep**: Search file contents - **WebFetch**: Fetch and analyze web content - And more... You can control which tools are available per session using the `allowedTools` and `disallowedTools` options. ## Extended Thinking The Claude Code provider supports Claude Opus 4's extended thinking capabilities with proper timeout management. When using extended thinking, make sure to provide an appropriate AbortSignal with a timeout of up to 10 minutes: ```ts const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10 * 60 * 1000); // 10 minutes try { const { text } = await generateText({ model: claudeCode('opus'), prompt: 'Solve this complex problem...', abortSignal: controller.signal, }); } finally { clearTimeout(timeout); } ``` ## Requirements - Node.js 18 or higher - Claude Code CLI installed (`npm install -g @anthropic-ai/claude-code`) - Claude Code authenticated with Pro or Max subscription, or API key. --- File: /ai/content/providers/03-community-providers/index.mdx --- --- title: Community Providers description: Learn how to use Language Model Specification. --- # Community Providers The AI SDK provides a [Language Model Specification](https://github.com/vercel/ai/tree/main/packages/provider/src/language-model/v2). You can [write your own provider](./community-providers/custom-providers) that adheres to the specification and it will be compatible with the AI SDK. Here are the community providers that implement the Language Model Specification: <CommunityModelCards /> --- File: /ai/content/providers/04-adapters/01-langchain.mdx --- --- title: LangChain description: Learn how to use LangChain with the AI SDK. --- # LangChain [LangChain](https://js.langchain.com/docs/) is a framework for developing applications powered by language models. It provides tools and abstractions for working with AI models, agents, vector stores, and other data sources for retrieval augmented generation (RAG). However, LangChain does not provide a way to easily build UIs or a standard way to stream data to the client. ## Example: Completion Here is a basic example that uses both the AI SDK and LangChain together with the [Next.js](https://nextjs.org/docs) App Router. The [`@ai-sdk/langchain` package](/docs/reference/stream-helpers/langchain-adapter) uses the result from [LangChain ExpressionLanguage streaming](https://js.langchain.com/docs/how_to/streaming) to pipe text to the client. `toDataStreamResponse()` is compatible with the LangChain Expression Language `.stream()` function response. ```tsx filename="app/api/completion/route.ts" highlight={"16"} import { ChatOpenAI } from '@langchain/openai'; import { toDataStreamResponse } from '@ai-sdk/langchain'; export const maxDuration = 60; export async function POST(req: Request) { const { prompt } = await req.json(); const model = new ChatOpenAI({ model: 'gpt-3.5-turbo-0125', temperature: 0, }); const stream = await model.stream(prompt); return toDataStreamResponse(stream); } ``` Then, we use the AI SDK's [`useCompletion`](/docs/ai-sdk-ui/completion) method in the page component to handle the completion: ```tsx filename="app/page.tsx" 'use client'; import { useCompletion } from '@ai-sdk/react'; export default function Chat() { const { completion, input, handleInputChange, handleSubmit } = useCompletion(); return ( <div> {completion} <form onSubmit={handleSubmit}> <input value={input} onChange={handleInputChange} /> </form> </div> ); } ``` ## More Examples You can find additional examples in the AI SDK [examples/next-langchain](https://github.com/vercel/ai/tree/main/examples/next-langchain) folder. --- File: /ai/content/providers/04-adapters/02-llamaindex.mdx --- --- title: LlamaIndex description: Learn how to use LlamaIndex with the AI SDK. --- # LlamaIndex [LlamaIndex](https://ts.llamaindex.ai/) is a framework for building LLM-powered applications. LlamaIndex helps you ingest, structure, and access private or domain-specific data. LlamaIndex.TS offers the core features of LlamaIndex for Python for popular runtimes like Node.js (official support), Vercel Edge Functions (experimental), and Deno (experimental). ## Example: Completion Here is a basic example that uses both AI SDK and LlamaIndex together with the [Next.js](https://nextjs.org/docs) App Router. The AI SDK [`@ai-sdk/llamaindex` package](/docs/reference/stream-helpers/llamaindex-adapter) uses the stream result from calling the `chat` method on a [LlamaIndex ChatEngine](https://ts.llamaindex.ai/modules/chat_engine) or the `query` method on a [LlamaIndex QueryEngine](https://ts.llamaindex.ai/modules/query_engines) to pipe text to the client. ```tsx filename="app/api/completion/route.ts" highlight="17" import { OpenAI, SimpleChatEngine } from 'llamaindex'; import { toDataStreamResponse } from '@ai-sdk/llamaindex'; export const maxDuration = 60; export async function POST(req: Request) { const { prompt } = await req.json(); const llm = new OpenAI({ model: 'gpt-4o' }); const chatEngine = new SimpleChatEngine({ llm }); const stream = await chatEngine.chat({ message: prompt, stream: true, }); return toDataStreamResponse(stream); } ``` Then, we use the AI SDK's [`useCompletion`](/docs/ai-sdk-ui/completion) method in the page component to handle the completion: ```tsx filename="app/page.tsx" 'use client'; import { useCompletion } from '@ai-sdk/react'; export default function Chat() { const { completion, input, handleInputChange, handleSubmit } = useCompletion(); return ( <div> {completion} <form onSubmit={handleSubmit}> <input value={input} onChange={handleInputChange} /> </form> </div> ); } ``` ## More Examples [create-llama](https://github.com/run-llama/create-llama) is the easiest way to get started with LlamaIndex. It uses the AI SDK to connect to LlamaIndex in all its generated code. --- File: /ai/content/providers/04-adapters/index.mdx --- --- title: Adapters description: Learn how to use AI SDK Adapters. --- # Adapters Adapters are lightweight integrations that enable you to use the AI SDK UI functions (`useChat` and `useCompletion`) with 3rd party libraries. The following adapters are currently available: - [LangChain](/providers/adapters/langchain) - [LlamaIndex](/providers/adapters/llamaindex) --- File: /ai/content/providers/05-observability/braintrust.mdx --- --- title: Braintrust description: Monitoring and tracing LLM applications with Braintrust --- # Braintrust Observability Braintrust is an end-to-end platform for building AI applications. When building with the AI SDK, you can integrate Braintrust to [log](https://www.braintrust.dev/docs/guides/logging), monitor, and take action on real-world interactions. ## Setup Braintrust natively supports OpenTelemetry and works out of the box with the AI SDK, either via Next.js or Node.js. ### Next.js If you are using Next.js, you can use the Braintrust exporter with `@vercel/otel` for the cleanest setup: ```typescript import { registerOTel } from '@vercel/otel'; import { BraintrustExporter } from 'braintrust'; // In your instrumentation.ts file export function register() { registerOTel({ serviceName: 'my-braintrust-app', traceExporter: new BraintrustExporter({ parent: 'project_name:your-project-name', filterAISpans: true, // Only send AI-related spans }), }); } ``` Or set the following environment variables in your app's `.env` file, with your API key and project ID: ``` OTEL_EXPORTER_OTLP_ENDPOINT=https://api.braintrust.dev/otel OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <Your API Key>, x-bt-parent=project_id:<Your Project ID>" ``` Traced LLM calls will appear under the Braintrust project or experiment provided in the `x-bt-parent` header. When you call the AI SDK, make sure to set `experimental_telemetry`: ```typescript const result = await generateText({ model: openai('gpt-4o-mini'), prompt: 'What is 2 + 2?', experimental_telemetry: { isEnabled: true, metadata: { query: 'weather', location: 'San Francisco', }, }, }); ``` <Note> The integration supports streaming functions like `streamText`. Each streamed call will produce `ai.streamText` spans in Braintrust. ```typescript import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; export async function POST(req: Request) { const { prompt } = await req.json(); const result = await streamText({ model: openai('gpt-4o-mini'), prompt, experimental_telemetry: { isEnabled: true }, }); return result.toDataStreamResponse(); } ``` </Note> ### Node.js If you are using Node.js without a framework, you must configure the `NodeSDK` directly. In this case, it's more straightforward to use the `BraintrustSpanProcessor`. First, install the necessary dependencies: ```bash npm install ai @ai-sdk/openai braintrust @opentelemetry/sdk-node @opentelemetry/sdk-trace-base zod ``` Then, set up the OpenTelemetry SDK: ```typescript import { NodeSDK } from '@opentelemetry/sdk-node'; import { generateText, tool } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; import { BraintrustSpanProcessor } from 'braintrust'; const sdk = new NodeSDK({ spanProcessors: [ new BraintrustSpanProcessor({ parent: 'project_name:your-project-name', filterAISpans: true, }), ], }); sdk.start(); async function main() { const result = await generateText({ model: openai('gpt-4o-mini'), messages: [ { role: 'user', content: 'What are my orders and where are they? My user ID is 123', }, ], tools: { listOrders: tool({ description: 'list all orders', parameters: z.object({ userId: z.string() }), execute: async ({ userId }) => `User ${userId} has the following orders: 1`, }), viewTrackingInformation: tool({ description: 'view tracking information for a specific order', parameters: z.object({ orderId: z.string() }), execute: async ({ orderId }) => `Here is the tracking information for ${orderId}`, }), }, experimental_telemetry: { isEnabled: true, functionId: 'my-awesome-function', metadata: { something: 'custom', someOtherThing: 'other-value', }, }, maxSteps: 10, }); await sdk.shutdown(); } main().catch(console.error); ``` ## Resources To see a step-by-step example, check out the Braintrust [cookbook](https://www.braintrust.dev/docs/cookbook/recipes/OTEL-logging). After you log your application in Braintrust, explore other workflows like: - Adding [tools](https://www.braintrust.dev/docs/guides/functions/tools) to your library and using them in [experiments](https://www.braintrust.dev/docs/guides/evals) and the [playground](https://www.braintrust.dev/docs/guides/playground) - Creating [custom scorers](https://www.braintrust.dev/docs/guides/functions/scorers) to assess the quality of your LLM calls - Adding your logs to a [dataset](https://www.braintrust.dev/docs/guides/datasets) and running evaluations comparing models and prompts --- File: /ai/content/providers/05-observability/helicone.mdx --- --- title: Helicone description: Monitor and optimize your AI SDK applications with minimal configuration using Helicone --- # Helicone Observability [Helicone](https://helicone.ai) is an open-source LLM observability platform that helps you monitor, analyze, and optimize your AI applications through a proxy-based approach, requiring minimal setup and zero additional dependencies. ## Setup Setting up Helicone: 1. Create a Helicone account at [helicone.ai](https://helicone.ai) 2. Set your API key as an environment variable: ```bash filename=".env" HELICONE_API_KEY=your-helicone-api-key ``` 3. Update your model provider configuration to use Helicone's proxy: ```javascript import { createOpenAI } from '@ai-sdk/openai'; const openai = createOpenAI({ baseURL: 'https://oai.helicone.ai/v1', headers: { 'Helicone-Auth': `Bearer ${process.env.HELICONE_API_KEY}`, }, }); // Use normally with AI SDK const response = await generateText({ model: openai('gpt-4o-mini'), prompt: 'Hello world', }); ``` That's it! Your requests are now being logged and monitored through Helicone. [→ Learn more about getting started with Helicone on AI SDK](https://docs.helicone.ai/getting-started/integration-method/vercelai) ## Integration Approach While other observability solutions require OpenTelemetry instrumentation, Helicone uses a simple proxy approach: <Tabs items={['Helicone Proxy (3 lines)', 'Typical OTEL Setup (simplified)']}> <Tab> ```javascript const openai = createOpenAI({ baseURL: "https://oai.helicone.ai/v1", headers: { "Helicone-Auth": `Bearer ${process.env.HELICONE_API_KEY}` }, }); ``` </Tab> <Tab> ```javascript // Install multiple packages // @vercel/otel, @opentelemetry/sdk-node, @opentelemetry/auto-instrumentations-node, etc. // Create exporter const exporter = new OtherProviderExporter({ projectApiKey: process.env.API_KEY }); // Setup SDK const sdk = new NodeSDK({ traceExporter: exporter, instrumentations: [getNodeAutoInstrumentations()], resource: new Resource({...}), }); // Start SDK sdk.start(); // Enable telemetry on each request const response = await generateText({ model: openai("gpt-4o-mini"), prompt: "Hello world", experimental_telemetry: { isEnabled: true } }); // Shutdown SDK to flush traces await sdk.shutdown(); ``` </Tab> </Tabs> **Characteristics of Helicone's Proxy Approach:** - No additional packages required - Compatible with JavaScript environments - Minimal code changes to existing implementations - Supports features such as caching and rate limiting [→ Learn more about Helicone's proxy approach](https://docs.helicone.ai/references/proxy-vs-async) ## Core Features ### User Tracking Monitor how individual users interact with your AI application: ```javascript const response = await generateText({ model: openai('gpt-4o-mini'), prompt: 'Hello world', headers: { 'Helicone-User-Id': 'user@example.com', }, }); ``` [→ Learn more about User Metrics](https://docs.helicone.ai/features/advanced-usage/user-metrics) ### Custom Properties Add structured metadata to filter and analyze requests: ```javascript const response = await generateText({ model: openai('gpt-4o-mini'), prompt: 'Translate this text to French', headers: { 'Helicone-Property-Feature': 'translation', 'Helicone-Property-Source': 'mobile-app', 'Helicone-Property-Language': 'French', }, }); ``` [→ Learn more about Custom Properties](https://docs.helicone.ai/features/advanced-usage/custom-properties) ### Session Tracking Group related requests into coherent conversations: ```javascript const response = await generateText({ model: openai('gpt-4o-mini'), prompt: 'Tell me more about that', headers: { 'Helicone-Session-Id': 'convo-123', 'Helicone-Session-Name': 'Travel Planning', 'Helicone-Session-Path': '/chats/travel', }, }); ``` [→ Learn more about Sessions](https://docs.helicone.ai/features/sessions) ## Advanced Configuration ### Request Caching Reduce costs by caching identical requests: ```javascript const response = await generateText({ model: openai('gpt-4o-mini'), prompt: 'What is the capital of France?', headers: { 'Helicone-Cache-Enabled': 'true', }, }); ``` [→ Learn more about Caching](https://docs.helicone.ai/features/advanced-usage/caching) ### Rate Limiting Control usage by adding a rate limit policy: ```javascript const response = await generateText({ model: openai('gpt-4o-mini'), prompt: 'Generate creative content', headers: { // Allow 10,000 requests per hour 'Helicone-RateLimit-Policy': '10000;w=3600', // Optional: limit by user 'Helicone-User-Id': 'user@example.com', }, }); ``` Format: `[quota];w=[time_window];u=[unit];s=[segment]` where: - `quota`: Maximum requests allowed in the time window - `w`: Time window in seconds (minimum 60s) - `u`: Optional unit - "request" (default) or "cents" - `s`: Optional segment - "user", custom property, or global (default) [→ Learn more about Rate Limiting](https://docs.helicone.ai/features/advanced-usage/custom-rate-limits) ### LLM Security Protect against prompt injection, jailbreaking, and other LLM-specific threats: ```javascript const response = await generateText({ model: openai('gpt-4o-mini'), prompt: userInput, headers: { // Basic protection (Prompt Guard model) 'Helicone-LLM-Security-Enabled': 'true', // Optional: Advanced protection (Llama Guard model) 'Helicone-LLM-Security-Advanced': 'true', }, }); ``` Protects against multiple attack vectors in 8 languages with minimal latency. Advanced mode adds protection across 14 threat categories. [→ Learn more about LLM Security](https://docs.helicone.ai/features/advanced-usage/llm-security) ## Resources - [Helicone Documentation](https://docs.helicone.ai) - [GitHub Repository](https://github.com/Helicone/helicone) - [Discord Community](https://discord.com/invite/2TkeWdXNPQ) --- File: /ai/content/providers/05-observability/index.mdx --- --- title: Observability Integrations description: AI SDK Integration for monitoring and tracing LLM applications --- # Observability Integrations Several LLM observability providers offer integrations with the AI SDK telemetry data: - [Braintrust](/providers/observability/braintrust) - [Helicone](/providers/observability/helicone) - [Traceloop](/providers/observability/traceloop) - [Weave](/providers/observability/weave) - [Langfuse](/providers/observability/langfuse) - [LangSmith](/providers/observability/langsmith) - [Laminar](/providers/observability/laminar) - [LangWatch](/providers/observability/langwatch) - [Maxim](/providers/observability/maxim) - [HoneyHive](https://docs.honeyhive.ai/integrations/vercel) - [SigNoz](/providers/observability/signoz) There are also providers that provide monitoring and tracing for the AI SDK through model wrappers: - [Literal AI](https://docs.literalai.com/integrations/vercel-ai-sdk) <Note> Do you have an observability integration that supports the AI SDK and has an integration guide? Please open a pull request to add it to the list. </Note> --- File: /ai/content/providers/05-observability/laminar.mdx --- --- title: Laminar description: Monitor your AI SDK applications with Laminar --- # Laminar observability [Laminar](https://www.lmnr.ai) is the open-source platform for tracing and evaluating AI applications. Laminar features: - [Tracing compatible with AI SDK and more](https://docs.lmnr.ai/tracing/introduction), - [Evaluations](https://docs.lmnr.ai/evaluations/introduction), - [Browser agent observability](https://docs.lmnr.ai/tracing/browser-agent-observability) <Note> A version of this guide is available in [Laminar's docs](https://docs.lmnr.ai/tracing/integrations/vercel-ai-sdk). </Note> ## Setup Laminar's tracing is based on OpenTelemetry. It supports AI SDK [telemetry](/docs/ai-sdk-core/telemetry). ### Installation To start with Laminar's tracing, first [install](https://docs.lmnr.ai/installation) the `@lmnr-ai/lmnr` package. <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @lmnr-ai/lmnr" dark /> </Tab> <Tab> <Snippet text="npm install @lmnr-ai/lmnr" dark /> </Tab> <Tab> <Snippet text="yarn add @lmnr-ai/lmnr" dark /> </Tab> </Tabs> ### Get your project API key and set in the environment Then, either sign up on [Laminar](https://www.lmnr.ai) or self-host an instance ([github](https://github.com/lmnr-ai/lmnr)) and create a new project. In the project settings, create and copy the API key. In your .env ```bash LMNR_PROJECT_API_KEY=... ``` ## Next.js ### Initialize tracing In Next.js, Laminar initialization should be done in `instrumentation.{ts,js}`: ```javascript export async function register() { // prevent this from running in the edge runtime if (process.env.NEXT_RUNTIME === 'nodejs') { const { Laminar } = await import('@lmnr-ai/lmnr'); Laminar.initialize({ projectApiKey: process.env.LMNR_API_KEY, }); } } ``` ### Add @lmnr-ai/lmnr to your next.config In your `next.config.js` (`.ts` / `.mjs`), add the following lines: ```javascript const nextConfig = { serverExternalPackages: ['@lmnr-ai/lmnr'], }; export default nextConfig; ``` This is because Laminar depends on OpenTelemetry, which uses some Node.js-specific functionality, and we need to inform Next.js about it. Learn more in the [Next.js docs](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages). ### Tracing AI SDK calls Then, when you call AI SDK functions in any of your API routes, add the Laminar tracer to the `experimental_telemetry` option. ```javascript highlight="3,8-11" import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import { getTracer } from '@lmnr-ai/lmnr'; const { text } = await generateText({ model: openai('gpt-4o-mini'), prompt: 'What is Laminar flow?', experimental_telemetry: { isEnabled: true, tracer: getTracer(), }, }); ``` This will create spans for `ai.generateText`. Laminar collects and displays the following information: - LLM call input and output - Start and end time - Duration / latency - Provider and model used - Input and output tokens - Input and output price - Additional metadata and span attributes ### Older versions of Next.js If you are using 13.4 ≤ Next.js < 15, you will also need to enable the experimental instrumentation hook. Place the following in your `next.config.js`: ```javascript module.exports = { experimental: { instrumentationHook: true, }, }; ``` For more information, see Laminar's [Next.js guide](https://docs.lmnr.ai/tracing/nextjs) and Next.js [instrumentation docs](https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation). You can also learn how to enable all traces for Next.js in the docs. ### Usage with `@vercel/otel` Laminar can live alongside `@vercel/otel` and trace AI SDK calls. The default Laminar setup will ensure that - regular Next.js traces are sent via `@vercel/otel` to your Telemetry backend configured with Vercel, - AI SDK and other LLM or browser agent traces are sent via Laminar. ```javascript import { registerOTel } from '@vercel/otel'; export async function register() { registerOTel('my-service-name'); if (process.env.NEXT_RUNTIME === 'nodejs') { const { Laminar } = await import('@lmnr-ai/lmnr'); // Make sure to initialize Laminar **after** `@registerOTel` Laminar.initialize({ projectApiKey: process.env.LMNR_PROJECT_API_KEY, }); } } ``` For an advanced configuration that allows you to trace all Next.js traces via Laminar, see an example [repo](https://github.com/lmnr-ai/lmnr-ts/tree/main/examples/nextjs). ### Usage with `@sentry/node` Laminar can live alongside `@sentry/node` and trace AI SDK calls. Make sure to initialize Laminar **after** `Sentry.init`. This will ensure that - Whatever is instrumented by Sentry is sent to your Sentry backend, - AI SDK and other LLM or browser agent traces are sent via Laminar. ```javascript export async function register() { if (process.env.NEXT_RUNTIME === 'nodejs') { const Sentry = await import('@sentry/node'); const { Laminar } = await import('@lmnr-ai/lmnr'); Sentry.init({ dsn: process.env.SENTRY_DSN, }); // Make sure to initialize Laminar **after** `Sentry.init` Laminar.initialize({ projectApiKey: process.env.LMNR_PROJECT_API_KEY, }); } } ``` ## Node.js ### Initialize tracing Then, initialize tracing in your application: ```javascript import { Laminar } from '@lmnr-ai/lmnr'; Laminar.initialize(); ``` This must be done once in your application, as early as possible, but _after_ other tracing libraries (e.g. `@sentry/node`) are initialized. Read more in Laminar [docs](https://docs.lmnr.ai/tracing/introduction). ### Tracing AI SDK calls Then, when you call AI SDK functions in any of your API routes, add the Laminar tracer to the `experimental_telemetry` option. ```javascript highlight="3,8-11" import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import { getTracer } from '@lmnr-ai/lmnr'; const { text } = await generateText({ model: openai('gpt-4o-mini'), prompt: 'What is Laminar flow?', experimental_telemetry: { isEnabled: true, tracer: getTracer(), }, }); ``` This will create spans for `ai.generateText`. Laminar collects and displays the following information: - LLM call input and output - Start and end time - Duration / latency - Provider and model used - Input and output tokens - Input and output price - Additional metadata and span attributes ### Usage with `@sentry/node` Laminar can work with `@sentry/node` to trace AI SDK calls. Make sure to initialize Laminar **after** `Sentry.init`: ```javascript const Sentry = await import('@sentry/node'); const { Laminar } = await import('@lmnr-ai/lmnr'); Sentry.init({ dsn: process.env.SENTRY_DSN, }); Laminar.initialize({ projectApiKey: process.env.LMNR_PROJECT_API_KEY, }); ``` This will ensure that - Whatever is instrumented by Sentry is sent to your Sentry backend, - AI SDK and other LLM or browser agent traces are sent via Laminar. The two libraries allow for additional advanced configuration, but the default setup above is recommended. ## Additional configuration ### Nested spans If you want to trace not just the AI SDK calls, but also other functions in your application, you can use Laminar's `observe` wrapper. ```javascript highlight="3" import { getTracer, observe } from '@lmnr-ai/lmnr'; const result = await observe({ name: 'my-function' }, async () => { // ... some work await generateText({ //... }); // ... some work }); ``` This will create a span with the name "my-function" and trace the function call. Inside it, you will see the nested `ai.generateText` spans. To trace input arguments of the function that you wrap in `observe`, pass them to the wrapper as additional arguments. The return value of the function will be returned from the wrapper and traced as the span's output. ```javascript const result = await observe( { name: 'poem writer' }, async (topic: string, mood: string) => { const { text } = await generateText({ model: openai('gpt-4.1-nano'), prompt: `Write a poem about ${topic} in ${mood} mood.`, }); return text; }, 'Laminar flow', 'happy', ); ``` ### Metadata In Laminar, metadata is set on the trace level. Metadata contains key-value pairs and can be used to filter traces. ```javascript import { getTracer } from '@lmnr-ai/lmnr'; const { text } = await generateText({ model: openai('gpt-4.1-nano'), prompt: `Write a poem about Laminar flow.`, experimental_telemetry: { isEnabled: true, tracer: getTracer(), metadata: { 'my-key': 'my-value', 'another-key': 'another-value', }, }, }); ``` This is converted to Laminar's metadata and stored in the trace. --- File: /ai/content/providers/05-observability/langfuse.mdx --- --- title: Langfuse description: Monitor, evaluate and debug your AI SDK application with Langfuse --- # Langfuse Observability [Langfuse](https://langfuse.com/) ([GitHub](https://github.com/langfuse/langfuse)) is an open source LLM engineering platform that helps teams to collaboratively develop, monitor, and debug AI applications. Langfuse integrates with the AI SDK to provide: - [Application traces](https://langfuse.com/docs/tracing) - Usage patterns - Cost data by user and model - Replay sessions to debug issues - [Evaluations](https://langfuse.com/docs/scores/overview) ## Setup The AI SDK supports tracing via OpenTelemetry. With the `LangfuseExporter` you can collect these traces in Langfuse. While telemetry is experimental ([docs](/docs/ai-sdk-core/telemetry#enabling-telemetry)), you can enable it by setting `experimental_telemetry` on each request that you want to trace. ```ts highlight="4" const result = await generateText({ model: openai('gpt-4o'), prompt: 'Write a short story about a cat.', experimental_telemetry: { isEnabled: true }, }); ``` To collect the traces in Langfuse, you need to add the `LangfuseExporter` to your application. You can set the Langfuse credentials via environment variables or directly to the `LangfuseExporter` constructor. To get your Langfuse API keys, you can [self-host Langfuse](https://langfuse.com/docs/deployment/self-host) or sign up for Langfuse Cloud [here](https://cloud.langfuse.com). Create a project in the Langfuse dashboard to get your `secretKey` and `publicKey.` <Tabs items={["Environment Variables", "Constructor"]}> <Tab> ```bash filename=".env" LANGFUSE_SECRET_KEY="sk-lf-..." LANGFUSE_PUBLIC_KEY="pk-lf-..." LANGFUSE_BASEURL="https://cloud.langfuse.com" # 🇪🇺 EU region, use "https://us.cloud.langfuse.com" for US region ``` </Tab> <Tab> ```ts import { LangfuseExporter } from 'langfuse-vercel'; new LangfuseExporter({ secretKey: 'sk-lf-...', publicKey: 'pk-lf-...', baseUrl: 'https://cloud.langfuse.com', // 🇪🇺 EU region // baseUrl: "https://us.cloud.langfuse.com", // 🇺🇸 US region }); ``` </Tab> </Tabs> Now you need to register this exporter via the OpenTelemetry SDK. <Tabs items={["Next.js","Node.js"]}> <Tab> Next.js has support for OpenTelemetry instrumentation on the framework level. Learn more about it in the [Next.js OpenTelemetry guide](https://nextjs.org/docs/app/building-your-application/optimizing/open-telemetry). Install dependencies: ```bash npm install @vercel/otel langfuse-vercel @opentelemetry/api-logs @opentelemetry/instrumentation @opentelemetry/sdk-logs ``` Add `LangfuseExporter` to your instrumentation: ```ts filename="instrumentation.ts" highlight="7" import { registerOTel } from '@vercel/otel'; import { LangfuseExporter } from 'langfuse-vercel'; export function register() { registerOTel({ serviceName: 'langfuse-vercel-ai-nextjs-example', traceExporter: new LangfuseExporter(), }); } ``` </Tab> <Tab> ```ts highlight="5, 8, 31" import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import { NodeSDK } from '@opentelemetry/sdk-node'; import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; import { LangfuseExporter } from 'langfuse-vercel'; const sdk = new NodeSDK({ traceExporter: new LangfuseExporter(), instrumentations: [getNodeAutoInstrumentations()], }); sdk.start(); async function main() { const result = await generateText({ model: openai('gpt-4o'), maxOutputTokens: 50, prompt: 'Invent a new holiday and describe its traditions.', experimental_telemetry: { isEnabled: true, functionId: 'my-awesome-function', metadata: { something: 'custom', someOtherThing: 'other-value', }, }, }); console.log(result.text); await sdk.shutdown(); // Flushes the trace to Langfuse } main().catch(console.error); ``` </Tab> </Tabs> Done! All traces that contain AI SDK spans are automatically captured in Langfuse. ## Example Application Check out the sample repository ([langfuse/langfuse-vercel-ai-nextjs-example](https://github.com/langfuse/langfuse-vercel-ai-nextjs-example)) based on the [next-openai](https://github.com/vercel/ai/tree/main/examples/next-openai) template to showcase the integration of Langfuse with Next.js and AI SDK. ## Configuration ### Group multiple executions in one trace You can open a Langfuse trace and pass the trace ID to AI SDK calls to group multiple execution spans under one trace. The passed name in `functionId` will be the root span name of the respective execution. ```ts import { randomUUID } from 'crypto'; import { Langfuse } from 'langfuse'; const langfuse = new Langfuse(); const parentTraceId = randomUUID(); langfuse.trace({ id: parentTraceId, name: 'holiday-traditions', }); for (let i = 0; i < 3; i++) { const result = await generateText({ model: openai('gpt-3.5-turbo'), maxOutputTokens: 50, prompt: 'Invent a new holiday and describe its traditions.', experimental_telemetry: { isEnabled: true, functionId: `holiday-tradition-${i}`, metadata: { langfuseTraceId: parentTraceId, langfuseUpdateParent: false, // Do not update the parent trace with execution results }, }, }); console.log(result.text); } await langfuse.flushAsync(); await sdk.shutdown(); ``` The resulting trace hierarchy will be: ![Vercel nested trace in Langfuse UI](https://langfuse.com/images/docs/vercel-nested-trace.png) ### Disable Tracking of Input/Output By default, the exporter captures the input and output of each request. You can disable this behavior by setting the `recordInputs` and `recordOutputs` options to `false`. ### Link Langfuse prompts to traces You can link Langfuse prompts to AI SDK generations by setting the `langfusePrompt` property in the `metadata` field: ```typescript import { generateText } from 'ai'; import { Langfuse } from 'langfuse'; const langfuse = new Langfuse(); const fetchedPrompt = await langfuse.getPrompt('my-prompt'); const result = await generateText({ model: openai('gpt-4o'), prompt: fetchedPrompt.prompt, experimental_telemetry: { isEnabled: true, metadata: { langfusePrompt: fetchedPrompt.toJSON(), }, }, }); ``` The resulting generation will have the prompt linked to the trace in Langfuse. Learn more about prompts in Langfuse [here](https://langfuse.com/docs/prompts/get-started). ### Pass Custom Attributes All of the `metadata` fields are automatically captured by the exporter. You can also pass custom trace attributes to e.g. track users or sessions. ```ts highlight="6-12" const result = await generateText({ model: openai('gpt-4o'), prompt: 'Write a short story about a cat.', experimental_telemetry: { isEnabled: true, functionId: 'my-awesome-function', // Trace name metadata: { langfuseTraceId: 'trace-123', // Langfuse trace tags: ['story', 'cat'], // Custom tags userId: 'user-123', // Langfuse user sessionId: 'session-456', // Langfuse session foo: 'bar', // Any custom attribute recorded in metadata }, }, }); ``` ## Debugging Enable the `debug` option to see the logs of the exporter. ```ts new LangfuseExporter({ debug: true }); ``` ## Troubleshooting - If you deploy on Vercel, Vercel's OpenTelemetry Collector is only available on Pro and Enterprise Plans ([docs](https://vercel.com/docs/observability/otel-overview)). - You need to be on `"ai": "^3.3.0"` to use the telemetry feature. In case of any issues, please update to the latest version. - On NextJS, make sure that you only have a single instrumentation file. - If you use Sentry, make sure to either: - set `skipOpenTelemetrySetup: true` in Sentry.init - follow Sentry's docs on how to manually set up Sentry with OTEL ## Learn more - After setting up Langfuse Tracing for the AI SDK, you can utilize any of the other Langfuse [platform features](https://langfuse.com/docs): - [Prompt Management](https://langfuse.com/docs/prompts): Collaboratively manage and iterate on prompts, use them with low-latency in production. - [Evaluations](https://langfuse.com/docs/scores): Test the application holistically in development and production using user feedback, LLM-as-a-judge evaluators, manual reviews, or custom evaluation pipelines. - [Experiments](https://langfuse.com/docs/datasets): Iterate on prompts, models, and application design in a structured manner with datasets and evaluations. - For more information, see the [telemetry documentation](/docs/ai-sdk-core/telemetry) of the AI SDK. --- File: /ai/content/providers/05-observability/langsmith.mdx --- --- title: LangSmith description: Monitor and evaluate your AI SDK application with LangSmith --- # LangSmith Observability [LangSmith](https://docs.smith.langchain.com) is a platform for building production-grade LLM applications. It allows you to closely monitor and evaluate your application, so you can ship quickly and with confidence. Use of LangChain's open-source frameworks is not necessary. <Note> A version of this guide is also available in the [LangSmith documentation](https://docs.smith.langchain.com/observability/how_to_guides/tracing/trace_with_vercel_ai_sdk). If you are using an older version of the `langsmith` client, see the legacy guide linked from that page. </Note> ## Setup <Note>The steps in this guide assume you are using `langsmith>=0.3.37.`.</Note> Install an [AI SDK model provider](/providers/ai-sdk-providers) and the [LangSmith client SDK](https://npmjs.com/package/langsmith). The code snippets below will use the [AI SDK's OpenAI provider](/providers/ai-sdk-providers/openai), but you can use any [other supported provider](/providers/ai-sdk-providers/) as well. <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @ai-sdk/openai langsmith" dark /> </Tab> <Tab> <Snippet text="npm install @ai-sdk/openai langsmith" dark /> </Tab> <Tab> <Snippet text="yarn add @ai-sdk/openai langsmith" dark /> </Tab> </Tabs> You will also need to install the following OpenTelemetry packages: <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-proto @opentelemetry/context-async-hooks" dark /> </Tab> <Tab> <Snippet text="npm install @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-proto @opentelemetry/context-async-hooks" dark /> </Tab> <Tab> <Snippet text="yarn add @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-proto @opentelemetry/context-async-hooks" dark /> </Tab> </Tabs> Next, set required environment variables. ```bash export LANGCHAIN_TRACING_V2=true export LANGCHAIN_API_KEY=<your-api-key> export OTEL_ENABLED=true export OPENAI_API_KEY=<your-openai-api-key> # The examples use OpenAI (replace with your selected provider) ``` ## Trace Logging To start tracing, you will need to import and call the `initializeOTEL` method at the start of your code: ```ts import { initializeOTEL } from 'langsmith/experimental/otel/setup'; const { DEFAULT_LANGSMITH_SPAN_PROCESSOR } = initializeOTEL(); ``` Afterwards, add the `experimental_telemetry` argument to your AI SDK calls that you want to trace. <Note> Do not forget to call `await DEFAULT_LANGSMITH_SPAN_PROCESSOR.shutdown()` or `.forceFlush()` before your application shuts down in order to flush any remaining traces to LangSmith. </Note> ```ts highlight="14" import { initializeOTEL } from 'langsmith/experimental/otel/setup'; const { DEFAULT_LANGSMITH_SPAN_PROCESSOR } = initializeOTEL(); import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; let result; try { result = await generateText({ model: openai('gpt-4.1-nano'), prompt: 'Write a vegetarian lasagna recipe for 4 people.', experimental_telemetry: { isEnabled: true }, }); } finally { await DEFAULT_LANGSMITH_SPAN_PROCESSOR.shutdown(); } ``` You should see a trace in your LangSmith dashboard [like this one](https://smith.langchain.com/public/21d33490-d522-4928-a944-a09e988d539c/r). You can also trace runs with tool calls: ```ts import { generateText, tool, stepCountIs } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; await generateText({ model: openai('gpt-4.1-nano'), messages: [ { role: 'user', content: 'What are my orders and where are they? My user ID is 123', }, ], tools: { listOrders: tool({ description: 'list all orders', parameters: z.object({ userId: z.string() }), execute: async ({ userId }) => `User ${userId} has the following orders: 1`, }), viewTrackingInformation: tool({ description: 'view tracking information for a specific order', parameters: z.object({ orderId: z.string() }), execute: async ({ orderId }) => `Here is the tracking information for ${orderId}`, }), }, experimental_telemetry: { isEnabled: true, }, stopWhen: stepCountIs(10), }); ``` Which results in a trace like [this one](https://smith.langchain.com/public/e6122734-2762-4ae0-986b-0cbe4d68692f/r). ### With `traceable` You can wrap `traceable` calls around or within AI SDK tool calls. If you do so, we recommend you initialize a LangSmith `client` instance that you pass into each `traceable`, then call `client.awaitPendingTraceBatches();` to ensure all traces flush. If you do this, you do not need to manually call `shutdown()` or `forceFlush()` on the `DEFAULT_LANGSMITH_SPAN_PROCESSOR`. Here's an example: ```ts highlight="40" import { initializeOTEL } from 'langsmith/experimental/otel/setup'; initializeOTEL(); import { Client } from 'langsmith'; import { traceable } from 'langsmith/traceable'; import { generateText, tool } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; const client = new Client(); const wrappedText = traceable( async (content: string) => { const { text } = await generateText({ model: openai('gpt-4.1-nano'), messages: [{ role: 'user', content }], tools: { listOrders: tool({ description: 'list all orders', parameters: z.object({ userId: z.string() }), execute: async ({ userId }) => { const getOrderNumber = traceable( async () => { return '1234'; }, { name: 'getOrderNumber' }, ); const orderNumber = await getOrderNumber(); return `User ${userId} has the following order: ${orderNumber}`; }, }), }, experimental_telemetry: { isEnabled: true, }, maxSteps: 10, }); return { text }; }, { name: 'parentTraceable', client }, ); let result; try { result = await wrappedText('What are my orders?'); } finally { await client.awaitPendingTraceBatches(); } ``` The resulting trace will look [like this](https://smith.langchain.com/public/296a0134-f3d4-4e54-afc7-b18f2c190911/r). ## Further reading For more examples and instructions for setting up tracing in specific environments, see the links below: - [LangSmith docs](https://docs.smith.langchain.com) - [LangSmith guide on tracing with the AI SDK](https://docs.smith.langchain.com/observability/how_to_guides/tracing/trace_with_vercel_ai_sdk) And once you've set up LangSmith tracing for your project, try gathering a dataset and evaluating it: - [LangSmith evaluation](https://docs.smith.langchain.com/evaluation) --- File: /ai/content/providers/05-observability/langwatch.mdx --- --- title: LangWatch description: Track, monitor, guardrail and evaluate your AI SDK applications with LangWatch. --- # LangWatch Observability [LangWatch](https://langwatch.ai/) ([GitHub](https://github.com/langwatch/langwatch)) is an LLM Ops platform for monitoring, experimenting, measuring and improving LLM pipelines, with a fair-code distribution model. ## Setup Obtain your `LANGWATCH_API_KEY` from the [LangWatch dashboard](https://app.langwatch.com/). <Tabs items={['pnpm', 'npm', 'yarn']}> <Tab> <Snippet text="pnpm add langwatch" dark /> </Tab> <Tab> <Snippet text="npm install langwatch" dark /> </Tab> <Tab> <Snippet text="yarn add langwatch" dark /> </Tab> </Tabs> Ensure `LANGWATCH_API_KEY` is set: <Tabs items={["Environment variables", "Client parameters"]} > <Tab title="Environment variable"> ```bash filename=".env" LANGWATCH_API_KEY='your_api_key_here' ``` </Tab> <Tab title="Client parameters"> ```typescript import { LangWatch } from 'langwatch'; const langwatch = new LangWatch({ apiKey: 'your_api_key_here', }); ``` </Tab> </Tabs> ## Basic Concepts - Each message triggering your LLM pipeline as a whole is captured with a [Trace](https://docs.langwatch.ai/concepts#traces). - A [Trace](https://docs.langwatch.ai/concepts#traces) contains multiple [Spans](https://docs.langwatch.ai/concepts#spans), which are the steps inside your pipeline. - A span can be an LLM call, a database query for a RAG retrieval, or a simple function transformation. - Different types of [Spans](https://docs.langwatch.ai/concepts#spans) capture different parameters. - [Spans](https://docs.langwatch.ai/concepts#spans) can be nested to capture the pipeline structure. - [Traces](https://docs.langwatch.ai/concepts#traces) can be grouped together on LangWatch Dashboard by having the same [`thread_id`](https://docs.langwatch.ai/concepts#threads) in their metadata, making the individual messages become part of a conversation. - It is also recommended to provide the [`user_id`](https://docs.langwatch.ai/concepts#user-id) metadata to track user analytics. ## Configuration The AI SDK supports tracing via Next.js OpenTelemetry integration. By using the `LangWatchExporter`, you can automatically collect those traces to LangWatch. First, you need to install the necessary dependencies: ```bash npm install @vercel/otel langwatch @opentelemetry/api-logs @opentelemetry/instrumentation @opentelemetry/sdk-logs ``` Then, set up the OpenTelemetry for your application, follow one of the tabs below depending whether you are using AI SDK with Next.js or on Node.js: <Tabs items={['Next.js', 'Node.js']}> <Tab title="Next.js"> You need to enable the `instrumentationHook` in your `next.config.js` file if you haven't already: ```javascript /** @type {import('next').NextConfig} */ const nextConfig = { experimental: { instrumentationHook: true, }, }; module.exports = nextConfig; ``` Next, you need to create a file named `instrumentation.ts` (or `.js`) in the **root directory** of the project (or inside `src` folder if using one), with `LangWatchExporter` as the traceExporter: ```typescript import { registerOTel } from '@vercel/otel'; import { LangWatchExporter } from 'langwatch'; export function register() { registerOTel({ serviceName: 'next-app', traceExporter: new LangWatchExporter(), }); } ``` (Read more about Next.js OpenTelemetry configuration [on the official guide](https://nextjs.org/docs/app/building-your-application/optimizing/open-telemetry#manual-opentelemetry-configuration)) Finally, enable `experimental_telemetry` tracking on the AI SDK calls you want to trace: ```typescript const result = await generateText({ model: openai('gpt-4o-mini'), prompt: 'Explain why a chicken would make a terrible astronaut, be creative and humorous about it.', experimental_telemetry: { isEnabled: true, // optional metadata metadata: { userId: 'myuser-123', threadId: 'mythread-123', }, }, }); ``` </Tab> <Tab title="Node.js"> For Node.js, start by following the official OpenTelemetry guide: - [OpenTelemetry Node.js Getting Started](https://opentelemetry.io/docs/languages/js/getting-started/nodejs/) Once you have set up OpenTelemetry, you can use the `LangWatchExporter` to automatically send your traces to LangWatch: ```typescript import { LangWatchExporter } from 'langwatch'; const sdk = new NodeSDK({ traceExporter: new LangWatchExporter({ apiKey: process.env.LANGWATCH_API_KEY, }), // ... }); ``` </Tab> </Tabs> That's it! Your messages will now be visible on LangWatch: ![AI SDK](https://mintlify.s3.us-west-1.amazonaws.com/langwatch/images/integration/vercel-ai-sdk.png) ### Example Project You can find a full example project with a more complex pipeline and AI SDK and LangWatch integration [on our GitHub](https://github.com/langwatch/langwatch/blob/main/typescript-sdk/example/lib/chat/vercel-ai.tsx). ### Manual Integration The docs from here below are for manual integration, in case you are not using the AI SDK OpenTelemetry integration, you can manually start a trace to capture your messages: ```typescript import { LangWatch } from 'langwatch'; const langwatch = new LangWatch(); const trace = langwatch.getTrace({ metadata: { threadId: 'mythread-123', userId: 'myuser-123' }, }); ``` Then, you can start an LLM span inside the trace with the input about to be sent to the LLM. ```typescript const span = trace.startLLMSpan({ name: 'llm', model: model, input: { type: 'chat_messages', value: messages, }, }); ``` This will capture the LLM input and register the time the call started. Once the LLM call is done, end the span to get the finish timestamp to be registered, and capture the output and the token metrics, which will be used for cost calculation, e.g.: ```typescript span.end({ output: { type: 'chat_messages', value: [chatCompletion.choices[0]!.message], }, metrics: { promptTokens: chatCompletion.usage?.prompt_tokens, completionTokens: chatCompletion.usage?.completion_tokens, }, }); ``` ## Resources For more information and examples, you can read more below: - [LangWatch documentation](https://docs.langwatch.ai/) - [LangWatch GitHub](https://github.com/langwatch/langwatch) ## Support If you have questions or need help, join our community: - [LangWatch Discord](https://discord.gg/kT4PhDS2gH) - [Email support](mailto:support@langwatch.ai) --- File: /ai/content/providers/05-observability/maxim.mdx --- --- title: Maxim description: Evaluate & Observe LLM applications with Maxim --- # Maxim Observability [Maxim AI](https://getmaxim.ai) streamlines AI application development and deployment by applying traditional software best practices to non-deterministic AI workflows. Our evaluation and observability tools help teams maintain quality, reliability, and speed throughout the AI application lifecycle. Maxim integrates with the AI SDK to provide: - Automatic Observability – Adds tracing, logging, and metadata to AI SDK calls with a simple wrapper. - Unified Model Wrapping – Supports OpenAI, Anthropic, and Google etc. models uniformly. - Custom Metadata & Tagging – Enables attaching trace names, tags, and session IDs to track usage. - Streaming & Structured Output Support – Handles streaming responses and structured outputs seamlessly. # Setting up Maxim with the AI SDK ## Requirements ``` "ai" "@ai-sdk/openai" "@ai-sdk/anthropic" "@ai-sdk/google" "@maximai/maxim-js" ``` ## Environment Variables ``` MAXIM_API_KEY= MAXIM_LOG_REPO_ID= OPENAI_API_KEY= ANTHROPIC_API_KEY= ``` ## Initialize Logger ```javascript import { Maxim } from '@maximai/maxim-js'; async function initializeMaxim() { const apiKey = process.env.MAXIM_API_KEY || ''; if (!apiKey) { throw new Error( 'MAXIM_API_KEY is not defined in the environment variables', ); } const maxim = new Maxim({ apiKey }); const logger = await maxim.logger({ id: process.env.MAXIM_LOG_REPO_ID || '', }); if (!logger) { throw new Error('Logger is not available'); } return { maxim, logger }; } ``` ## Wrap AI SDK Models with Maxim ```javascript import { openai } from '@ai-sdk/openai'; import { wrapMaximAISDKModel } from '@maximai/maxim-js/vercel-ai-sdk'; const model = wrapMaximAISDKModel(openai('gpt-4'), logger); ``` ## Make LLM calls using wrapped models ```javascript import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; import { wrapMaximAISDKModel } from '@maximai/maxim-js/vercel-ai-sdk'; const model = wrapMaximAISDKModel(openai('gpt-4'), logger); // Generate text with automatic logging const response = await generateText({ model: model, prompt: 'Write a haiku about recursion in programming.', temperature: 0.8, system: 'You are a helpful assistant.', }); console.log('Response:', response.text); ``` ## Working with Different AI SDK Functions The wrapped model works seamlessly with all Vercel AI SDK functions: ### **Generate Object** ```javascript import { generateObject } from 'ai'; import { z } from 'zod'; const response = await generateObject({ model: model, prompt: 'Generate a user profile for John Doe', schema: z.object({ name: z.string(), age: z.number(), email: z.string().email(), interests: z.array(z.string()), }), }); console.log(response.object); ``` ### **Stream Text** ```javascript import { streamText } from 'ai'; const { textStream } = await streamText({ model: model, prompt: 'Write a short story about space exploration', system: 'You are a creative writer', }); for await (const textPart of textStream) { process.stdout.write(textPart); } ``` ## Custom Metadata and Tracing ### **Using Custom Metadata** ```javascript import { MaximVercelProviderMetadata } from '@maximai/maxim-js/vercel-ai-sdk'; const response = await generateText({ model: model, prompt: 'Hello, how are you?', providerOptions: { maxim: { traceName: 'custom-trace-name', traceTags: { type: 'demo', priority: 'high', }, } as MaximVercelProviderMetadata, }, }); ``` ### **Available Metadata Fields** **Entity Naming:** - `sessionName` - Override the default session name - `traceName` - Override the default trace name - `spanName` - Override the default span name - `generationName` - Override the default LLM generation name **Entity Tagging:** - `sessionTags` - Add custom tags to the session `(object: {key: value})` - `traceTags` - Add custom tags to the trace `(object: {key: value})` - `spanTags` - Add custom tags to span `(object: {key: value})` - `generationTags` - Add custom tags to LLM generations `(object: {key: value})` **ID References:** - `sessionId` - Link this trace to an existing session - `traceId` - Use a specific trace ID - `spanId` - Use a specific span ID ![Maxim Demo](https://cdn.getmaxim.ai/public/images/maxim_vercel.gif) ## Streaming Support ```javascript import { streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; import { wrapMaximAISDKModel, MaximVercelProviderMetadata } from '@maximai/maxim-js/vercel-ai-sdk'; const model = wrapMaximAISDKModel(openai('gpt-4'), logger); const { textStream } = await streamText({ model: model, prompt: 'Write a story about a robot learning to paint.', system: 'You are a creative storyteller', providerOptions: { maxim: { traceName: 'Story Generation', traceTags: { type: 'creative', format: 'streaming' }, } as MaximVercelProviderMetadata, }, }); for await (const textPart of textStream) { process.stdout.write(textPart); } ``` ## Multiple Provider Support ```javascript import { openai } from '@ai-sdk/openai'; import { anthropic } from '@ai-sdk/anthropic'; import { google } from '@ai-sdk/google'; import { wrapMaximAISDKModel } from '@maximai/maxim-js/vercel-ai-sdk'; // Wrap different provider models const openaiModel = wrapMaximAISDKModel(openai('gpt-4'), logger); const anthropicModel = wrapMaximAISDKModel( anthropic('claude-3-5-sonnet-20241022'), logger, ); const googleModel = wrapMaximAISDKModel(google('gemini-pro'), logger); // Use them with the same interface const responses = await Promise.all([ generateText({ model: openaiModel, prompt: 'Hello from OpenAI' }), generateText({ model: anthropicModel, prompt: 'Hello from Anthropic' }), generateText({ model: googleModel, prompt: 'Hello from Google' }), ]); ``` ## Next.js Integration ### **API Route Example** ```javascript // app/api/chat/route.js import { streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; import { wrapMaximAISDKModel, MaximVercelProviderMetadata } from '@maximai/maxim-js/vercel-ai-sdk'; import { Maxim } from "@maximai/maxim-js"; const maxim = new Maxim({ apiKey }); const logger = await maxim.logger({ id: process.env.MAXIM_LOG_REPO_ID }); const model = wrapMaximAISDKModel(openai('gpt-4'), logger); export async function POST(req) { const { messages } = await req.json(); const result = await streamText({ model: model, messages, system: 'You are a helpful assistant', providerOptions: { maxim: { traceName: 'Chat API', traceTags: { endpoint: '/api/chat', type: 'conversation' }, } as MaximVercelProviderMetadata, }, }); return result.toAIStreamResponse(); } ``` ### **Client-side Integration** ```javascript // components/Chat.jsx import { useChat } from 'ai/react'; export default function Chat() { const { messages, input, handleInputChange, handleSubmit } = useChat({ api: '/api/chat', }); return ( <div> {messages.map(m => ( <div key={m.id}> <strong>{m.role}:</strong> {m.content} </div> ))} <form onSubmit={handleSubmit}> <input value={input} onChange={handleInputChange} placeholder="Say something..." /> <button type="submit">Send</button> </form> </div> ); } ``` ## Learn more - After setting up Maxim tracing for the Vercel AI SDK, you can explore other Maxim platform capabilities: - Prompt Management: Version, manage, and dynamically apply prompts across environments and agents. - Evaluations: Run automated and manual evaluations on traces, generations, and full agent trajectories. - Simulations: Test agents in real-world scenarios with simulated multi-turn interactions and workflows. For further details, checkout Vercel AI SDK's [Maxim integration documentation](https://www.getmaxim.ai/docs/sdk/typescript/integrations/vercel/vercel). --- File: /ai/content/providers/05-observability/patronus.mdx --- --- title: Patronus description: Monitor, evaluate and debug your AI SDK application with Patronus --- # Patronus Observability [Patronus AI](https://patronus.ai) provides an end-to-end system to evaluate, monitor and improve performance of an LLM system, enabling developers to ship AI products safely and confidently. Learn more [here](https://docs.patronus.ai/docs). When you build with the AI SDK, you can stream OpenTelemetry (OTEL) traces straight into Patronus and pair every generation with rich automatic evaluations. ## Setup ### 1. OpenTelemetry Patronus exposes a fully‑managed OTEL endpoint. Configure an **OTLP exporter** to point at it, pass your API key, and you’re done—Patronus will automatically convert LLM spans into prompt/response records you can explore and evaluate. #### Environment variables (recommended) ```bash filename=".env.local" OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.patronus.ai/v1/traces OTEL_EXPORTER_OTLP_HEADERS="x-api-key:<PATRONUS_API_KEY>" ``` #### With `@vercel/otel` ```ts filename="instrumentation.ts" import { registerOTel } from '@vercel/otel'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'; export function register() { registerOTel({ serviceName: 'next-app', additionalSpanProcessors: [ new BatchSpanProcessor( new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, headers: { 'x-api-key': process.env.PATRONUS_API_KEY!, }, }), ), ], }); } ``` <Note> If you need gRPC instead of HTTP, swap the exporter for `@opentelemetry/exporter-trace-otlp-grpc` and use `https://otel.patronus.ai:4317`. </Note> ### 2. Enable telemetry on individual calls The AI SDK emits a span only when you opt in with `experimental_telemetry`: ```ts import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = await generateText({ model: openai('gpt-4o'), prompt: 'Write a haiku about spring.', experimental_telemetry: { isEnabled: true, functionId: 'spring-haiku', // span name metadata: { userId: 'user-123', // custom attrs surface in Patronus UI }, }, }); ``` Every attribute inside `metadata` becomes an OTEL attribute and is indexed by Patronus for filtering. ## Example — tracing and automated evaluation ```ts filename="app/api/chat/route.ts" import { trace } from '@opentelemetry/api'; import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; export async function POST(req: Request) { const body = await req.json(); const tracer = trace.getTracer('next-app'); return await tracer.startActiveSpan('chat-evaluate', async span => { try { /* 1️⃣ generate answer */ const answer = await generateText({ model: openai('gpt-4o'), prompt: body.prompt, experimental_telemetry: { isEnabled: true, functionId: 'chat' }, }); /* 2️⃣ run Patronus evaluation inside the same trace */ await fetch('https://api.patronus.ai/v1/evaluate', { method: 'POST', headers: { 'X-API-Key': process.env.PATRONUS_API_KEY!, 'Content-Type': 'application/json', }, body: JSON.stringify({ evaluators: [ { evaluator: 'lynx', criteria: 'patronus:hallucination' }, ], evaluated_model_input: body.prompt, evaluated_model_output: answer.text, trace_id: span.spanContext().traceId, span_id: span.spanContext().spanId, }), }); return new Response(answer.text); } finally { span.end(); } }); } ``` Result: a single trace containing the root HTTP request, the LLM generation span, and your evaluation span—**all visible in Patronus** with the hallucination score attached. ## Once you've traced - If you're tracing an agent, Patronus's AI assistant Percival will assist with error analysis and prompt optimization. Learn more [here](https://docs.patronus.ai/docs/percival/percival) - Get set up on production monitoring and alerting by viewing logs and traces on Patronus and configuring webhooks for alerting. Learn more [here](https://docs.patronus.ai/docs/real_time_monitoring/webhooks) ## Resources - [Patronus docs](https://docs.patronus.ai) - [OpenTelemetry SDK (JS)](https://opentelemetry.io/docs/instrumentation/js/) --- File: /ai/content/providers/05-observability/signoz.mdx --- --- title: SigNoz description: Monitor, obeserve and debug your AI SDK application with SigNoz --- # SigNoz Observability [SigNoz](https://signoz.io/) is a single tool for all your monitoring and observability needs. Here are a few reasons why you should choose SigNoz: - Single tool for observability(logs, metrics, and traces) - Built on top of [OpenTelemetry](https://opentelemetry.io/), the open-source standard which frees you from any type of vendor lock-in - Correlated logs, metrics and traces for much richer context while debugging - Uses ClickHouse (used by likes of Uber & Cloudflare) as datastore - an extremely fast and highly optimized storage for observability data - DIY Query builder, PromQL, and ClickHouse queries to fulfill all your use-cases around querying observability data # Setup - Create a [SigNoz Cloud Account](https://signoz.io/teams/) - Generate a SigNoz Ingestion Key ## Instrument your Next.js application Check out detailed instructions on how to set up OpenTelemetry instrumentation in your Nextjs applications and view your application traces in SigNoz over [here](https://signoz.io/docs/instrumentation/opentelemetry-nextjs/). ## Send traces directly to SigNoz Cloud **Step 1.** Install OpenTelemetry packages ```bash npm install @vercel/otel @opentelemetry/api ``` **Step 2.** Update **`next.config.mjs`** to include instrumentationHook > This step is only needed when using NextJs 14 and below ```jsx /** @type {import('next').NextConfig} */ const nextConfig = { // include instrumentationHook experimental feature experimental: { instrumentationHook: true, }, }; export default nextConfig; ``` **Step 3.** Create **`instrumentation.ts`** file(in root project directory) ```jsx import { registerOTel, OTLPHttpJsonTraceExporter } from '@vercel/otel'; // Add otel logging import { diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api'; diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.ERROR); // set diaglog level to DEBUG when debugging export function register() { registerOTel({ serviceName: '<service_name>', traceExporter: new OTLPHttpJsonTraceExporter({ url: 'https://ingest.<region>.signoz.cloud:443/v1/traces', headers: { 'signoz-ingestion-key': '<your-ingestion-key>' }, }), }); } ``` - **`<service_name>`** is the name of your service - Set the **`<region>`** to match your SigNoz Cloud [**region**](https://signoz.io/docs/ingestion/signoz-cloud/overview/#endpoint) - Replace **`<your-ingestion-key>`** with your SigNoz [**ingestion key**](https://signoz.io/docs/ingestion/signoz-cloud/keys/) > The instrumentation file should be in the root of your project and not inside the app or pages directory. If you're using the src folder, then place the file inside src alongside pages and app. Your Next.js app should be properly instrumented now. ## Enable Telemetry for Vercel AI SDK The Vercel AI SDK uses [OpenTelemetry](https://signoz.io/blog/what-is-opentelemetry/) to collect telemetry data. OpenTelemetry is an open-source observability framework designed to provide standardized instrumentation for collecting telemetry data. ## Enabling Telemetry Check out more detailed information about Vercel AI SDK’s telemetry options visit [here](https://ai-sdk.dev/docs/ai-sdk-core/telemetry#telemetry). You can then use the `experimental_telemetry` option to enable telemetry on specific function calls while the feature is experimental: ```jsx const result = await generateText({ model: openai('gpt-4-turbo'), prompt: 'Write a short story about a cat.', experimental_telemetry: { isEnabled: true }, }); ``` When telemetry is enabled, you can also control whether you want to record the input values and the output values for the function. By default, both are enabled. You can disable them by setting the `recordInputs` and `recordOutputs` options to `false`. ```jsx experimental_telemetry: { isEnabled: true, recordInputs: false, recordOutputs: false} ``` Disabling the recording of inputs and outputs can be useful for privacy, data transfer, and performance reasons. You might, for example, want to disable recording inputs if they contain sensitive information. ## Telemetry Metadata You can provide a `functionId` to identify the function that the telemetry data is for, and `metadata` to include additional information in the telemetry data. ```jsx const result = await generateText({ model: openai('gpt-4-turbo'), prompt: 'Write a short story about a cat.', experimental_telemetry: { isEnabled: true, functionId: 'my-awesome-function', metadata: { something: 'custom', someOtherThing: 'other-value', }, }, }); ``` ## Custom Tracer You may provide a `tracer` which must return an OpenTelemetry `Tracer`. This is useful in situations where you want your traces to use a `TracerProvider` other than the one provided by the `@opentelemetry/api` singleton. ```jsx const tracerProvider = new NodeTracerProvider(); const result = await generateText({ model: openai('gpt-4-turbo'), prompt: 'Write a short story about a cat.', experimental_telemetry: { isEnabled: true, tracer: tracerProvider.getTracer('ai'), }, }); ``` Your Vercel AI SDK commands should now automatically emit traces, spans, and events. You can find more details on the types of spans and events generated [here](https://ai-sdk.dev/docs/ai-sdk-core/telemetry#collected-data). Finally, you should be able to view this data in Signoz Cloud under the traces tab. --- File: /ai/content/providers/05-observability/traceloop.mdx --- --- title: Traceloop description: Monitoring and evaluating LLM applications with Traceloop --- # Traceloop [Traceloop](https://www.traceloop.com/) is a development platform for building reliable AI applications. After integrating with the AI SDK, you can use Traceloop to trace, monitor, and experiment with LLM providers, prompts and flows. ## Setup Traceloop supports [AI SDK telemetry data](/docs/ai-sdk-core/telemetry) through [OpenTelemetry](https://opentelemetry.io/docs/). You'll need to sign up at https://app.traceloop.com and get an API Key. ### Next.js To use the AI SDK to send telemetry data to Traceloop, set these environment variables in your Next.js app's `.env` file: ```bash OTEL_EXPORTER_OTLP_ENDPOINT=https://api.traceloop.com OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <Your API Key>" ``` You can then use the `experimental_telemetry` option to enable telemetry on supported AI SDK function calls: ```typescript highlight="7-13" import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; const result = await generateText({ model: openai('gpt-4o-mini'), prompt: 'What is 2 + 2?', experimental_telemetry: { isEnabled: true, metadata: { query: 'weather', location: 'San Francisco', }, }, }); ``` ## Resources - [Traceloop demo chatbot](https://www.traceloop.com/docs/demo) - [Traceloop docs](https://www.traceloop.com/docs) --- File: /ai/content/providers/05-observability/weave.mdx --- --- title: Weave description: Monitor and evaluate LLM applications with Weave. --- # Weave Observability [Weave](https://wandb.ai/site/weave) is a toolkit built by [Weights & Biases](https://wandb.ai/site/) for tracking, experimenting with, evaluating, deploying, and improving LLM-based applications. After integrating with the AI SDK, you can use Weave to view and interact with trace information for your AI SDK application including prompts, responses, flow, cost and more. ## Setup To set up Weave as an [OpenTelemetry](https://opentelemetry.io/docs/) backend, you'll need to route the traces to Weave's OpenTelemetry endpoint, set your API key, and specify a team and project. In order to log your traces to Weave, you must you must have a [Weights & Biases account](https://wandb.ai/site/weave). ### Authentication First, go to [wandb.ai/authorize](https://wandb.ai/authorize), copy your API key and generate a base64-encoded authorization string by running: ```bash echo -n "api:<YOUR_API_KEY>" | base64 ``` Note the output. You'll use it in your environment configuration. ### Project Configuration Your W&B project ID identifies where your telemetry data will be logged. It follows the format `<YOUR_TEAM_NAME>/<YOUR_PROJECT_NAME>`. 1. Navigate to the [Weights & Biases dashboard](https://wandb.ai/home). 2. In the **Teams** section, select or create a team. 3. Select an existing project or create a new one. 4. Note `<YOUR_TEAM_NAME>/<YOUR_PROJECT_NAME>` for the next step. ### Next.js In your Next.js app’s `.env` file, set the OTEL environment variables. Replace `<BASE64_AUTH_STRING>` and `<YOUR_TEAM_NAME>/<YOUR_PROJECT_NAME>` with your values from the previous steps: ```bash OTEL_EXPORTER_OTLP_ENDPOINT="https://trace.wandb.ai/otel/v1/traces" OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <BASE64_AUTH_STRING>,project_id=<YOUR_TEAM_NAME>/<YOUR_PROJECT_NAME>" ``` You can then use the `experimental_telemetry` option to enable telemetry on supported AI SDK function calls: ```typescript import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; const result = await generateText({ model: openai('gpt-4o-mini'), prompt: 'What is 2 + 2?', experimental_telemetry: { isEnabled: true, metadata: { query: 'math', difficulty: 'easy', }, }, }); ``` ## Resources - [Weave Documentation](https://weave-docs.wandb.ai) - [OpenTelemetry Documentation](https://opentelemetry.io/docs/) - [AI SDK Telemetry Guide](/docs/ai-sdk-core/telemetry) --- File: /ai/examples/ai-core/src/agent/openai-generate.ts --- import { openai } from '@ai-sdk/openai'; import { Experimental_Agent as Agent } from 'ai'; import 'dotenv/config'; async function main() { const agent = new Agent({ model: openai('gpt-4o'), system: 'You are a helpful assistant.', }); const { text, usage } = await agent.generate({ prompt: 'Invent a new holiday and describe its traditions.', }); console.log(text); console.log(); console.log('Usage:', usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/agent/openai-stream-tools.ts --- import { openai } from '@ai-sdk/openai'; import { Experimental_Agent as Agent, stepCountIs, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const agent = new Agent({ model: openai('gpt-3.5-turbo'), system: 'You are a helpful that answers questions about the weather.', tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), }); const result = agent.stream({ prompt: 'What is the weather in Tokyo?', }); 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); --- File: /ai/examples/ai-core/src/agent/openai-stream.ts --- import { openai } from '@ai-sdk/openai'; import { Experimental_Agent as Agent } from 'ai'; import 'dotenv/config'; async function main() { const agent = new Agent({ model: openai('gpt-3.5-turbo'), system: 'You are a helpful assistant.', }); const result = agent.stream({ 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); --- File: /ai/examples/ai-core/src/complex/math-agent/agent-required-tool-choice.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, stepCountIs, tool } from 'ai'; import 'dotenv/config'; import * as mathjs from 'mathjs'; import { z } from 'zod/v4'; async function main() { const { toolCalls } = await generateText({ model: openai('gpt-4o-2024-08-06'), tools: { calculate: tool({ description: 'A tool for evaluating mathematical expressions. Example expressions: ' + "'1.2 * (2 + 4.5)', '12.7 cm to inch', 'sin(45 deg) ^ 2'.", inputSchema: z.object({ expression: z.string() }), execute: async ({ expression }) => mathjs.evaluate(expression), }), // answer tool: the LLM will provide a structured answer answer: tool({ description: 'A tool for providing the final answer.', inputSchema: z.object({ steps: z.array( z.object({ calculation: z.string(), reasoning: z.string(), }), ), answer: z.string(), }), // no execute function - invoking it will terminate the agent }), }, toolChoice: 'required', stopWhen: stepCountIs(10), onStepFinish: async ({ toolResults }) => { console.log(`STEP RESULTS: ${JSON.stringify(toolResults, null, 2)}`); }, system: 'You are solving math problems. ' + 'Reason step by step. ' + 'Use the calculator when necessary. ' + 'The calculator can only do simple additions, subtractions, multiplications, and divisions. ' + 'When you give the final answer, provide an explanation for how you got it.', prompt: 'A taxi driver earns $9461 per 1-hour work. ' + 'If he works 12 hours a day and in 1 hour he uses 14-liters petrol with price $134 for 1-liter. ' + 'How much money does he earn in one day?', }); console.log(`FINAL TOOL CALLS: ${JSON.stringify(toolCalls, null, 2)}`); } main().catch(console.error); --- File: /ai/examples/ai-core/src/complex/math-agent/agent.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, stepCountIs, tool } from 'ai'; import 'dotenv/config'; import * as mathjs from 'mathjs'; import { z } from 'zod/v4'; async function main() { const { text: answer } = await generateText({ model: openai('gpt-4o-2024-08-06'), tools: { calculate: tool({ description: 'A tool for evaluating mathematical expressions. Example expressions: ' + "'1.2 * (2 + 4.5)', '12.7 cm to inch', 'sin(45 deg) ^ 2'.", inputSchema: z.object({ expression: z.string() }), execute: async ({ expression }) => mathjs.evaluate(expression), }), }, stopWhen: stepCountIs(10), onStepFinish: async ({ toolResults }) => { console.log(`STEP RESULTS: ${JSON.stringify(toolResults, null, 2)}`); }, system: 'You are solving math problems. ' + 'Reason step by step. ' + 'Use the calculator when necessary. ' + 'The calculator can only do simple additions, subtractions, multiplications, and divisions. ' + 'When you give the final answer, provide an explanation for how you got it.', prompt: 'A taxi driver earns $9461 per 1-hour work. ' + 'If he works 12 hours a day and in 1 hour he uses 14-liters petrol with price $134 for 1-liter. ' + 'How much money does he earn in one day?', }); console.log(`FINAL ANSWER: ${answer}`); } main().catch(console.error); --- File: /ai/examples/ai-core/src/complex/semantic-router/main.ts --- import { openai } from '@ai-sdk/openai'; import 'dotenv/config'; import { SemanticRouter } from './semantic-router'; async function main() { const router = new SemanticRouter({ embeddingModel: openai.embedding('text-embedding-3-small'), similarityThreshold: 0.2, routes: [ { name: 'sports' as const, values: [ "who's your favorite football team?", 'The World Cup is the most exciting event.', 'I enjoy running marathons on weekends.', ], }, { name: 'music' as const, values: [ "what's your favorite genre of music?", 'Classical music helps me concentrate.', 'I recently attended a jazz festival.', ], }, ], }); // topic is strongly typed const topic = await router.route( 'Many consider Michael Jordan the greatest basketball player ever.', ); switch (topic) { case 'sports': console.log('sports'); break; case 'music': console.log('music'); break; case null: console.log('no topic found'); break; } } main().catch(console.error); --- File: /ai/examples/ai-core/src/complex/semantic-router/semantic-router.ts --- import { Embedding, EmbeddingModel, embed, embedMany, cosineSimilarity, } from 'ai'; export interface Route<NAME extends string> { name: NAME; values: string[]; } /** * Routes values based on their distance to the values from a set of clusters. * When the distance is below a certain threshold, the value is classified as belonging to the route, * and the route name is returned. Otherwise, the value is classified as null. */ export class SemanticRouter<ROUTES extends Array<Route<string>>> { readonly routes: ROUTES; readonly embeddingModel: EmbeddingModel<string>; readonly similarityThreshold: number; private routeValues: | Array<{ routeName: string; routeValue: string; embedding: Embedding }> | undefined; constructor({ routes, embeddingModel, similarityThreshold, }: { routes: ROUTES; embeddingModel: EmbeddingModel<string>; similarityThreshold: number; }) { this.routes = routes; this.embeddingModel = embeddingModel; this.similarityThreshold = similarityThreshold; } private async getRouteValues(): Promise< Array<{ embedding: Embedding; routeValue: string; routeName: string }> > { if (this.routeValues != null) { return this.routeValues; } this.routeValues = []; for (const route of this.routes) { const { embeddings } = await embedMany({ model: this.embeddingModel, values: route.values, }); for (let i = 0; i < embeddings.length; i++) { this.routeValues.push({ routeName: route.name, routeValue: route.values[i], embedding: embeddings[i], }); } } return this.routeValues; } async route(value: string) { const { embedding } = await embed({ model: this.embeddingModel, value }); const routeValues = await this.getRouteValues(); const allMatches: Array<{ similarity: number; routeValue: string; routeName: string; }> = []; for (const routeValue of routeValues) { const similarity = cosineSimilarity(embedding, routeValue.embedding); if (similarity >= this.similarityThreshold) { allMatches.push({ similarity, routeValue: routeValue.routeValue, routeName: routeValue.routeName, }); } } // sort (highest similarity first) allMatches.sort((a, b) => b.similarity - a.similarity); return allMatches.length > 0 ? (allMatches[0].routeName as unknown as RouteNames<ROUTES>) : null; } } type RouteNames<ROUTES> = ROUTES extends Array<Route<infer NAME>> ? NAME : never; --- File: /ai/examples/ai-core/src/e2e/cerebras.test.ts --- import 'dotenv/config'; import { expect } from 'vitest'; import { CerebrasErrorData, cerebras as provider } from '@ai-sdk/cerebras'; import { createFeatureTestSuite, createLanguageModelWithCapabilities, } from './feature-test-suite'; import { APICallError } from '@ai-sdk/provider'; const createChatModel = (modelId: string) => createLanguageModelWithCapabilities(provider.chat(modelId), [ 'objectGeneration', 'textCompletion', 'toolCalls', ]); createFeatureTestSuite({ name: 'Cerebras', models: { invalidModel: provider.chat('no-such-model'), languageModels: [ createChatModel('llama3.1-8b'), createChatModel('llama3.1-70b'), createChatModel('llama-3.3-70b'), ], }, timeout: 30000, customAssertions: { errorValidator: (error: APICallError) => { expect((error.data as CerebrasErrorData).message).toMatch(/not exist/i); }, }, })(); --- File: /ai/examples/ai-core/src/e2e/cohere.test.ts --- import 'dotenv/config'; import { cohere as provider } from '@ai-sdk/cohere'; import { createFeatureTestSuite, createLanguageModelWithCapabilities, } from './feature-test-suite'; const createChatModel = (modelId: string) => createLanguageModelWithCapabilities(provider.languageModel(modelId)); createFeatureTestSuite({ name: 'Cohere', models: { languageModels: [ createChatModel('command-a-03-2025'), createChatModel('command-r-plus'), createChatModel('command-r'), createChatModel('command'), createChatModel('command-light'), ], }, timeout: 30000, })(); --- File: /ai/examples/ai-core/src/e2e/deepinfra.test.ts --- import 'dotenv/config'; import { deepinfra as provider } from '@ai-sdk/deepinfra'; import { createEmbeddingModelWithCapabilities, createFeatureTestSuite, createLanguageModelWithCapabilities, } from './feature-test-suite'; const createChatModel = (modelId: string) => createLanguageModelWithCapabilities(provider.chatModel(modelId)); createFeatureTestSuite({ name: 'DeepInfra', models: { languageModels: [ createChatModel('meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8'), createChatModel('meta-llama/Llama-4-Scout-17B-16E-Instruct'), createChatModel('deepseek-ai/DeepSeek-V3'), createChatModel('deepseek-ai/DeepSeek-R1'), createChatModel('deepseek-ai/DeepSeek-R1-Distill-Llama-70B'), createChatModel('deepseek-ai/DeepSeek-R1-Turbo'), createChatModel('google/codegemma-7b-it'), createChatModel('google/gemma-2-9b-it'), createChatModel('meta-llama/Llama-3.2-11B-Vision-Instruct'), createChatModel('meta-llama/Llama-3.2-90B-Vision-Instruct'), createChatModel('meta-llama/Llama-3.3-70B-Instruct-Turbo'), createChatModel('meta-llama/Llama-3.3-70B-Instruct'), createChatModel('meta-llama/Meta-Llama-3.1-405B-Instruct'), createChatModel('meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo'), createChatModel('meta-llama/Meta-Llama-3.1-70B-Instruct'), createChatModel('meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo'), createChatModel('meta-llama/Meta-Llama-3.1-8B-Instruct'), createChatModel('microsoft/WizardLM-2-8x22B'), createChatModel('mistralai/Mixtral-8x7B-Instruct-v0.1'), createChatModel('nvidia/Llama-3.1-Nemotron-70B-Instruct'), createChatModel('Qwen/Qwen2-7B-Instruct'), createChatModel('Qwen/Qwen2.5-72B-Instruct'), createChatModel('Qwen/Qwen2.5-Coder-32B-Instruct'), createChatModel('Qwen/QwQ-32B-Preview'), ], embeddingModels: [ createEmbeddingModelWithCapabilities( provider.textEmbeddingModel('BAAI/bge-base-en-v1.5'), ), createEmbeddingModelWithCapabilities( provider.textEmbeddingModel('intfloat/e5-base-v2'), ), createEmbeddingModelWithCapabilities( provider.textEmbeddingModel('sentence-transformers/all-mpnet-base-v2'), ), ], }, timeout: 60000, })(); --- File: /ai/examples/ai-core/src/e2e/deepseek.test.ts --- import 'dotenv/config'; import { expect } from 'vitest'; import { deepseek as provider } from '@ai-sdk/deepseek'; import { APICallError } from 'ai'; import { createFeatureTestSuite, createLanguageModelWithCapabilities, } from './feature-test-suite'; import { DeepSeekErrorData } from '@ai-sdk/deepseek'; const createChatModel = (modelId: string) => createLanguageModelWithCapabilities(provider.chat(modelId)); createFeatureTestSuite({ name: 'DeepSeek', models: { invalidModel: provider.chat('no-such-model'), languageModels: [createChatModel('deepseek-chat')], }, timeout: 10000, customAssertions: { errorValidator: (error: APICallError) => { expect( (error.data as DeepSeekErrorData).error.message === 'Model Not Exist', ).toBe(true); }, }, })(); --- File: /ai/examples/ai-core/src/e2e/feature-test-suite.ts --- import type { GoogleGenerativeAIProviderMetadata } from '@ai-sdk/google'; import type { EmbeddingModelV2, ImageModelV2, LanguageModelV2, } from '@ai-sdk/provider'; import { APICallError, embed, embedMany, experimental_generateImage as generateImage, generateObject, generateText, stepCountIs, streamObject, streamText, } from 'ai'; import fs from 'fs'; import { describe, expect, it, vi } from 'vitest'; import { z } from 'zod/v4'; export type Capability = | 'audioInput' | 'embedding' | 'imageGeneration' | 'imageInput' | 'objectGeneration' | 'pdfInput' | 'searchGrounding' | 'textCompletion' | 'toolCalls'; export type ModelCapabilities = Capability[]; export interface ModelWithCapabilities<T> { model: T; capabilities?: ModelCapabilities; } export const defaultChatModelCapabilities: ModelCapabilities = [ // audioInput is not supported by most language models. // embedding is not supported by language models. // imageGeneration is not supported by language models. 'imageInput', 'objectGeneration', 'pdfInput', // searchGrounding is not supported by most language models. 'textCompletion', 'toolCalls', ]; export const createLanguageModelWithCapabilities = ( model: LanguageModelV2, capabilities: ModelCapabilities = defaultChatModelCapabilities, ): ModelWithCapabilities<LanguageModelV2> => ({ model, capabilities, }); export const createEmbeddingModelWithCapabilities = ( model: EmbeddingModelV2<string>, capabilities: ModelCapabilities = ['embedding'], ): ModelWithCapabilities<EmbeddingModelV2<string>> => ({ model, capabilities, }); export const createImageModelWithCapabilities = ( model: ImageModelV2, capabilities: ModelCapabilities = ['imageGeneration'], ): ModelWithCapabilities<ImageModelV2> => ({ model, capabilities, }); export interface ModelVariants { invalidModel?: LanguageModelV2; languageModels?: ModelWithCapabilities<LanguageModelV2>[]; embeddingModels?: ModelWithCapabilities<EmbeddingModelV2<string>>[]; invalidImageModel?: ImageModelV2; imageModels?: ModelWithCapabilities<ImageModelV2>[]; } export interface TestSuiteOptions { name: string; models: ModelVariants; timeout?: number; customAssertions?: { skipUsage?: boolean; errorValidator?: (error: APICallError) => void; }; } const createModelObjects = <T extends { modelId: string }>( models: ModelWithCapabilities<T>[] | undefined, ) => models?.map(({ model, capabilities }) => ({ modelId: model.modelId, model, capabilities, })) || []; const verifyGroundingMetadata = (groundingMetadata: any) => { expect(Array.isArray(groundingMetadata?.webSearchQueries)).toBe(true); expect(groundingMetadata?.webSearchQueries?.length).toBeGreaterThan(0); // Verify search entry point exists expect(groundingMetadata?.searchEntryPoint?.renderedContent).toBeDefined(); // Verify grounding supports expect(Array.isArray(groundingMetadata?.groundingSupports)).toBe(true); const support = groundingMetadata?.groundingSupports?.[0]; expect(support?.segment).toBeDefined(); expect(Array.isArray(support?.groundingChunkIndices)).toBe(true); expect(Array.isArray(support?.confidenceScores)).toBe(true); }; const verifySafetyRatings = (safetyRatings: any[]) => { expect(Array.isArray(safetyRatings)).toBe(true); expect(safetyRatings?.length).toBeGreaterThan(0); // Verify each safety rating has required properties safetyRatings?.forEach(rating => { expect(rating.category).toBeDefined(); expect(rating.probability).toBeDefined(); expect(typeof rating.probabilityScore).toBe('number'); expect(rating.severity).toBeDefined(); expect(typeof rating.severityScore).toBe('number'); }); }; const shouldRunTests = ( capabilities: ModelCapabilities | undefined, requiredCapabilities: Capability[], ) => { return capabilities ? requiredCapabilities.every(cap => capabilities.includes(cap)) : false; }; function describeIfCapability( capabilities: ModelCapabilities | undefined, requiredCapabilities: Capability[], description: string, callback: () => void, ) { if (shouldRunTests(capabilities, requiredCapabilities)) { describe(description, callback); } } export function createFeatureTestSuite({ name, models, timeout = 10000, customAssertions = { skipUsage: false }, }: TestSuiteOptions) { return () => { const errorValidator = customAssertions.errorValidator || ((error: APICallError) => { throw new Error('errorValidator not implemented'); }); describe(`${name} Feature Test Suite`, () => { vi.setConfig({ testTimeout: timeout }); describe.each(createModelObjects(models.languageModels))( 'Language Model: $modelId', ({ model, capabilities }) => { describeIfCapability( capabilities, ['textCompletion'], 'Basic Text Generation', () => { it('should generate text', async () => { const result = await generateText({ model, prompt: 'Write a haiku about programming.', }); expect(result.text).toBeTruthy(); if (!customAssertions.skipUsage) { expect(result.usage?.totalTokens).toBeGreaterThan(0); } }); it('should generate text with system prompt', async () => { const result = await generateText({ model, messages: [ { role: 'system', content: 'You are a helpful assistant.', }, { role: 'user', content: [ { type: 'text', text: 'Write a haiku about programming.', }, ], }, ], }); expect(result.text).toBeTruthy(); expect(result.usage?.totalTokens).toBeGreaterThan(0); }); it('should stream text', async () => { const result = streamText({ model, prompt: 'Count from 1 to 5 slowly.', }); const chunks: string[] = []; for await (const chunk of result.textStream) { chunks.push(chunk); } expect(chunks.length).toBeGreaterThan(0); if (!customAssertions.skipUsage) { expect((await result.usage)?.totalTokens).toBeGreaterThan(0); } }); }, ); describeIfCapability( capabilities, ['objectGeneration'], 'Object Generation', () => { it('should generate basic blog metadata', async () => { const result = await generateObject({ model, schema: z.object({ title: z.string(), tags: z.array(z.string()), }), prompt: 'Generate metadata for a blog post about TypeScript.', }); expect(result.object.title).toBeTruthy(); expect(Array.isArray(result.object.tags)).toBe(true); if (!customAssertions.skipUsage) { expect(result.usage?.totalTokens).toBeGreaterThan(0); } }); it('should stream RPG character list', async () => { const result = streamObject({ model, schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe( 'Character class, e.g. warrior, mage, or thief.', ), description: z.string(), }), ), }), prompt: 'Generate 3 RPG character descriptions.', }); const parts = []; for await (const part of result.partialObjectStream) { parts.push(part); } expect(parts.length).toBeGreaterThan(0); if (!customAssertions.skipUsage) { expect((await result.usage).totalTokens).toBeGreaterThan(0); } }); it('should generate a simple object', async () => { const result = await generateObject({ model, schema: z.object({ name: z.string(), age: z.number(), }), prompt: 'Generate details for a person.', }); expect(result.object.name).toBeTruthy(); expect(typeof result.object.age).toBe('number'); if (!customAssertions.skipUsage) { expect(result.usage?.totalTokens).toBeGreaterThan(0); } }); it('should generate multiple simple items', async () => { const result = await generateObject({ model, schema: z.object({ items: z .array( z.object({ name: z.string(), quantity: z.number(), }), ) .length(3), }), prompt: 'Generate a shopping list with 3 items.', }); expect(result.object.items).toHaveLength(3); expect(result.object.items[0].name).toBeTruthy(); expect(typeof result.object.items[0].quantity).toBe('number'); }); it('should generate nested objects', async () => { const result = await generateObject({ model, schema: z.object({ user: z.object({ name: z.string(), contact: z.object({ email: z.string(), phone: z.string(), }), }), preferences: z.object({ theme: z.enum(['light', 'dark']), notifications: z.boolean(), }), }), prompt: 'Generate a user profile with contact details and preferences.', }); // Verify the nested structure is present and populated expect(typeof result.object.user.name).toBe('string'); expect(typeof result.object.user.contact.email).toBe('string'); expect(typeof result.object.user.contact.phone).toBe('string'); expect(['light', 'dark']).toContain( result.object.preferences.theme, ); expect(typeof result.object.preferences.notifications).toBe( 'boolean', ); }); it('should generate arrays of objects', async () => { const result = await generateObject({ model, schema: z.object({ posts: z .array( z.object({ title: z.string(), comments: z .array( z.object({ author: z.string(), text: z.string(), }), ) .min(1), }), ) .min(2), }), prompt: 'Generate a blog with multiple posts and comments.', }); expect(result.object.posts.length).toBeGreaterThanOrEqual(2); expect( result.object.posts[0].comments.length, ).toBeGreaterThanOrEqual(1); }); it('should handle cross-referenced schemas', async () => { const BaseProduct = z.object({ name: z.string(), category: z.string(), usage_instructions: z.string(), }); const MorningProduct = BaseProduct.extend({ morning_specific_instructions: z.string(), }); const EveningProduct = BaseProduct.extend({ evening_specific_instructions: z.string(), }); const result = await generateObject({ model, schema: z.object({ morning_routine: z.array(MorningProduct), evening_routine: z.array(EveningProduct), notes: z.string(), }), prompt: 'Generate a skincare routine with morning and evening products.', }); expect(result.object.morning_routine.length).toBeGreaterThan(0); expect(result.object.evening_routine.length).toBeGreaterThan(0); expect( result.object.morning_routine[0] .morning_specific_instructions, ).toBeTruthy(); expect( result.object.evening_routine[0] .evening_specific_instructions, ).toBeTruthy(); }); it('should handle equivalent flat schemas', async () => { const result = await generateObject({ model, schema: z.object({ morning_routine: z.array( z.object({ name: z.string(), category: z.string(), usage_instructions: z.string(), morning_specific_instructions: z.string(), }), ), evening_routine: z.array( z.object({ name: z.string(), category: z.string(), usage_instructions: z.string(), evening_specific_instructions: z.string(), }), ), notes: z.string(), }), prompt: 'Generate a skincare routine with morning and evening products.', }); expect(result.object.morning_routine.length).toBeGreaterThan(0); expect(result.object.evening_routine.length).toBeGreaterThan(0); expect( result.object.morning_routine[0] .morning_specific_instructions, ).toBeTruthy(); expect( result.object.evening_routine[0] .evening_specific_instructions, ).toBeTruthy(); }); it('should stream complex nested objects', async () => { const result = streamObject({ model, schema: z.object({ chapters: z.array( z.object({ title: z.string(), sections: z.array( z.object({ heading: z.string(), content: z.string(), subsections: z.array( z.object({ title: z.string(), paragraphs: z.array(z.string()), }), ), }), ), }), ), }), prompt: 'Generate a book outline with chapters, sections, and subsections.', }); const parts = []; for await (const part of result.partialObjectStream) { parts.push(part); } const finalResult = await result.object; expect(finalResult.chapters.length).toBeGreaterThan(0); expect(finalResult.chapters[0].sections.length).toBeGreaterThan( 0, ); expect(parts.length).toBeGreaterThan(0); }); describe('Schema and Prompt Variations', () => { it('should generate with field descriptions', async () => { const result = await generateObject({ model, schema: z.object({ title: z .string() .describe('A catchy title for the article'), summary: z .string() .describe('A 2-3 sentence overview of the main points'), readingTime: z .number() .describe('Estimated reading time in minutes'), targetAudience: z .array(z.string()) .describe('The intended reader groups'), }), prompt: 'Generate metadata for a technical article.', }); expect(result.object.title).toBeTruthy(); expect(result.object.summary.length).toBeGreaterThan(50); }); it('should handle detailed system prompts', async () => { const result = await generateObject({ model, schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.string()), steps: z.array(z.string()), }), }), messages: [ { role: 'system', content: 'You are a professional chef. Always provide detailed, precise cooking instructions.', }, { role: 'user', content: 'Create a pasta recipe.' }, ], }); expect(result.object.recipe.steps.length).toBeGreaterThan(3); expect( result.object.recipe.ingredients.length, ).toBeGreaterThan(3); }); it('should generate complex objects with both descriptions and system context', async () => { const ProductSchema = z.object({ name: z .string() .describe('Product name, should be unique and memorable'), price: z .number() .describe( 'Price in USD, should be competitive for market', ), features: z .array(z.string()) .describe('Key selling points, 3-5 items'), marketingPlan: z .object({ targetMarket: z .string() .describe('Primary customer demographic'), channels: z .array(z.string()) .describe('Marketing channels to use'), budget: z .number() .describe('Proposed marketing budget in USD'), }) .describe('Marketing strategy details'), }); const result = await generateObject({ model, schemaName: 'product', schemaDescription: 'A product listing', schema: ProductSchema, messages: [ { role: 'system', content: 'You are a senior product manager with 15 years of experience in tech products.', }, { role: 'user', content: 'Create a product plan for a new smart home device.', }, ], }); expect(result.object.features.length).toBeGreaterThanOrEqual( 3, ); expect(result.object.marketingPlan.budget).toBeGreaterThan(0); expect(result.object.price).toBeGreaterThan(0); }); }); }, ); describeIfCapability( capabilities, ['toolCalls'], 'Tool Calls', () => { it('should generate text with tool calls', async () => { const result = await generateText({ model, prompt: 'What is 2+2? Use the calculator tool to compute this.', tools: { calculator: { inputSchema: z.object({ expression: z .string() .describe('The mathematical expression to evaluate'), }), execute: async ({ expression }) => eval(expression).toString(), }, }, }); expect(result.toolCalls?.[0]).toMatchObject({ toolName: 'calculator', input: { expression: '2+2' }, }); expect(result.toolResults?.[0].output).toBe('4'); if (!customAssertions.skipUsage) { expect(result.usage?.totalTokens).toBeGreaterThan(0); } }); it('should stream text with tool calls', async () => { let toolCallCount = 0; const result = streamText({ model, prompt: 'What is 2+2? Use the calculator tool to compute this.', tools: { calculator: { inputSchema: z.object({ expression: z.string(), }), execute: async ({ expression }) => { toolCallCount++; return eval(expression).toString(); }, }, }, }); const parts = []; for await (const part of result.fullStream) { parts.push(part); } expect(parts.some(part => part.type === 'tool-call')).toBe( true, ); expect(toolCallCount).toBe(1); if (!customAssertions.skipUsage) { expect((await result.usage).totalTokens).toBeGreaterThan(0); } }); it('should handle multiple sequential tool calls', async () => { let weatherCalls = 0; let musicCalls = 0; const sfTemp = 15; const result = await generateText({ model, prompt: 'Check the temperature in San Francisco and play music that matches the weather. Be sure to report the chosen song name.', tools: { getTemperature: { inputSchema: z.object({ city: z .string() .describe('The city to check temperature for'), }), execute: async ({ city }) => { weatherCalls++; return `${sfTemp}`; }, }, playWeatherMusic: { inputSchema: z.object({ temperature: z .number() .describe('Temperature in Celsius'), }), execute: async ({ temperature }) => { musicCalls++; if (temperature <= 10) { return 'Playing "Winter Winds" by Mumford & Sons'; } else if (temperature <= 20) { return 'Playing "Foggy Day" by Frank Sinatra'; } else if (temperature <= 30) { return 'Playing "Here Comes the Sun" by The Beatles'; } else { return 'Playing "Hot Hot Hot" by Buster Poindexter'; } }, }, }, stopWhen: stepCountIs(10), }); expect(weatherCalls).toBe(1); expect(musicCalls).toBe(1); expect(result.text).toContain('Foggy Day'); }); }, ); describeIfCapability( capabilities, ['imageInput'], 'Image Input', () => { it('should generate text with image URL input', async () => { const result = await generateText({ model, messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.', }, { type: 'image', image: 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/comic-cat.png?raw=true', }, ], }, ], }); expect(result.text).toBeTruthy(); expect(result.text.toLowerCase()).toContain('cat'); if (!customAssertions.skipUsage) { expect(result.usage?.totalTokens).toBeGreaterThan(0); } }); it('should generate text with image input', async () => { const result = await generateText({ model, messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.', }, { type: 'image', // TODO(shaper): Some tests omit the .toString() below. image: fs .readFileSync('./data/comic-cat.png') .toString('base64'), }, ], }, ], }); expect(result.text.toLowerCase()).toContain('cat'); if (!customAssertions.skipUsage) { expect(result.usage?.totalTokens).toBeGreaterThan(0); } }); it('should stream text with image URL input', async () => { const result = streamText({ model, messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.', }, { type: 'image', image: 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/comic-cat.png?raw=true', }, ], }, ], }); const chunks: string[] = []; for await (const chunk of result.textStream) { chunks.push(chunk); } const fullText = chunks.join(''); expect(chunks.length).toBeGreaterThan(0); expect(fullText.toLowerCase()).toContain('cat'); expect((await result.usage)?.totalTokens).toBeGreaterThan(0); }); it('should stream text with image input', async () => { const result = streamText({ model, messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.', }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png'), }, ], }, ], }); const chunks: string[] = []; for await (const chunk of result.textStream) { chunks.push(chunk); } const fullText = chunks.join(''); expect(fullText.toLowerCase()).toContain('cat'); expect(chunks.length).toBeGreaterThan(0); if (!customAssertions.skipUsage) { expect((await result.usage)?.totalTokens).toBeGreaterThan(0); } }); }, ); describeIfCapability(capabilities, ['pdfInput'], 'PDF Input', () => { it('should generate text with PDF input', async () => { const result = await generateText({ model, messages: [ { role: 'user', content: [ { type: 'text', text: 'Summarize the contents of this PDF.', }, { type: 'file', data: fs .readFileSync('./data/ai.pdf') .toString('base64'), mediaType: 'application/pdf', }, ], }, ], }); expect(result.text).toBeTruthy(); expect(result.text.toLowerCase()).toContain('embedding'); expect(result.usage?.totalTokens).toBeGreaterThan(0); }); }); describeIfCapability( capabilities, ['audioInput'], 'Audio Input', () => { it('should generate text from audio input', async () => { const result = await generateText({ model, messages: [ { role: 'user', content: [ { type: 'text', text: 'Output a transcript of spoken words. Break up transcript lines when there are pauses. Include timestamps in the format of HH:MM:SS.SSS.', }, { type: 'file', data: Buffer.from( fs.readFileSync('./data/galileo.mp3'), ), mediaType: 'audio/mpeg', }, ], }, ], }); expect(result.text).toBeTruthy(); expect(result.text.toLowerCase()).toContain('galileo'); expect(result.usage?.totalTokens).toBeGreaterThan(0); }); }, ); describeIfCapability( capabilities, ['searchGrounding'], 'Search Grounding', () => { it('should include search grounding metadata in response when search grounding is enabled', async () => { const result = await generateText({ model, prompt: 'What is the current population of Tokyo?', }); expect(result.text).toBeTruthy(); expect(result.text.toLowerCase()).toContain('tokyo'); expect(result.usage?.totalTokens).toBeGreaterThan(0); const metadata = result.providerMetadata?.google as | GoogleGenerativeAIProviderMetadata | undefined; verifyGroundingMetadata(metadata?.groundingMetadata); }); it('should include search grounding metadata when streaming with search grounding enabled', async () => { const result = streamText({ model, prompt: 'What is the current population of Tokyo?', }); const chunks: string[] = []; for await (const chunk of result.textStream) { chunks.push(chunk); } const metadata = (await result.providerMetadata)?.google as | GoogleGenerativeAIProviderMetadata | undefined; const completeText = chunks.join(''); expect(completeText).toBeTruthy(); expect(completeText.toLowerCase()).toContain('tokyo'); expect((await result.usage)?.totalTokens).toBeGreaterThan(0); verifyGroundingMetadata(metadata?.groundingMetadata); }); it('should include safety ratings in response when search grounding is enabled', async () => { const result = await generateText({ model, prompt: 'What is the current population of Tokyo?', }); const metadata = result.providerMetadata?.google as | GoogleGenerativeAIProviderMetadata | undefined; verifySafetyRatings(metadata?.safetyRatings ?? []); }); it('should include safety ratings when streaming with search grounding enabled', async () => { const result = streamText({ model, prompt: 'What is the current population of Tokyo?', }); for await (const _ of result.textStream) { // consume the stream } const metadata = (await result.providerMetadata)?.google as | GoogleGenerativeAIProviderMetadata | undefined; verifySafetyRatings(metadata?.safetyRatings ?? []); }); }, ); }, ); if (models.invalidModel) { describe('Chat Model Error Handling:', () => { const invalidModel = models.invalidModel!; it('should throw error on generate text attempt with invalid model ID', async () => { try { await generateText({ model: invalidModel, prompt: 'This should fail', }); } catch (error) { expect(error).toBeInstanceOf(APICallError); errorValidator(error as APICallError); } }); it('should throw error on stream text attempt with invalid model ID', async () => { try { const result = streamText({ model: invalidModel, prompt: 'This should fail', }); // Try to consume the stream to trigger the error for await (const _ of result.textStream) { // Do nothing with the chunks } // If we reach here, the test should fail expect(true).toBe(false); // Force test to fail if no error is thrown } catch (error) { expect(error).toBeInstanceOf(APICallError); errorValidator(error as APICallError); } }); }); } if (models.invalidImageModel) { describe('Image Model Error Handling:', () => { const invalidModel = models.invalidImageModel!; it('should throw error on generate image attempt with invalid model ID', async () => { try { await generateImage({ model: invalidModel, prompt: 'This should fail', }); } catch (error) { expect(error).toBeInstanceOf(APICallError); errorValidator(error as APICallError); } }); }); } if (models.embeddingModels && models.embeddingModels.length > 0) { describe.each(createModelObjects(models.embeddingModels))( 'Embedding Model: $modelId', ({ model, capabilities }) => { describeIfCapability( capabilities, ['embedding'], 'Embedding Generation', () => { it('should generate single embedding', async () => { const result = await embed({ model, value: 'This is a test sentence for embedding.', }); expect(Array.isArray(result.embedding)).toBe(true); expect(result.embedding.length).toBeGreaterThan(0); if (!customAssertions.skipUsage) { expect(result.usage?.tokens).toBeGreaterThan(0); } }); it('should generate multiple embeddings', async () => { const result = await embedMany({ model, values: [ 'First test sentence.', 'Second test sentence.', 'Third test sentence.', ], }); expect(Array.isArray(result.embeddings)).toBe(true); expect(result.embeddings.length).toBe(3); if (!customAssertions.skipUsage) { expect(result.usage?.tokens).toBeGreaterThan(0); } }); }, ); }, ); } if (models.imageModels && models.imageModels.length > 0) { describe.each(createModelObjects(models.imageModels))( 'Image Model: $modelId', ({ model, capabilities }) => { describeIfCapability( capabilities, ['imageGeneration'], 'Image Generation', () => { it('should generate an image', async () => { const result = await generateImage({ model, prompt: 'A cute cartoon cat', }); // Verify we got a base64 string back expect(result.image.base64).toBeTruthy(); expect(typeof result.image.base64).toBe('string'); // Check the decoded length is reasonable (at least 10KB) const decoded = Buffer.from(result.image.base64, 'base64'); expect(decoded.length).toBeGreaterThan(10 * 1024); }); }, ); }, ); } }); }; } --- File: /ai/examples/ai-core/src/e2e/fireworks.test.ts --- import 'dotenv/config'; import { expect } from 'vitest'; import { fireworks as provider, FireworksErrorData } from '@ai-sdk/fireworks'; import { APICallError } from '@ai-sdk/provider'; import { createEmbeddingModelWithCapabilities, createFeatureTestSuite, createImageModelWithCapabilities, createLanguageModelWithCapabilities, } from './feature-test-suite'; const createChatModel = (modelId: string) => createLanguageModelWithCapabilities(provider.chatModel(modelId)); const createCompletionModel = (modelId: string) => createLanguageModelWithCapabilities(provider.completionModel(modelId), [ 'textCompletion', ]); createFeatureTestSuite({ name: 'Fireworks', models: { invalidModel: provider.chatModel('no-such-model'), languageModels: [ // createChatModel('accounts/fireworks/models/deepseek-v3'), createChatModel('accounts/fireworks/models/llama-v3p3-70b-instruct'), // createChatModel('accounts/fireworks/models/mixtral-8x7b-instruct'), // createChatModel('accounts/fireworks/models/qwen2p5-72b-instruct'), // createCompletionModel('accounts/fireworks/models/llama-v3-8b-instruct'), createCompletionModel( 'accounts/fireworks/models/llama-v3p2-11b-vision-instruct', ), ], embeddingModels: [ createEmbeddingModelWithCapabilities( provider.textEmbeddingModel('nomic-ai/nomic-embed-text-v1.5'), ), ], imageModels: [ createImageModelWithCapabilities( provider.image('accounts/fireworks/models/flux-1-dev-fp8'), ), ], }, timeout: 10000, customAssertions: { errorValidator: (error: APICallError) => { expect((error.data as FireworksErrorData).error).toBe( 'Model not found, inaccessible, and/or not deployed', ); }, }, })(); --- File: /ai/examples/ai-core/src/e2e/gateway.test.ts --- import 'dotenv/config'; import { gateway as provider } from '@ai-sdk/gateway'; import { createFeatureTestSuite, createLanguageModelWithCapabilities, } from './feature-test-suite'; const createChatModel = (modelId: string) => createLanguageModelWithCapabilities(provider.languageModel(modelId)); createFeatureTestSuite({ name: 'Gateway', models: { languageModels: [createChatModel('xai/grok-3-beta')], }, timeout: 30000, })(); --- File: /ai/examples/ai-core/src/e2e/google-vertex-anthropic.test.ts --- import { createVertexAnthropic as createVertexAnthropicNode, vertexAnthropic, vertexAnthropic as vertexAnthropicNode, } from '@ai-sdk/google-vertex/anthropic'; import { createVertexAnthropic as createVertexAnthropicEdge, vertexAnthropic as vertexAnthropicEdge, } from '@ai-sdk/google-vertex/anthropic/edge'; import { LanguageModelV2 } from '@ai-sdk/provider'; import { APICallError, generateText, stepCountIs } from 'ai'; import 'dotenv/config'; import fs from 'fs'; import { describe, expect, it } from 'vitest'; import { createFeatureTestSuite, createLanguageModelWithCapabilities, ModelWithCapabilities, } from './feature-test-suite'; const RUNTIME_VARIANTS = { edge: { name: 'Edge Runtime', createVertexAnthropic: createVertexAnthropicEdge, vertexAnthropic: vertexAnthropicEdge, }, node: { name: 'Node Runtime', createVertexAnthropic: createVertexAnthropicNode, vertexAnthropic: vertexAnthropicNode, }, } as const; const createModelObject = ( model: LanguageModelV2, ): { model: LanguageModelV2; modelId: string } => ({ model: model, modelId: model.modelId, }); const createLanguageModel = ( createVertexAnthropic: | typeof createVertexAnthropicNode | typeof createVertexAnthropicEdge, modelId: string, additionalTests: ((model: LanguageModelV2) => void)[] = [], ): ModelWithCapabilities<LanguageModelV2> => { const model = createVertexAnthropic({ project: process.env.GOOGLE_VERTEX_PROJECT!, // Anthropic models are typically only available in us-east5 region. location: process.env.GOOGLE_VERTEX_LOCATION ?? 'us-east5', })(modelId); if (additionalTests.length > 0) { describe.each([createModelObject(model)])( 'Provider-specific tests: $modelId', ({ model }) => { additionalTests.forEach(test => test(model)); }, ); } return createLanguageModelWithCapabilities(model); }; const createModelVariants = ( createVertexAnthropic: | typeof createVertexAnthropicNode | typeof createVertexAnthropicEdge, modelId: string, ): ModelWithCapabilities<LanguageModelV2>[] => [ createLanguageModel(createVertexAnthropic, modelId, [toolTests]), ]; // Model variants to test against const CHAT_MODELS = [ 'claude-3-5-sonnet-v2@20241022', // 'claude-3-5-haiku@20241022', // 'claude-3-5-sonnet@20240620', // Models must be individually enabled through the Cloud Console. The above are the latest and most likely to be used. // 'claude-3-haiku@20240307', // 'claude-3-sonnet@20240229', // 'claude-3-opus@20240229', ]; const createModelsForRuntime = ( createVertexAnthropic: | typeof createVertexAnthropicNode | typeof createVertexAnthropicEdge, ) => ({ languageModels: CHAT_MODELS.flatMap(modelId => createModelVariants(createVertexAnthropic, modelId), ), }); const LONG_TEST_MILLIS = 20000; const COMPUTER_USE_TEST_MILLIS = 45000; describe.each(Object.values(RUNTIME_VARIANTS))( 'Vertex Anthropic E2E Tests - $name', ({ createVertexAnthropic }) => { createFeatureTestSuite({ name: `Vertex Anthropic (${createVertexAnthropic.name})`, models: createModelsForRuntime(createVertexAnthropic), timeout: LONG_TEST_MILLIS, customAssertions: { skipUsage: false, errorValidator: (error: APICallError) => { expect(error.message).toMatch(/Model .* not found/); }, }, })(); }, ); const toolTests = (model: LanguageModelV2) => { it.skipIf(!['claude-3-5-sonnet-v2@20241022'].includes(model.modelId))( 'should execute computer tool commands', async () => { const result = await generateText({ model, tools: { computer: vertexAnthropic.tools.computer_20241022({ displayWidthPx: 1024, displayHeightPx: 768, async execute({ action, coordinate, text }) { switch (action) { case 'screenshot': { return { type: 'image', data: fs .readFileSync('./data/screenshot-editor.png') .toString('base64'), }; } default: { return `executed ${action}`; } } }, toModelOutput(result) { return { type: 'content', value: [ typeof result === 'string' ? { type: 'text', text: result } : { type: 'media', data: result.data, mediaType: 'image/png', }, ], }; }, }), }, prompt: 'How can I switch to dark mode? Take a look at the screen and tell me.', stopWhen: stepCountIs(5), }); console.log(result.text); expect(result.text).toBeTruthy(); expect(result.text.toLowerCase()).toMatch(/color theme|dark mode/); expect(result.usage?.totalTokens).toBeGreaterThan(0); }, { timeout: COMPUTER_USE_TEST_MILLIS }, ); it.skipIf(!['claude-3-5-sonnet-v2@20241022'].includes(model.modelId))( 'should execute computer use bash tool commands', async () => { const result = await generateText({ model, tools: { bash: vertexAnthropic.tools.bash_20241022({ async execute({ command }) { return [ { type: 'text', text: ` ❯ ${command} README.md build data node_modules package.json src tsconfig.json `, }, ]; }, }), }, prompt: 'List the files in my directory.', stopWhen: stepCountIs(2), }); expect(result.text).toBeTruthy(); expect(result.text).toContain('README.md'); // Check for specific file expect(result.text).toContain('package.json'); // Check for another specific file expect(result.text).toContain('node_modules'); // Check for directory expect(result.usage?.totalTokens).toBeGreaterThan(0); }, { timeout: COMPUTER_USE_TEST_MILLIS }, ); it.skipIf(!['claude-3-5-sonnet-v2@20241022'].includes(model.modelId))( 'should execute computer user editor tool commands', async () => { let editorContent = '## README\nThis is a test file.'; const result = await generateText({ model, tools: { str_replace_editor: vertexAnthropic.tools.textEditor_20241022({ async execute({ command, path, old_str, new_str }) { switch (command) { case 'view': { return editorContent; } case 'create': case 'insert': { editorContent = new_str!; return editorContent; } case 'str_replace': { editorContent = editorContent.replace(old_str!, new_str!); return editorContent; } default: return ''; } }, }), }, prompt: 'Update my README file to talk about AI.', stopWhen: stepCountIs(5), }); expect(result.text).toBeTruthy(); expect(editorContent).not.toBe('## README\nThis is a test file.'); expect(result.usage?.totalTokens).toBeGreaterThan(0); }, { timeout: COMPUTER_USE_TEST_MILLIS }, ); }; --- File: /ai/examples/ai-core/src/e2e/google-vertex.test.ts --- import { vertex as vertexNode } from '@ai-sdk/google-vertex'; import { vertex as vertexEdge } from '@ai-sdk/google-vertex/edge'; import { ImageModelV2, LanguageModelV2 } from '@ai-sdk/provider'; import { APICallError, experimental_generateImage as generateImage } from 'ai'; import 'dotenv/config'; import { describe, expect, it, vi } from 'vitest'; import { createEmbeddingModelWithCapabilities, createFeatureTestSuite, createImageModelWithCapabilities, createLanguageModelWithCapabilities, defaultChatModelCapabilities, ModelWithCapabilities, } from './feature-test-suite'; import { wrapLanguageModel } from 'ai'; import { defaultSettingsMiddleware } from 'ai'; const RUNTIME_VARIANTS = { edge: { name: 'Edge Runtime', vertex: vertexEdge, }, node: { name: 'Node Runtime', vertex: vertexNode, }, } as const; const createBaseModel = ( vertex: typeof vertexNode | typeof vertexEdge, modelId: string, ): ModelWithCapabilities<LanguageModelV2> => createLanguageModelWithCapabilities(vertex(modelId), [ ...defaultChatModelCapabilities, 'audioInput', ]); const createSearchGroundedModel = ( vertex: typeof vertexNode | typeof vertexEdge, modelId: string, ): ModelWithCapabilities<LanguageModelV2> => ({ model: wrapLanguageModel({ model: vertex(modelId), middleware: defaultSettingsMiddleware({ settings: { providerOptions: { google: { useSearchGrounding: true, }, }, }, }), }), capabilities: [...defaultChatModelCapabilities, 'searchGrounding'], }); const createModelObject = ( imageModel: ImageModelV2, ): { model: ImageModelV2; modelId: string } => ({ model: imageModel, modelId: imageModel.modelId, }); const createImageModel = ( vertex: typeof vertexNode | typeof vertexEdge, modelId: string, additionalTests: ((model: ImageModelV2) => void)[] = [], ): ModelWithCapabilities<ImageModelV2> => { const model = vertex.image(modelId); if (additionalTests.length > 0) { describe.each([createModelObject(model)])( 'Provider-specific tests: $modelId', ({ model }) => { additionalTests.forEach(test => test(model)); }, ); } return createImageModelWithCapabilities(model); }; const createModelVariants = ( vertex: typeof vertexNode | typeof vertexEdge, modelId: string, ): ModelWithCapabilities<LanguageModelV2>[] => [ createBaseModel(vertex, modelId), createSearchGroundedModel(vertex, modelId), ]; const createModelsForRuntime = ( vertex: typeof vertexNode | typeof vertexEdge, ) => ({ invalidModel: vertex('no-such-model'), languageModels: [ ...createModelVariants(vertex, 'gemini-2.0-flash-exp'), ...createModelVariants(vertex, 'gemini-1.5-flash'), // Gemini 2.0 and Pro models have low quota limits and may require billing enabled. // ...createModelVariants(vertex, 'gemini-1.5-pro-001'), // ...createModelVariants(vertex, 'gemini-1.0-pro-001'), ], embeddingModels: [ createEmbeddingModelWithCapabilities( vertex.textEmbeddingModel('textembedding-gecko'), ), createEmbeddingModelWithCapabilities( vertex.textEmbeddingModel('textembedding-gecko-multilingual'), ), ], imageModels: [ createImageModel(vertex, 'imagen-3.0-fast-generate-001', [imageTest]), createImageModel(vertex, 'imagen-3.0-generate-002', [imageTest]), ], }); describe.each(Object.values(RUNTIME_VARIANTS))( 'Google Vertex AI - $name', ({ vertex }) => { createFeatureTestSuite({ name: `Google Vertex AI (${vertex.name})`, models: createModelsForRuntime(vertex), timeout: 20000, customAssertions: { skipUsage: false, errorValidator: (error: APICallError) => { expect(error.message).toMatch(/Model .* not found/); }, }, })(); }, ); const mediaTypeSignatures = [ { mediaType: 'image/gif' as const, bytes: [0x47, 0x49, 0x46] }, { mediaType: 'image/png' as const, bytes: [0x89, 0x50, 0x4e, 0x47] }, { mediaType: 'image/jpeg' as const, bytes: [0xff, 0xd8] }, { mediaType: 'image/webp' as const, bytes: [0x52, 0x49, 0x46, 0x46] }, ]; function detectImageMediaType( image: Uint8Array, ): 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp' | undefined { for (const { bytes, mediaType } of mediaTypeSignatures) { if ( image.length >= bytes.length && bytes.every((byte, index) => image[index] === byte) ) { return mediaType; } } return undefined; } const imageTest = (model: ImageModelV2) => { vi.setConfig({ testTimeout: 10000 }); it('should generate an image with correct dimensions and format', async () => { const { image } = await generateImage({ model, prompt: 'A burrito launched through a tunnel', providerOptions: { vertex: { aspectRatio: '3:4', }, }, }); // Verify we got a Uint8Array back expect(image.uint8Array).toBeInstanceOf(Uint8Array); // Check the file size is reasonable (at least 10KB, less than 10MB) expect(image.uint8Array.length).toBeGreaterThan(10 * 1024); expect(image.uint8Array.length).toBeLessThan(10 * 1024 * 1024); // Verify PNG format const mediaType = detectImageMediaType(image.uint8Array); expect(mediaType).toBe('image/png'); // Create a temporary buffer to verify image dimensions const tempBuffer = Buffer.from(image.uint8Array); // PNG dimensions are stored at bytes 16-24 const width = tempBuffer.readUInt32BE(16); const height = tempBuffer.readUInt32BE(20); // https://cloud.google.com/vertex-ai/generative-ai/docs/image/generate-images#performance-limits expect(width).toBe(896); expect(height).toBe(1280); }); }; --- File: /ai/examples/ai-core/src/e2e/google.test.ts --- import { GoogleErrorData, google as provider } from '@ai-sdk/google'; import { APICallError, ImageModelV2, LanguageModelV2 } from '@ai-sdk/provider'; import 'dotenv/config'; import { expect } from 'vitest'; import { ModelWithCapabilities, createEmbeddingModelWithCapabilities, createFeatureTestSuite, createLanguageModelWithCapabilities, createImageModelWithCapabilities, defaultChatModelCapabilities, } from './feature-test-suite'; import { wrapLanguageModel } from 'ai'; import { defaultSettingsMiddleware } from 'ai'; const createChatModel = ( modelId: string, ): ModelWithCapabilities<LanguageModelV2> => createLanguageModelWithCapabilities(provider.chat(modelId)); const createImageModel = ( modelId: string, ): ModelWithCapabilities<ImageModelV2> => createImageModelWithCapabilities(provider.image(modelId)); const createSearchGroundedModel = ( modelId: string, ): ModelWithCapabilities<LanguageModelV2> => { const model = provider.chat(modelId); return { model: wrapLanguageModel({ model, middleware: defaultSettingsMiddleware({ settings: { providerOptions: { google: { useSearchGrounding: true } }, }, }), }), capabilities: [...defaultChatModelCapabilities, 'searchGrounding'], }; }; createFeatureTestSuite({ name: 'Google Generative AI', models: { invalidModel: provider.chat('no-such-model'), languageModels: [ createSearchGroundedModel('gemini-1.5-flash-latest'), createChatModel('gemini-1.5-flash-latest'), // Gemini 2.0 and Pro models have low quota limits and may require billing enabled. // createChatModel('gemini-2.0-flash-exp'), // createSearchGroundedModel('gemini-2.0-flash-exp'), // createChatModel('gemini-1.5-pro-latest'), // createChatModel('gemini-1.0-pro'), ], embeddingModels: [ createEmbeddingModelWithCapabilities( provider.textEmbeddingModel('gemini-embedding-001'), ), ], imageModels: [createImageModel('imagen-3.0-generate-002')], }, timeout: 20000, customAssertions: { skipUsage: true, errorValidator: (error: APICallError) => { console.log(error); expect((error.data as GoogleErrorData).error.message).match( /models\/no\-such\-model is not found/, ); }, }, })(); --- File: /ai/examples/ai-core/src/e2e/groq.test.ts --- import { groq as provider } from '@ai-sdk/groq'; import { createFeatureTestSuite, createLanguageModelWithCapabilities, } from './feature-test-suite'; import 'dotenv/config'; const createChatModel = (modelId: string) => createLanguageModelWithCapabilities(provider.languageModel(modelId)); createFeatureTestSuite({ name: 'Groq', models: { languageModels: [ createChatModel('deepseek-r1-distill-llama-70b'), createChatModel('llama-3.1-8b-instant'), createChatModel('llama-3.3-70b-versatile'), createChatModel('mistral-saba-24b'), createChatModel('qwen-qwq-32b'), ], }, timeout: 30000, })(); --- File: /ai/examples/ai-core/src/e2e/luma.test.ts --- import { expect } from 'vitest'; import { luma as provider, LumaErrorData } from '@ai-sdk/luma'; import { APICallError } from '@ai-sdk/provider'; import { createFeatureTestSuite, createImageModelWithCapabilities, } from './feature-test-suite'; import 'dotenv/config'; createFeatureTestSuite({ name: 'Luma', models: { invalidImageModel: provider.image('no-such-model'), imageModels: [ createImageModelWithCapabilities(provider.image('photon-flash-1')), createImageModelWithCapabilities(provider.image('photon-1')), ], }, timeout: 30000, customAssertions: { errorValidator: (error: APICallError) => { expect((error.data as LumaErrorData).detail[0].msg).toMatch( /Input should be/i, ); }, }, })(); --- File: /ai/examples/ai-core/src/e2e/openai.test.ts --- import { openai as provider } from '@ai-sdk/openai'; import { LanguageModelV2 } from '@ai-sdk/provider'; import { APICallError } from 'ai'; import 'dotenv/config'; import { expect } from 'vitest'; import { ModelWithCapabilities, createEmbeddingModelWithCapabilities, createFeatureTestSuite, createLanguageModelWithCapabilities, } from './feature-test-suite'; const createChatModel = ( modelId: string, ): ModelWithCapabilities<LanguageModelV2> => createLanguageModelWithCapabilities(provider.chat(modelId)); createFeatureTestSuite({ name: 'OpenAI', models: { invalidModel: provider.chat('no-such-model'), languageModels: [ createChatModel('gpt-4.1'), createChatModel('gpt-4.1-mini'), createChatModel('gpt-4.1-nano'), createChatModel('o3'), createChatModel('o4-mini'), createChatModel('o1-mini'), createChatModel('gpt-4o-mini'), createChatModel('gpt-3.5-turbo'), createChatModel('gpt-4-turbo-preview'), ], embeddingModels: [ createEmbeddingModelWithCapabilities( provider.textEmbeddingModel('text-embedding-3-small'), ), ], }, timeout: 30000, customAssertions: { skipUsage: false, errorValidator: (error: APICallError) => { expect(error.message).toMatch(/The model .* does not exist/); }, }, })(); --- File: /ai/examples/ai-core/src/e2e/perplexity.test.ts --- import 'dotenv/config'; import { expect } from 'vitest'; import { perplexity as provider } from '@ai-sdk/perplexity'; import { createFeatureTestSuite, createLanguageModelWithCapabilities, } from './feature-test-suite'; import { APICallError } from '@ai-sdk/provider'; const createChatModel = (modelId: string) => createLanguageModelWithCapabilities(provider(modelId)); createFeatureTestSuite({ name: 'perplexity', models: { invalidModel: provider('no-such-model'), languageModels: [createChatModel('sonar-pro'), createChatModel('sonar')], }, timeout: 30000, customAssertions: { errorValidator: (error: APICallError) => { expect((error.data as any).code).toBe( 'Some requested entity was not found', ); }, }, })(); --- File: /ai/examples/ai-core/src/e2e/raw-chunks.test.ts --- import { openai } from '@ai-sdk/openai'; import { anthropic } from '@ai-sdk/anthropic'; import { google } from '@ai-sdk/google'; import { streamText } from 'ai'; import 'dotenv/config'; import { describe, expect, it, vi } from 'vitest'; describe('Raw Chunks E2E Tests', () => { vi.setConfig({ testTimeout: 30000 }); const providers = [ { name: 'OpenAI', model: openai('gpt-4o-mini') }, { name: 'Anthropic', model: anthropic('claude-3-5-haiku-latest') }, { name: 'Google', model: google('gemini-1.5-flash') }, ]; providers.forEach(({ name, model }) => { describe(`${name} Provider`, () => { it('should include raw chunks when includeRawChunks is enabled', async () => { const result = streamText({ model, prompt: 'Say hello!', includeRawChunks: true, }); const chunks = []; for await (const chunk of result.fullStream) { chunks.push(chunk); } expect(chunks.filter(chunk => chunk.type === 'raw')).toHaveLength(1); }); it('should not include raw chunks when includeRawChunks is disabled', async () => { const result = streamText({ model, prompt: 'Say hello!', includeRawChunks: false, }); const chunks = []; for await (const chunk of result.fullStream) { chunks.push(chunk); } expect(chunks.filter(chunk => chunk.type === 'raw')).toHaveLength(0); }); it('should forward provider-specific raw chunk data', async () => { const result = streamText({ model, prompt: 'Say hello!', includeRawChunks: true, }); const chunks = []; for await (const chunk of result.fullStream) { chunks.push(chunk); } expect(chunks.filter(chunk => chunk.type === 'raw')).toHaveLength(1); }); }); }); }); --- File: /ai/examples/ai-core/src/e2e/togetherai.test.ts --- import 'dotenv/config'; import { expect } from 'vitest'; import { togetherai as provider, TogetherAIErrorData, } from '@ai-sdk/togetherai'; import { APICallError } from 'ai'; import { createEmbeddingModelWithCapabilities, createFeatureTestSuite, createLanguageModelWithCapabilities, } from './feature-test-suite'; const createChatModel = (modelId: string) => createLanguageModelWithCapabilities(provider.chatModel(modelId)); const createCompletionModel = (modelId: string) => createLanguageModelWithCapabilities(provider.completionModel(modelId), [ 'textCompletion', ]); createFeatureTestSuite({ name: 'TogetherAI', models: { invalidModel: provider.chatModel('no-such-model'), languageModels: [ createChatModel('deepseek-ai/DeepSeek-V3'), // no tools, objects, or images createChatModel('meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo'), createChatModel('mistralai/Mistral-7B-Instruct-v0.1'), createChatModel('google/gemma-2b-it'), createChatModel('meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo'), createChatModel('mistralai/Mixtral-8x7B-Instruct-v0.1'), createChatModel('Qwen/Qwen2.5-72B-Instruct-Turbo'), createChatModel('databricks/dbrx-instruct'), createCompletionModel('Qwen/Qwen2.5-Coder-32B-Instruct'), ], embeddingModels: [ createEmbeddingModelWithCapabilities( provider.textEmbeddingModel( 'togethercomputer/m2-bert-80M-8k-retrieval', ), ), createEmbeddingModelWithCapabilities( provider.textEmbeddingModel('BAAI/bge-base-en-v1.5'), ), ], }, timeout: 10000, customAssertions: { skipUsage: true, errorValidator: (error: APICallError) => { expect((error.data as TogetherAIErrorData).error.message).toMatch( /^Unable to access model/, ); }, }, })(); --- File: /ai/examples/ai-core/src/e2e/xai.test.ts --- import 'dotenv/config'; import { expect } from 'vitest'; import { xai as provider, XaiErrorData } from '@ai-sdk/xai'; import { createFeatureTestSuite, createLanguageModelWithCapabilities, } from './feature-test-suite'; import { APICallError } from '@ai-sdk/provider'; const createChatModel = (modelId: string) => createLanguageModelWithCapabilities(provider.chat(modelId)); const createCompletionModel = (modelId: string) => createLanguageModelWithCapabilities(provider.languageModel(modelId), [ 'textCompletion', ]); createFeatureTestSuite({ name: 'xAI', models: { invalidModel: provider.chat('no-such-model'), languageModels: [ createChatModel('grok-4'), createChatModel('grok-3-beta'), createChatModel('grok-3-fast-beta'), createChatModel('grok-3-mini-beta'), createChatModel('grok-3-mini-fast-beta'), createChatModel('grok-beta'), createChatModel('grok-2-1212'), createChatModel('grok-vision-beta'), createChatModel('grok-2-vision-1212'), createCompletionModel('grok-beta'), createCompletionModel('grok-2-1212'), createCompletionModel('grok-vision-beta'), createCompletionModel('grok-2-vision-1212'), ], }, timeout: 30000, customAssertions: { errorValidator: (error: APICallError) => { expect((error.data as XaiErrorData).error.message).toContain('model'); }, }, })(); --- File: /ai/examples/ai-core/src/embed/amazon-bedrock.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { embed } from 'ai'; import 'dotenv/config'; async function main() { const { embedding, usage } = await embed({ model: bedrock.embedding('amazon.titan-embed-text-v2:0'), value: 'sunny day at the beach', }); console.log(embedding); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed/azure.ts --- import { azure } from '@ai-sdk/azure'; import { embed } from 'ai'; import 'dotenv/config'; async function main() { const { embedding, usage } = await embed({ model: azure.embedding('my-embedding-deployment'), value: 'sunny day at the beach', }); console.log(embedding); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed/cohere.ts --- import { cohere } from '@ai-sdk/cohere'; import { embed } from 'ai'; import 'dotenv/config'; async function main() { const { embedding, usage } = await embed({ model: cohere.embedding('embed-multilingual-v3.0'), value: 'sunny day at the beach', }); console.log(embedding); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed/gateway.ts --- import { embed } from 'ai'; import 'dotenv/config'; async function main() { const result = await embed({ model: 'openai/text-embedding-3-small', value: 'sunny day at the beach', }); console.log('Embedding:', result.embedding); console.log('Usage:', result.usage); if (result.providerMetadata) { console.log('\nProvider Metadata:'); console.log(JSON.stringify(result.providerMetadata, null, 2)); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed/google-vertex.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { embed } from 'ai'; import 'dotenv/config'; async function main() { const { embedding, usage } = await embed({ model: vertex.textEmbeddingModel('text-embedding-004'), value: 'sunny day at the beach', }); console.log(embedding); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed/google.ts --- import { google } from '@ai-sdk/google'; import { embed } from 'ai'; import 'dotenv/config'; async function main() { const { embedding, usage } = await embed({ model: google.textEmbeddingModel('gemini-embedding-001'), value: 'sunny day at the beach', }); console.log(embedding); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed/mistral.ts --- import { mistral } from '@ai-sdk/mistral'; import { embed } from 'ai'; import 'dotenv/config'; async function main() { const { embedding, usage } = await embed({ model: mistral.embedding('mistral-embed'), value: 'sunny day at the beach', }); console.log(embedding); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed/openai-compatible-togetherai.ts --- import 'dotenv/config'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { embed } from 'ai'; async function main() { const togetherai = createOpenAICompatible({ baseURL: 'https://api.together.xyz/v1', name: 'togetherai', headers: { Authorization: `Bearer ${process.env.TOGETHER_AI_API_KEY}`, }, }); const model = togetherai.textEmbeddingModel('BAAI/bge-large-en-v1.5'); const { embedding, usage } = await embed({ model, value: 'sunny day at the beach', }); console.log(embedding); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed/openai.ts --- import { openai } from '@ai-sdk/openai'; import { embed } from 'ai'; import 'dotenv/config'; async function main() { const { embedding, usage } = await embed({ model: openai.embedding('text-embedding-3-small'), value: 'sunny day at the beach', }); console.log(embedding); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed/togetherai.ts --- import { togetherai } from '@ai-sdk/togetherai'; import { embed } from 'ai'; import 'dotenv/config'; async function main() { const { embedding, usage } = await embed({ model: togetherai.textEmbeddingModel('BAAI/bge-base-en-v1.5'), value: 'sunny day at the beach', }); console.log(embedding); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed-many/amazon-bedrock.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { embedMany } from 'ai'; import 'dotenv/config'; async function main() { const { embeddings, usage } = await embedMany({ model: bedrock.embedding('amazon.titan-embed-text-v2:0'), values: [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ], }); console.log(embeddings); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed-many/azure.ts --- import { azure } from '@ai-sdk/azure'; import { embedMany } from 'ai'; import 'dotenv/config'; async function main() { const { embeddings, usage } = await embedMany({ model: azure.embedding('my-embedding-deployment'), values: [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ], }); console.log(embeddings); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed-many/cohere.ts --- import { cohere } from '@ai-sdk/cohere'; import { embedMany } from 'ai'; import 'dotenv/config'; async function main() { const { embeddings, usage } = await embedMany({ model: cohere.embedding('embed-multilingual-v3.0'), values: [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ], }); console.log(embeddings); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed-many/gateway.ts --- import { embedMany } from 'ai'; import 'dotenv/config'; async function main() { const result = await embedMany({ model: 'openai/text-embedding-3-large', values: [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ], }); console.log('Embeddings:', result.embeddings); console.log('Usage:', result.usage); if (result.providerMetadata) { console.log('\nProvider Metadata:'); console.log(JSON.stringify(result.providerMetadata, null, 2)); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed-many/google-vertex.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { embedMany } from 'ai'; import 'dotenv/config'; async function main() { const { embeddings, usage } = await embedMany({ model: vertex.textEmbeddingModel('text-embedding-004'), values: [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ], }); console.log(embeddings); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed-many/google.ts --- import { google } from '@ai-sdk/google'; import { embedMany } from 'ai'; import 'dotenv/config'; async function main() { const { embeddings, usage } = await embedMany({ model: google.textEmbeddingModel('gemini-embedding-001'), values: [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ], }); console.log(embeddings); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed-many/mistral.ts --- import { mistral } from '@ai-sdk/mistral'; import { embedMany } from 'ai'; import 'dotenv/config'; async function main() { const { embeddings, usage } = await embedMany({ model: mistral.embedding('mistral-embed'), values: [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ], }); console.log(embeddings); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed-many/openai-compatible-togetherai.ts --- import 'dotenv/config'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { embedMany } from 'ai'; async function main() { const togetherai = createOpenAICompatible({ baseURL: 'https://api.together.xyz/v1', name: 'togetherai', headers: { Authorization: `Bearer ${process.env.TOGETHER_AI_API_KEY}`, }, }); const model = togetherai.textEmbeddingModel('BAAI/bge-large-en-v1.5'); const { embeddings, usage } = await embedMany({ model, values: [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ], }); console.log(embeddings); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed-many/openai-cosine-similarity.ts --- import { openai } from '@ai-sdk/openai'; import { cosineSimilarity, embedMany } from 'ai'; import 'dotenv/config'; async function main() { const { embeddings } = await embedMany({ model: openai.embedding('text-embedding-3-small'), values: ['sunny day at the beach', 'rainy afternoon in the city'], }); console.log( `cosine similarity: ${cosineSimilarity(embeddings[0], embeddings[1])}`, ); } main().catch(console.error); --- File: /ai/examples/ai-core/src/embed-many/openai.ts --- import { openai } from '@ai-sdk/openai'; import { embedMany } from 'ai'; import 'dotenv/config'; async function main() { const { embeddings, usage } = await embedMany({ model: openai.embedding('text-embedding-3-small'), values: [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ], }); console.log(embeddings); console.log(usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/amazon-bedrock.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const result = await generateImage({ model: bedrock.imageModel('amazon.nova-canvas-v1:0'), prompt: 'A salamander at dusk in a forest pond with fireflies in the background, in the style of anime', size: '512x512', seed: 42, providerOptions: { bedrock: { quality: 'premium', }, }, }); await presentImages(result.images); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/azure.ts --- import { azure } from '@ai-sdk/azure'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const { image } = await generateImage({ model: azure.imageModel('dalle-3'), // Use your own deployment prompt: 'Santa Claus driving a Cadillac', }); await presentImages([image]); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/deepinfra.ts --- import { deepinfra } from '@ai-sdk/deepinfra'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const result = await generateImage({ model: deepinfra.image('black-forest-labs/FLUX-1-schnell'), prompt: 'A resplendent quetzal mid flight amidst raindrops', }); await presentImages(result.images); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/fal-kontext.ts --- import { fal } from '@ai-sdk/fal'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const { images } = await generateImage({ model: fal.image('fal-ai/flux-pro/kontext/max'), prompt: 'Put a donut next to the flour.', providerOptions: { fal: { image_url: 'https://v3.fal.media/files/rabbit/rmgBxhwGYb2d3pl3x9sKf_output.png', }, }, }); await presentImages(images); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/fal-photon.ts --- import { fal } from '@ai-sdk/fal'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const { images } = await generateImage({ model: fal.image('fal-ai/luma-photon'), prompt: 'A hyrax atop a stump in a forest among fireflies at dusk', }); await presentImages(images); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/fal-recraft.ts --- import { fal } from '@ai-sdk/fal'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const { images } = await generateImage({ model: fal.image('fal-ai/recraft-v3'), prompt: 'A Sumatran rhino meandering through a dense forest among fireflies at dusk', }); await presentImages(images); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/fal.ts --- import { fal } from '@ai-sdk/fal'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const { images } = await generateImage({ model: fal.image('fal-ai/flux/schnell'), prompt: 'A cat wearing an intricate robe while gesticulating wildly, in the style of 80s pop art', }); await presentImages(images); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/fireworks.ts --- import { fireworks } from '@ai-sdk/fireworks'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const result = await generateImage({ model: fireworks.image( 'accounts/fireworks/models/stable-diffusion-xl-1024-v1-0', ), prompt: 'A burrito launched through a tunnel', size: '1024x1024', seed: 0, n: 2, providerOptions: { fireworks: { // https://fireworks.ai/models/fireworks/stable-diffusion-xl-1024-v1-0/playground cfg_scale: 10, steps: 30, }, }, }); await presentImages(result.images); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/google-vertex.ts --- import { GoogleVertexImageProviderOptions, vertex, } from '@ai-sdk/google-vertex'; import { experimental_generateImage as generateImage } from 'ai'; import 'dotenv/config'; import { presentImages } from '../lib/present-image'; async function main() { const { image } = await generateImage({ model: vertex.image('imagen-3.0-generate-002'), prompt: 'A burrito launched through a tunnel', aspectRatio: '1:1', providerOptions: { vertex: { addWatermark: false, } satisfies GoogleVertexImageProviderOptions, }, }); await presentImages([image]); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/google.ts --- import { google, GoogleGenerativeAIImageProviderOptions } from '@ai-sdk/google'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const { image } = await generateImage({ model: google.image('imagen-3.0-generate-002'), prompt: 'A burrito launched through a tunnel', aspectRatio: '1:1', providerOptions: { google: { personGeneration: 'dont_allow', } satisfies GoogleGenerativeAIImageProviderOptions, }, }); await presentImages([image]); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/luma-character-reference.ts --- import { luma } from '@ai-sdk/luma'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const result = await generateImage({ model: luma.image('photon-flash-1'), prompt: 'A woman with a cat riding a broomstick in a forest', aspectRatio: '1:1', providerOptions: { luma: { // https://docs.lumalabs.ai/docs/image-generation#character-reference character_ref: { identity0: { images: [ 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/future-me-8hcBWcZOkbE53q3gshhEm16S87qDpF.jpeg', ], }, }, }, }, }); await presentImages(result.images); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/luma-image-reference.ts --- import { luma } from '@ai-sdk/luma'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const result = await generateImage({ model: luma.image('photon-flash-1'), prompt: 'A salamander at dusk in a forest pond, in the style of ukiyo-e', aspectRatio: '1:1', providerOptions: { luma: { // https://docs.lumalabs.ai/docs/image-generation#image-reference image_ref: [ { url: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/future-me-8hcBWcZOkbE53q3gshhEm16S87qDpF.jpeg', weight: 0.8, }, ], }, }, }); await presentImages(result.images); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/luma-modify-image.ts --- import { luma } from '@ai-sdk/luma'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const result = await generateImage({ model: luma.image('photon-flash-1'), prompt: 'transform the bike to a boat', aspectRatio: '1:1', providerOptions: { luma: { // https://docs.lumalabs.ai/docs/image-generation#modify-image modify_image_ref: { url: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/future-me-8hcBWcZOkbE53q3gshhEm16S87qDpF.jpeg', weight: 1.0, }, }, }, }); await presentImages(result.images); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/luma-style-reference.ts --- import { luma } from '@ai-sdk/luma'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const result = await generateImage({ model: luma.image('photon-flash-1'), prompt: 'A blue cream Persian cat launching its website on Vercel', aspectRatio: '1:1', providerOptions: { luma: { // https://docs.lumalabs.ai/docs/image-generation#style-reference style_ref: [ { url: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/future-me-8hcBWcZOkbE53q3gshhEm16S87qDpF.jpeg', weight: 0.8, }, ], }, }, }); await presentImages(result.images); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/luma.ts --- import { luma } from '@ai-sdk/luma'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const result = await generateImage({ model: luma.image('photon-flash-1'), prompt: 'A salamander at dusk in a forest pond, in the style of ukiyo-e', aspectRatio: '1:1', providerOptions: { luma: { // add'l options here }, }, }); await presentImages(result.images); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/openai-gpt-image.ts --- import { openai } from '@ai-sdk/openai'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const { image } = await generateImage({ model: openai.image('gpt-image-1'), prompt: 'A salamander at sunrise in a forest pond in the Seychelles.', providerOptions: { openai: { quality: 'high' }, }, }); await presentImages([image]); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/openai-many.ts --- import { openai } from '@ai-sdk/openai'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const { images } = await generateImage({ model: openai.image('dall-e-3'), n: 3, // 3 calls; dall-e-3 can only generate 1 image at a time prompt: 'Santa Claus driving a Cadillac', }); await presentImages(images); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/openai.ts --- import { openai } from '@ai-sdk/openai'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const prompt = 'Santa Claus driving a Cadillac'; const result = await generateImage({ model: openai.image('dall-e-3'), prompt, }); // @ts-expect-error const revisedPrompt = result.providerMetadata.openai.images[0]?.revisedPrompt; console.log({ prompt, revisedPrompt, }); await presentImages([result.image]); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/replicate-1.ts --- import { replicate } from '@ai-sdk/replicate'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const { image } = await generateImage({ model: replicate.image('black-forest-labs/flux-schnell'), prompt: 'The Loch Ness Monster getting a manicure', }); await presentImages([image]); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/replicate-2.ts --- import { replicate } from '@ai-sdk/replicate'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const { image } = await generateImage({ model: replicate.image('black-forest-labs/flux-schnell'), prompt: 'The Loch Ness Monster getting a manicure', aspectRatio: '16:9', }); await presentImages([image]); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/replicate-3.ts --- import { replicate } from '@ai-sdk/replicate'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const { image } = await generateImage({ model: replicate.image('recraft-ai/recraft-v3'), prompt: 'The Loch Ness Monster getting a manicure', size: '1365x1024', providerOptions: { replicate: { style: 'realistic_image', }, }, }); await presentImages([image]); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/replicate-versioned.ts --- import { replicate } from '@ai-sdk/replicate'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const { image } = await generateImage({ model: replicate.image( 'bytedance/sdxl-lightning-4step:5599ed30703defd1d160a25a63321b4dec97101d98b4674bcc56e41f62f35637', ), prompt: 'The Loch Ness Monster getting a manicure', }); await presentImages([image]); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/togetherai.ts --- import { togetherai } from '@ai-sdk/togetherai'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const result = await generateImage({ model: togetherai.image('black-forest-labs/FLUX.1-dev'), prompt: 'A delighted resplendent quetzal mid flight amidst raindrops', size: '1024x1024', providerOptions: { togetherai: { // Together AI specific options steps: 40, }, }, }); await presentImages(result.images); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/xai-many.ts --- import { xai } from '@ai-sdk/xai'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const { images } = await generateImage({ model: xai.image('grok-2-image'), n: 3, prompt: 'A chicken flying into the sunset in the style of anime.', }); await presentImages(images); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-image/xai.ts --- import { xai } from '@ai-sdk/xai'; import { experimental_generateImage as generateImage } from 'ai'; import { presentImages } from '../lib/present-image'; import 'dotenv/config'; async function main() { const { image } = await generateImage({ model: xai.image('grok-2-image'), prompt: 'A salamander at dusk in a forest pond surrounded by fireflies.', }); await presentImages([image]); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/amazon-bedrock.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: bedrock('anthropic.claude-3-5-sonnet-20240620-v1:0'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/anthropic.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: anthropic('claude-3-5-sonnet-20240620'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/azure.ts --- import { azure } from '@ai-sdk/azure'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: azure('v0-gpt-35-turbo'), // use your own deployment schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/cohere.ts --- import { cohere } from '@ai-sdk/cohere'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: cohere('command-a-03-2025'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object.recipe, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/fireworks.ts --- import { fireworks } from '@ai-sdk/fireworks'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: fireworks('accounts/fireworks/models/firefunction-v1'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object.recipe, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/gateway.ts --- import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: 'xai/grok-3-beta', schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object.recipe, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/google-caching.ts --- import 'dotenv/config'; import { google } from '@ai-sdk/google'; import { generateObject } from 'ai'; import fs from 'node:fs'; import { z } from 'zod'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); async function main() { const result1 = await generateObject({ model: google('gemini-2.5-flash'), prompt: errorMessage, schema: z.object({ error: z.string(), stack: z.string(), }), }); console.log(result1.object); console.log(result1.providerMetadata?.google); // e.g. // { // groundingMetadata: null, // safetyRatings: null, // usageMetadata: { // thoughtsTokenCount: 1124, // promptTokenCount: 2152, // candidatesTokenCount: 916, // totalTokenCount: 4192 // } // } const result2 = await generateObject({ model: google('gemini-2.5-flash'), prompt: errorMessage, schema: z.object({ error: z.string(), stack: z.string(), }), }); console.log(result2.object); console.log(result2.providerMetadata?.google); // e.g. // { // groundingMetadata: null, // safetyRatings: null, // usageMetadata: { // cachedContentTokenCount: 1880, // thoughtsTokenCount: 2024, // promptTokenCount: 2152, // candidatesTokenCount: 1072, // totalTokenCount: 5248 // } // } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/google-complex-1.ts --- import { google } from '@ai-sdk/google'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { // split schema support: const Person = z.object({ name: z.string() }); const Team = z.object({ developers: z.array(Person), designers: z.array(Person), }); const result = await generateObject({ model: google('gemini-exp-1206'), schema: Team, prompt: 'Generate a fake team of developers and designers.', }); console.log(JSON.stringify(result.object, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/google-complex-2.ts --- import { google } from '@ai-sdk/google'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { // enum support: const result = await generateObject({ model: google('gemini-exp-1206'), schema: z.object({ title: z.string(), kind: z.enum(['text', 'code', 'image']), }), prompt: 'Generate a software artifact.', }); console.log(JSON.stringify(result.object, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/google-enum.ts --- import { google } from '@ai-sdk/google'; import { generateObject } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateObject({ model: google('gemini-1.5-pro-latest'), output: 'enum', enum: ['action', 'comedy', 'drama', 'horror', 'sci-fi'], prompt: 'Classify the genre of this movie plot: ' + '"A group of astronauts travel through a wormhole in search of a ' + 'new habitable planet for humanity."', }); console.log(result.object); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/google-gemini-files.ts --- import { GoogleAIFileManager } from '@google/generative-ai/server'; import { google } from '@ai-sdk/google'; import { generateObject } from 'ai'; import path from 'path'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const fileManager = new GoogleAIFileManager( process.env.GOOGLE_GENERATIVE_AI_API_KEY!, ); const filePath = path.resolve(__dirname, '../../data/ai.pdf'); const geminiFile = await fileManager.uploadFile(filePath, { name: `ai-${Math.random().toString(36).substring(7)}`, mimeType: 'application/pdf', }); const { object: summary } = await generateObject({ model: google('gemini-1.5-pro-latest'), schema: z.object({ title: z.string(), keyPoints: z.array(z.string()), }), messages: [ { role: 'user', content: [ { type: 'text', text: 'Extract title and key points from the PDF.', }, { type: 'file', data: geminiFile.file.uri, mediaType: geminiFile.file.mimeType, }, ], }, ], }); console.log(summary); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/google-no-structured-output.ts --- import { google } from '@ai-sdk/google'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: google('gemini-1.5-pro-latest'), providerOptions: { google: { structuredOutputs: false, }, }, schema: z.object({ name: z.string(), age: z.number(), contact: z.union([ z.object({ type: z.literal('email'), value: z.string(), }), z.object({ type: z.literal('phone'), value: z.string(), }), ]), occupation: z.union([ z.object({ type: z.literal('employed'), company: z.string(), position: z.string(), }), z.object({ type: z.literal('student'), school: z.string(), grade: z.number(), }), z.object({ type: z.literal('unemployed'), }), ]), }), prompt: 'Generate an example person for testing.', }); console.log(JSON.stringify(result.object, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/google-pdf-url.ts --- import { google } from '@ai-sdk/google'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const { object: summary } = await generateObject({ model: google('gemini-1.5-pro-latest'), schema: z.object({ title: z.string(), authors: z.array(z.string()), keyPoints: z.array(z.string()), }), messages: [ { role: 'user', content: [ { type: 'text', text: 'Extract title, authors, and key points from the PDF.', }, { type: 'file', data: 'https://user.phil.hhu.de/~cwurm/wp-content/uploads/' + '2020/01/7181-attention-is-all-you-need.pdf', mediaType: 'application/pdf', }, ], }, ], }); console.log(summary); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/google-vertex-anthropic.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateObject } from 'ai'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/google-vertex.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: vertex('gemini-1.5-pro'), schema: z.object({ recipe: z.object({ name: z.literal('Lasagna'), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object.recipe, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/google.ts --- import { google } from '@ai-sdk/google'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: google('gemini-2.0-flash'), providerOptions: { google: { structuredOutputs: true, }, }, schema: z.object({ name: z.string(), // nullable number with description age: z.number().nullable().describe('Age of the person.'), // object contact: z.object({ type: z.literal('email'), value: z.string(), }), // nullable enum level: z.enum(['L1', 'L2', 'L3']).nullable(), }), prompt: 'Generate an example person for testing.', }); console.log(JSON.stringify(result.object, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/groq-kimi-k2-structured-outputs.ts --- import { groq } from '@ai-sdk/groq'; import { generateObject } from 'ai'; import { z } from 'zod'; import 'dotenv/config'; async function main() { const result = await generateObject({ model: groq('moonshotai/kimi-k2-instruct'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.string()), instructions: z.array(z.string()), }), }), prompt: 'Generate a simple pasta recipe.', }); console.log(JSON.stringify(result.object, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/groq.ts --- import { groq } from '@ai-sdk/groq'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: groq('llama-3.1-70b-versatile'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object.recipe, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/mistral.ts --- import { mistral } from '@ai-sdk/mistral'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: mistral('open-mistral-7b'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object.recipe, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/mock-error.ts --- import { generateObject, NoObjectGeneratedError } from 'ai'; import { MockLanguageModelV2 } from 'ai/test'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { try { await generateObject({ model: new MockLanguageModelV2({ doGenerate: async () => ({ warnings: [], content: [{ type: 'text', text: `{"content":"Hello broken json` }], response: { id: 'id-1', timestamp: new Date(123), modelId: 'model-1', }, finishReason: 'stop', usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30, }, }), }), schema: z.object({ content: z.string() }), prompt: 'Hello, test!', }); } catch (error) { if (NoObjectGeneratedError.isInstance(error)) { console.log('NoObjectGeneratedError'); console.log('Cause:', error.cause); console.log('Text:', error.text); console.log('Response:', error.response); console.log('Usage:', error.usage); } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/mock-repair-add-close.ts --- import { generateObject, JSONParseError } from 'ai'; import { MockLanguageModelV2 } from 'ai/test'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: new MockLanguageModelV2({ doGenerate: async () => ({ usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30, }, warnings: [], finishReason: 'tool-calls', content: [ { type: 'text', text: `{ "content": "provider metadata test"` }, ], }), }), schema: z.object({ content: z.string() }), prompt: 'What are the tourist attractions in San Francisco?', experimental_repairText: async ({ text, error }) => { if (error instanceof JSONParseError) { return text + '}'; } return null; }, }); console.log('Object after repair:'); console.log(result.object); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/mock.ts --- import { generateObject } from 'ai'; import { MockLanguageModelV2 } from 'ai/test'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const { object, usage } = await generateObject({ model: new MockLanguageModelV2({ doGenerate: async () => ({ content: [{ type: 'text', text: `{"content":"Hello, world!"}` }], finishReason: 'stop', usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30, }, warnings: [], }), }), schema: z.object({ content: z.string() }), prompt: 'Hello, test!', }); console.log(object); console.log(); console.log('Usage:', usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/nim.ts --- import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { generateObject } from 'ai'; import { z } from 'zod/v4'; import 'dotenv/config'; async function main() { const nim = createOpenAICompatible({ baseURL: 'https://integrate.api.nvidia.com/v1', name: 'nim', headers: { Authorization: `Bearer ${process.env.NIM_API_KEY}`, }, }); const model = nim.chatModel('meta/llama-3.3-70b-instruct'); const result = await generateObject({ model, schema: z.array( z.object({ name: z.string(), breed: z.string(), }), ), prompt: 'Generate 10 cat names and breeds for a fictional book about a world where cats rule shrimp.', }); console.log(JSON.stringify(result.object, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/openai-array.ts --- import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: openai('gpt-4o-2024-08-06'), output: 'array', schema: z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), prompt: 'Generate 3 hero descriptions for a fantasy role playing game.', }); console.log(JSON.stringify(result.object, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/openai-compatible-togetherai.ts --- import 'dotenv/config'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { generateObject } from 'ai'; import { z } from 'zod/v4'; async function main() { const togetherai = createOpenAICompatible({ baseURL: 'https://api.together.xyz/v1', name: 'togetherai', headers: { Authorization: `Bearer ${process.env.TOGETHER_AI_API_KEY}`, }, }); const model = togetherai.chatModel('mistralai/Mistral-7B-Instruct-v0.1'); const result = await generateObject({ model, schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object.recipe, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/openai-date-parsing.ts --- import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const { object: { events }, } = await generateObject({ model: openai('gpt-4-turbo'), schema: z.object({ events: z.array( z.object({ date: z .string() .date() .transform(value => new Date(value)), event: z.string(), }), ), }), prompt: 'List 5 important events from the year 2000.', }); console.log(events); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/openai-enum.ts --- import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateObject({ model: openai('gpt-4o-mini'), output: 'enum', enum: ['action', 'comedy', 'drama', 'horror', 'sci-fi'], prompt: 'Classify the genre of this movie plot: ' + '"A group of astronauts travel through a wormhole in search of a ' + 'new habitable planet for humanity."', }); console.log(result.object); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/openai-full-result.ts --- import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: openai('gpt-4o-mini'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string() }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/openai-multimodal.ts --- import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; import { z } from 'zod/v4'; async function main() { const { object } = await generateObject({ model: openai('gpt-4-turbo'), schema: z.object({ artwork: z.object({ description: z.string(), style: z.string(), review: z.string(), }), }), system: 'You are an art critic reviewing a piece of art.', messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail and review it' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png') }, ], }, ], }); console.log(JSON.stringify(object.artwork, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/openai-no-schema.ts --- import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateObject({ model: openai('gpt-4o-2024-08-06'), output: 'no-schema', prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/openai-raw-json-schema.ts --- import { openai } from '@ai-sdk/openai'; import { generateObject, jsonSchema } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateObject({ model: openai('gpt-4-turbo'), schema: jsonSchema<{ recipe: { name: string; ingredients: { name: string; amount: string }[]; steps: string[]; }; }>({ type: 'object', properties: { recipe: { type: 'object', properties: { name: { type: 'string' }, ingredients: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, amount: { type: 'string' }, }, required: ['name', 'amount'], }, }, steps: { type: 'array', items: { type: 'string' }, }, }, required: ['name', 'ingredients', 'steps'], }, }, required: ['recipe'], }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object.recipe, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/openai-reasoning.ts --- import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: openai('o1'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object.recipe, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/openai-request-body.ts --- import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const { request } = await generateObject({ model: openai('gpt-4o-mini'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log('REQUEST BODY'); console.log(request.body); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/openai-responses.ts --- import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: openai.responses('gpt-4o-mini'), schemaDescription: 'Generate a lasagna recipe.', schemaName: 'recipe', schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object.recipe, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/openai-store-generation.ts --- import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: openai('gpt-4o-mini'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', providerOptions: { openai: { store: true, metadata: { custom: 'value', }, }, }, }); console.log(JSON.stringify(result.object.recipe, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/openai-structured-outputs-name-description.ts --- import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: openai('gpt-4o-2024-08-06'), schemaName: 'recipe', schemaDescription: 'A recipe for lasagna.', schema: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/openai-valibot.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { valibotSchema } from '@ai-sdk/valibot'; import { generateObject } from 'ai'; import 'dotenv/config'; import * as v from 'valibot'; async function main() { const result = await generateObject({ model: anthropic('claude-3-5-sonnet-20240620'), schema: valibotSchema( v.object({ recipe: v.object({ name: v.string(), ingredients: v.array( v.object({ name: v.string(), amount: v.string(), }), ), steps: v.array(v.string()), }), }), ), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object.recipe, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/openai.ts --- import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: openai('gpt-4o-mini'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object.recipe, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/perplexity.ts --- import 'dotenv/config'; import { perplexity } from '@ai-sdk/perplexity'; import { generateObject, generateText } from 'ai'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: perplexity('sonar-pro'), prompt: 'What has happened in San Francisco recently?', providerOptions: { perplexity: { search_recency_filter: 'week', }, }, output: 'array', schema: z.object({ title: z.string(), summary: z.string(), }), }); console.log(result.object); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); console.log('Metadata:', result.providerMetadata); } main().catch((error: Error) => { console.error(JSON.stringify(error, null, 2)); }); --- File: /ai/examples/ai-core/src/generate-object/togetherai.ts --- import { togetherai } from '@ai-sdk/togetherai'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: togetherai.chatModel('mistralai/Mistral-7B-Instruct-v0.1'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object.recipe, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/vercel.ts --- import { vercel } from '@ai-sdk/vercel'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: vercel('v0-1.5-md'), schema: z.object({ button: z.object({ element: z.string(), baseStyles: z.object({ padding: z.string(), borderRadius: z.string(), border: z.string(), backgroundColor: z.string(), color: z.string(), cursor: z.string(), }), hoverStyles: z.object({ backgroundColor: z.string(), transform: z.string().optional(), }), }), }), prompt: 'Generate CSS styles for a modern primary button component.', }); console.log(JSON.stringify(result.object.button, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/xai-structured-outputs-name-description.ts --- import { xai } from '@ai-sdk/xai'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: xai('grok-3-beta'), schemaName: 'recipe', schemaDescription: 'A recipe for lasagna.', schema: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-object/xai.ts --- import { xai } from '@ai-sdk/xai'; import { generateObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateObject({ model: xai('grok-3-beta'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); console.log(JSON.stringify(result.object.recipe, null, 2)); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-speech/azure.ts --- import { azure } from '@ai-sdk/azure'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import 'dotenv/config'; import { saveAudioFile } from '../lib/save-audio'; async function main() { const result = await generateSpeech({ model: azure.speech('tts-1'), text: 'Hello from the AI SDK!', }); console.log('Audio:', result.audio); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); await saveAudioFile(result.audio); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-speech/hume-instructions.ts --- import { hume } from '@ai-sdk/hume'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import 'dotenv/config'; import { saveAudioFile } from '../lib/save-audio'; async function main() { const result = await generateSpeech({ model: hume.speech(), text: 'Hello from the AI SDK!', instructions: 'Speak in a slow and steady tone', }); console.log('Audio:', result.audio); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); await saveAudioFile(result.audio); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-speech/hume-language.ts --- import { hume } from '@ai-sdk/hume'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import 'dotenv/config'; import { saveAudioFile } from '../lib/save-audio'; async function main() { const result = await generateSpeech({ model: hume.speech(), text: 'Hello from the AI SDK!', language: 'en', }); console.log('Audio:', result.audio); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); await saveAudioFile(result.audio); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-speech/hume-speed.ts --- import { hume } from '@ai-sdk/hume'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import 'dotenv/config'; import { saveAudioFile } from '../lib/save-audio'; async function main() { const result = await generateSpeech({ model: hume.speech(), text: 'Hello from the AI SDK!', speed: 1.5, }); console.log('Audio:', result.audio); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); await saveAudioFile(result.audio); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-speech/hume-voice.ts --- import { hume } from '@ai-sdk/hume'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import 'dotenv/config'; import { saveAudioFile } from '../lib/save-audio'; async function main() { const result = await generateSpeech({ model: hume.speech(), text: 'Hello from the AI SDK!', voice: 'nova', }); console.log('Audio:', result.audio); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); await saveAudioFile(result.audio); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-speech/hume.ts --- import { hume } from '@ai-sdk/hume'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import 'dotenv/config'; import { saveAudioFile } from '../lib/save-audio'; async function main() { const result = await generateSpeech({ model: hume.speech(), text: 'Hello from the AI SDK!', }); console.log('Audio:', result.audio); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); await saveAudioFile(result.audio); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-speech/lmnt-language.ts --- import { lmnt } from '@ai-sdk/lmnt'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import 'dotenv/config'; import { saveAudioFile } from '../lib/save-audio'; async function main() { const result = await generateSpeech({ model: lmnt.speech('aurora'), text: 'Hola desde el AI SDK!', language: 'es', // Spanish using standardized parameter }); console.log('Audio:', result.audio); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); await saveAudioFile(result.audio); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-speech/lmnt-speed.ts --- import { lmnt } from '@ai-sdk/lmnt'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import 'dotenv/config'; import { saveAudioFile } from '../lib/save-audio'; async function main() { const result = await generateSpeech({ model: lmnt.speech('aurora'), text: 'Hello from the AI SDK!', speed: 1.5, }); console.log('Audio:', result.audio); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); await saveAudioFile(result.audio); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-speech/lmnt-voice.ts --- import { lmnt } from '@ai-sdk/lmnt'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import 'dotenv/config'; import { saveAudioFile } from '../lib/save-audio'; async function main() { const result = await generateSpeech({ model: lmnt.speech('aurora'), text: 'Hello from the AI SDK!', voice: 'nova', }); console.log('Audio:', result.audio); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); await saveAudioFile(result.audio); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-speech/lmnt.ts --- import { lmnt } from '@ai-sdk/lmnt'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import 'dotenv/config'; import { saveAudioFile } from '../lib/save-audio'; async function main() { const result = await generateSpeech({ model: lmnt.speech('aurora'), text: 'Hello from the AI SDK!', }); console.log('Audio:', result.audio); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); await saveAudioFile(result.audio); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-speech/openai-instructions.ts --- import { openai } from '@ai-sdk/openai'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import 'dotenv/config'; import { saveAudioFile } from '../lib/save-audio'; async function main() { const result = await generateSpeech({ model: openai.speech('tts-1'), text: 'Hello from the AI SDK!', instructions: 'Speak in a slow and steady tone', }); console.log('Audio:', result.audio); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); await saveAudioFile(result.audio); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-speech/openai-language.ts --- import { openai } from '@ai-sdk/openai'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import 'dotenv/config'; import { saveAudioFile } from '../lib/save-audio'; async function main() { const result = await generateSpeech({ model: openai.speech('tts-1'), text: 'Hello from the AI SDK!', language: 'en', }); console.log('Audio:', result.audio); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); await saveAudioFile(result.audio); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-speech/openai-speed.ts --- import { openai } from '@ai-sdk/openai'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import 'dotenv/config'; import { saveAudioFile } from '../lib/save-audio'; async function main() { const result = await generateSpeech({ model: openai.speech('tts-1'), text: 'Hello from the AI SDK!', speed: 1.5, }); console.log('Audio:', result.audio); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); await saveAudioFile(result.audio); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-speech/openai-voice.ts --- import { openai } from '@ai-sdk/openai'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import 'dotenv/config'; import { saveAudioFile } from '../lib/save-audio'; async function main() { const result = await generateSpeech({ model: openai.speech('tts-1'), text: 'Hello from the AI SDK!', voice: 'nova', }); console.log('Audio:', result.audio); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); await saveAudioFile(result.audio); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-speech/openai.ts --- import { openai } from '@ai-sdk/openai'; import { experimental_generateSpeech as generateSpeech } from 'ai'; import 'dotenv/config'; import { saveAudioFile } from '../lib/save-audio'; async function main() { const result = await generateSpeech({ model: openai.speech('tts-1'), text: 'Hello from the AI SDK!', }); console.log('Audio:', result.audio); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); await saveAudioFile(result.audio); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-api-key.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { console.log('=== Amazon Bedrock API Key Authentication Example ===\n'); // Example 1: Using API key via environment variable (AWS_BEARER_TOKEN_BEDROCK) // This is the recommended approach for production applications console.log( 'Example 1: Using API key from environment variable (AWS_BEARER_TOKEN_BEDROCK)', ); try { const result1 = await generateText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), prompt: 'Write a haiku about API keys.', // Note: API key is automatically loaded from AWS_BEARER_TOKEN_BEDROCK environment variable }); console.log('Generated haiku:', result1.text); console.log('Token usage:', result1.usage); console.log('Finish reason:', result1.finishReason); } catch (error) { console.log( 'Error (expected if AWS_BEARER_TOKEN_BEDROCK not set):', (error as Error).message, ); } console.log('\n' + '='.repeat(60) + '\n'); // Example 2: Using API key directly in provider configuration // This demonstrates how to pass the API key directly (not recommended for production) console.log('Example 2: Using API key directly in provider configuration'); // For demonstration purposes - in real applications, load from secure environment const exampleApiKey = process.env.AWS_BEARER_TOKEN_BEDROCK || 'your-api-key-here'; try { // Create provider with explicit API key const { createAmazonBedrock } = await import('@ai-sdk/amazon-bedrock'); const bedrockWithApiKey = createAmazonBedrock({ apiKey: exampleApiKey, region: 'us-east-1', // Optional: specify region }); const result2 = await generateText({ model: bedrockWithApiKey('anthropic.claude-3-haiku-20240307-v1:0'), prompt: 'Explain the benefits of API key authentication over AWS SigV4.', }); console.log('Generated explanation:', result2.text); console.log('Token usage:', result2.usage); } catch (error) { console.log( 'Error (expected if API key not valid):', (error as Error).message, ); } console.log('\n' + '='.repeat(60) + '\n'); // Example 3: Comparison with SigV4 authentication console.log('Example 3: Comparison - API Key vs SigV4 Authentication'); console.log(` API Key Authentication (Simpler): - Set AWS_BEARER_TOKEN_BEDROCK environment variable - No need for AWS credentials (access key, secret key, session token) - Simpler configuration and setup - Bearer token authentication in HTTP headers SigV4 Authentication (Traditional AWS): - Requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY - Optional AWS_SESSION_TOKEN for temporary credentials - More complex request signing process - Full AWS IAM integration and policies API Key authentication is ideal for: - Simplified deployment scenarios - Applications that don't need full AWS IAM integration - Easier credential management - Reduced complexity in authentication flow `); // Example 4: Error handling and fallback console.log('Example 4: Demonstrating fallback behavior'); try { // This will use API key if AWS_BEARER_TOKEN_BEDROCK is set, // otherwise fall back to SigV4 authentication const result4 = await generateText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), prompt: 'Write a short poem about authentication methods.', }); console.log('Generated poem:', result4.text); console.log( 'Authentication method used: API Key or SigV4 (automatic fallback)', ); } catch (error) { console.log('Error:', (error as Error).message); console.log( 'Make sure either AWS_BEARER_TOKEN_BEDROCK or AWS credentials are configured', ); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-cache-point-assistant.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); async function main() { const result = await generateText({ model: bedrock('anthropic.claude-3-5-sonnet-20241022-v2:0'), messages: [ { role: 'assistant', content: [ { type: 'text', text: 'You are a JavaScript expert.', }, { type: 'text', text: `Error message: ${errorMessage}`, }, ], providerOptions: { bedrock: { cachePoint: { type: 'default' } } }, }, { role: 'user', content: [ { type: 'text', text: 'Explain the error message.', }, ], }, ], }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Cache token usage:', result.providerMetadata?.bedrock?.usage); console.log('Finish reason:', result.finishReason); console.log('Response headers:', result.response.headers); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-cache-point-system.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); async function main() { const result = await generateText({ model: bedrock('anthropic.claude-3-5-sonnet-20241022-v2:0'), maxOutputTokens: 512, messages: [ { role: 'system', content: `You are a helpful assistant. You may be asked about ${errorMessage}.`, providerOptions: { bedrock: { cachePoint: { type: 'default' } }, }, }, { role: 'user', content: `Explain the error message`, }, ], }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Cache token usage:', result.providerMetadata?.bedrock?.usage); console.log('Finish reason:', result.finishReason); console.log('Response headers:', result.response.headers); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-cache-point-tool-call.ts --- import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { bedrock } from '@ai-sdk/amazon-bedrock'; const weatherTool = tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), // location below is inferred to be a string: execute: async ({ location }) => ({ location, temperature: weatherData[location], }), }); const weatherData: Record<string, number> = { 'New York': 72.4, 'Los Angeles': 84.2, Chicago: 68.9, Houston: 89.7, Phoenix: 95.6, Philadelphia: 71.3, 'San Antonio': 88.4, 'San Diego': 76.8, Dallas: 86.5, 'San Jose': 75.2, Austin: 87.9, Jacksonville: 83.6, 'Fort Worth': 85.7, Columbus: 69.8, 'San Francisco': 68.4, Charlotte: 77.3, Indianapolis: 70.6, Seattle: 65.9, Denver: 71.8, 'Washington DC': 74.5, Boston: 69.7, 'El Paso': 91.2, Detroit: 67.8, Nashville: 78.4, Portland: 66.7, Memphis: 81.3, 'Oklahoma City': 82.9, 'Las Vegas': 93.4, Louisville: 75.6, Baltimore: 73.8, Milwaukee: 66.5, Albuquerque: 84.7, Tucson: 92.3, Fresno: 87.2, Sacramento: 82.5, Mesa: 94.8, 'Kansas City': 77.9, Atlanta: 80.6, Miami: 88.3, Raleigh: 76.4, Omaha: 73.5, 'Colorado Springs': 70.2, 'Long Beach': 79.8, 'Virginia Beach': 78.1, Oakland: 71.4, Minneapolis: 65.8, Tulsa: 81.7, Arlington: 85.3, Tampa: 86.9, 'New Orleans': 84.5, Wichita: 79.4, Cleveland: 68.7, Bakersfield: 88.6, Aurora: 72.3, Anaheim: 81.5, Honolulu: 84.9, 'Santa Ana': 80.7, Riverside: 89.2, 'Corpus Christi': 87.6, Lexington: 74.8, Henderson: 92.7, Stockton: 83.9, 'Saint Paul': 66.2, Cincinnati: 72.9, Pittsburgh: 70.4, Greensboro: 75.9, Anchorage: 52.3, Plano: 84.8, Lincoln: 74.2, Orlando: 85.7, Irvine: 78.9, Newark: 71.6, Toledo: 69.3, Durham: 77.1, 'Chula Vista': 77.4, 'Fort Wayne': 71.2, 'Jersey City': 72.7, 'St. Petersburg': 85.4, Laredo: 90.8, Madison: 67.3, Chandler: 93.6, Buffalo: 66.8, Lubbock: 83.2, Scottsdale: 94.1, Reno: 76.5, Glendale: 92.8, Gilbert: 93.9, 'Winston-Salem': 76.2, Irving: 85.1, Hialeah: 87.8, Garland: 84.6, Fremont: 73.9, Boise: 75.3, Richmond: 76.7, 'Baton Rouge': 83.7, Spokane: 67.4, 'Des Moines': 72.1, Tacoma: 66.3, 'San Bernardino': 88.1, Modesto: 84.3, Fontana: 87.4, 'Santa Clarita': 82.6, Birmingham: 81.9, }; async function main() { const result = await generateText({ model: bedrock('anthropic.claude-3-5-sonnet-20241022-v2:0'), tools: { weather: weatherTool, }, prompt: 'What is the weather in San Francisco?', // TODO: need a way to set cachePoint on `tools`. providerOptions: { bedrock: { cachePoint: { type: 'default', }, }, }, }); // typed tool calls: for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'weather': { toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log(result.text); console.log(JSON.stringify(result.toolCalls, null, 2)); console.log(JSON.stringify(result.providerMetadata, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-cache-point-user-image.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: bedrock('anthropic.claude-3-5-sonnet-20241022-v2:0'), messages: [ { role: 'user', content: [ { type: 'image', image: fs.readFileSync('./data/comic-cat.png') }, { type: 'text', text: 'What is in this image?', }, ], providerOptions: { bedrock: { cachePoint: { type: 'default' } } }, }, ], }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); // TODO: no cache token usage for some reason // https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html // the only delta is some of the lead-in to passing the message bytes, and // perhaps the size of the image. console.log('Cache token usage:', result.providerMetadata?.bedrock?.usage); console.log('Finish reason:', result.finishReason); console.log('Response headers:', result.response.headers); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-cache-point-user.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); async function main() { const result = await generateText({ model: bedrock('anthropic.claude-3-5-sonnet-20241022-v2:0'), messages: [ { role: 'user', content: [ { type: 'text', text: `I was dreaming last night and I dreamt of an error message: ${errorMessage}`, }, ], providerOptions: { bedrock: { cachePoint: { type: 'default' } } }, }, { role: 'user', content: [ { type: 'text', text: 'Explain the error message.', }, ], }, ], }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Cache token usage:', result.providerMetadata?.bedrock?.usage); console.log('Finish reason:', result.finishReason); console.log('Response headers:', result.response.headers); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-chatbot.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { ModelMessage, generateText } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { weatherTool } from '../tools/weather-tool'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; while (true) { if (!toolResponseAvailable) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); } const { text, toolCalls, toolResults, response } = await generateText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), tools: { weatherTool }, system: `You are a helpful, respectful and honest assistant. If the weather is requested use the `, messages, }); toolResponseAvailable = false; if (text) { process.stdout.write(`\nAssistant: ${text}`); } for (const { toolName, input } of toolCalls) { process.stdout.write( `\nTool call: '${toolName}' ${JSON.stringify(input)}`, ); } for (const { toolName, output } of toolResults) { process.stdout.write( `\nTool response: '${toolName}' ${JSON.stringify(output)}`, ); } process.stdout.write('\n\n'); messages.push(...response.messages); toolResponseAvailable = toolCalls.length > 0; } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-guardrails.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), prompt: 'Invent a new fake holiday and describe its traditions. ' + 'You are a comedian and should insult the audience as much as possible.', providerOptions: { bedrock: { guardrailConfig: { guardrailIdentifier: '<your-guardrail-identifier>', guardrailVersion: '1', trace: 'enabled' as const, streamProcessingMode: 'async', }, }, }, }); console.log(result.text); console.log(); console.log(JSON.stringify(result.providerMetadata?.bedrock.trace, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-image-url.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), maxOutputTokens: 512, messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/comic-cat.png?raw=true', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-image.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), maxOutputTokens: 512, messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png') }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-nova-tool-call.ts --- import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; import { bedrock } from '@ai-sdk/amazon-bedrock'; async function main() { const result = await generateText({ model: bedrock('us.amazon.nova-pro-v1:0'), tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, temperature: 0, topK: 1, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; break; } case 'weather': { toolCall.input.location; break; } } } for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { case 'weather': { toolResult.input.location; toolResult.output.location; toolResult.output.temperature; break; } } } console.log(result.text); console.log(JSON.stringify(result.toolCalls, null, 2)); console.log(JSON.stringify(result.toolResults, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-prefilled-assistant-message.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), messages: [ { role: 'user', content: 'Invent a new holiday and describe its traditions.', }, { role: 'assistant', content: 'Full Moon Festival', }, ], }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-reasoning-chatbot.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { ModelMessage, generateText, stepCountIs } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { weatherTool } from '../tools/weather-tool'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const { steps, response } = await generateText({ model: bedrock('us.anthropic.claude-3-7-sonnet-20250219-v1:0'), tools: { weatherTool }, system: `You are a helpful, respectful and honest assistant.`, messages, stopWhen: stepCountIs(5), providerOptions: { bedrock: { reasoningConfig: { type: 'enabled', budgetTokens: 2048 }, }, }, }); for (const step of steps) { console.log(step); if (step.reasoningText) { console.log(`\x1b[36m${step.reasoningText}\x1b[0m`); } if (step.text) { console.log(step.text); } if (step.toolCalls) { for (const toolCall of step.toolCalls) { console.log( `\x1b[33m${toolCall.toolName}\x1b[0m` + JSON.stringify(toolCall.input), ); } } } console.log('\n'); messages.push(...response.messages); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-reasoning.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText, stepCountIs } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: bedrock('us.anthropic.claude-3-7-sonnet-20250219-v1:0'), prompt: 'How many "r"s are in the word "strawberry"?', temperature: 0.5, // should get ignored (warning) providerOptions: { bedrock: { reasoningConfig: { type: 'enabled', budgetTokens: 2048 }, }, }, maxRetries: 0, stopWhen: stepCountIs(5), }); console.log('Reasoning:'); console.log(result.reasoning); console.log(); console.log('Text:'); console.log(result.text); console.log(); console.log('Warnings:', result.warnings); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-tool-call-image-result.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText, stepCountIs, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateText({ model: bedrock('us.anthropic.claude-3-7-sonnet-20250219-v1:0'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Please download this image https://upload.wikimedia.org/wikipedia/commons/f/f8/Alan_Turing_%281951%29.jpg and tell me what you see', }, ], }, ], tools: { submit: tool({ description: 'Download an image', inputSchema: z.object({ url: z.string().describe('The image URL'), }), execute: async ({ url }) => { const response = await fetch(url); const arrayBuffer = await response.arrayBuffer(); const bytes = new Uint8Array(arrayBuffer); return { bytes }; }, toModelOutput(result) { return { type: 'content', value: [ { type: 'media', data: Buffer.from(result.bytes).toString('base64'), mediaType: 'image/jpeg', }, ], }; }, }), }, stopWhen: stepCountIs(5), }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-tool-call.ts --- import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; import { bedrock } from '@ai-sdk/amazon-bedrock'; async function main() { const result = await generateText({ model: bedrock('anthropic.claude-3-5-sonnet-20240620-v1:0'), tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool calls: for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; // string break; } case 'weather': { toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // toolResult.input.city; // string // toolResult.result; // break; // } case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log(result.text); console.log(JSON.stringify(result.toolCalls, null, 2)); console.log(JSON.stringify(result.toolResults, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock-tool-choice.ts --- import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; import { bedrock } from '@ai-sdk/amazon-bedrock'; async function main() { const result = await generateText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, toolChoice: { type: 'tool', toolName: 'weather', }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/amazon-bedrock.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); console.log('Response headers:', result.response.headers); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-cache-control-beta-1h-streaming.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); const cachedMessage = `The time is ${new Date().toISOString()}. Error message: ${errorMessage}`; async function main() { const result = await streamText({ model: anthropic('claude-3-5-haiku-latest'), headers: { 'anthropic-beta': 'extended-cache-ttl-2025-04-11', }, messages: [ { role: 'user', content: [ { type: 'text', text: 'You are a JavaScript expert.', }, { type: 'text', text: cachedMessage, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral', ttl: '1h' }, }, }, }, { type: 'text', text: 'Explain the error message.', }, ], }, ], }); await result.consumeStream(); const providerMetadata = await result.providerMetadata; console.log( 'Streaming usage information:', providerMetadata?.anthropic?.usage, ); // e.g. // Streaming usage information: { // input_tokens: 10, // cache_creation_input_tokens: 2177, // cache_read_input_tokens: 0, // cache_creation: { ephemeral_5m_input_tokens: 0, ephemeral_1h_input_tokens: 2177 }, // output_tokens: 1, // service_tier: 'standard' // } const cachedResult = await streamText({ model: anthropic('claude-3-5-haiku-latest'), headers: { 'anthropic-beta': 'extended-cache-ttl-2025-04-11', }, messages: [ { role: 'user', content: [ { type: 'text', text: 'You are a JavaScript expert.', }, { type: 'text', text: cachedMessage, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral', ttl: '1h' }, }, }, }, { type: 'text', text: 'What is this?.', }, ], }, ], }); await cachedResult.consumeStream(); const cachedProviderMetadata = await cachedResult.providerMetadata; console.log( 'Streaming usage information:', cachedProviderMetadata?.anthropic?.usage, ); // e.g. // Streaming usage information: { // input_tokens: 8, // cache_creation_input_tokens: 0, // cache_read_input_tokens: 2177, // cache_creation: { ephemeral_5m_input_tokens: 0, ephemeral_1h_input_tokens: 0 }, // output_tokens: 1, // service_tier: 'standard' // } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-cache-control-beta-1h.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); const cachedMessage = `The time is ${new Date().toISOString()}. Error message: ${errorMessage}`; async function main() { const result = await generateText({ model: anthropic('claude-3-5-haiku-latest'), headers: { 'anthropic-beta': 'extended-cache-ttl-2025-04-11', }, messages: [ { role: 'user', content: [ { type: 'text', text: 'You are a JavaScript expert.', }, { type: 'text', text: cachedMessage, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral', ttl: '1h' }, }, }, }, { type: 'text', text: 'Explain the error message.', }, ], }, ], }); console.log('Usage information:', result.providerMetadata?.anthropic?.usage); // e.g. // Usage information: { // input_tokens: 10, // cache_creation_input_tokens: 2177, // cache_read_input_tokens: 0, // cache_creation: { ephemeral_5m_input_tokens: 0, ephemeral_1h_input_tokens: 2177 }, // output_tokens: 285, // service_tier: 'standard' // } const cachedResult = await generateText({ model: anthropic('claude-3-5-haiku-latest'), headers: { 'anthropic-beta': 'extended-cache-ttl-2025-04-11', }, messages: [ { role: 'user', content: [ { type: 'text', text: 'You are a JavaScript expert.', }, { type: 'text', text: cachedMessage, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral', ttl: '1h' }, }, }, }, { type: 'text', text: 'What is this?.', }, ], }, ], }); console.log( 'Usage information:', cachedResult.providerMetadata?.anthropic?.usage, ); // e.g. // Usage information: { // input_tokens: 8, // cache_creation_input_tokens: 0, // cache_read_input_tokens: 2177, // cache_creation: { ephemeral_5m_input_tokens: 0, ephemeral_1h_input_tokens: 0 }, // output_tokens: 317, // service_tier: 'standard' // } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-cache-control.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); async function main() { const result = await generateText({ model: anthropic('claude-3-5-sonnet-20240620'), messages: [ { role: 'user', content: [ { type: 'text', text: 'You are a JavaScript expert.', }, { type: 'text', text: `Error message: ${errorMessage}`, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, { type: 'text', text: 'Explain the error message.', }, ], }, ], }); console.log(result.text); console.log(); console.log('Cache read tokens:', result.usage.cachedInputTokens); console.log( 'Cache write tokens:', result.providerMetadata?.anthropic?.cacheCreationInputTokens, ); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-chatbot-websearch.ts --- import { createAnthropic } from '@ai-sdk/anthropic'; import { ModelMessage, generateText, stepCountIs } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; const anthropic = createAnthropic({ // example fetch wrapper that logs the input to the API call: fetch: async (url, options) => { console.log( `Body ${JSON.stringify(JSON.parse(options!.body! as string), null, 2)}`, ); return await fetch(url, options); }, }); const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const { content, response } = await generateText({ model: anthropic('claude-3-5-sonnet-latest'), tools: { web_search: anthropic.tools.webSearch_20250305({ onInputAvailable: async ({ input }) => { process.stdout.write(`\nTool call: '${JSON.stringify(input)}'`); }, }), }, system: `You are a helpful, respectful and honest assistant.`, messages, stopWhen: stepCountIs(3), }); console.log('Assistant:'); for (const part of content) { if (part.type === 'text') { console.log(part.text); } else { console.log(JSON.stringify(part, null, 2)); } } console.log(); console.log(); messages.push(...response.messages); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-chatbot.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { ModelMessage, generateText } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { weatherTool } from '../tools/weather-tool'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; while (true) { if (!toolResponseAvailable) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); } const { text, toolCalls, toolResults, response } = await generateText({ model: anthropic('claude-3-5-sonnet-20240620'), tools: { weatherTool }, system: `You are a helpful, respectful and honest assistant.`, messages, }); toolResponseAvailable = false; if (text) { process.stdout.write(`\nAssistant: ${text}`); } for (const { toolName, input } of toolCalls) { process.stdout.write( `\nTool call: '${toolName}' ${JSON.stringify(input)}`, ); } for (const { toolName, output } of toolResults) { process.stdout.write( `\nTool response: '${toolName}' ${JSON.stringify(output)}`, ); } process.stdout.write('\n\n'); messages.push(...response.messages); toolResponseAvailable = toolCalls.length > 0; } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-computer-use-bash.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText, stepCountIs } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: anthropic('claude-3-5-sonnet-20241022'), tools: { bash: anthropic.tools.bash_20241022({ async execute({ command }) { console.log('COMMAND', command); return [ { type: 'text', text: ` ❯ ls README.md build data node_modules package.json src tsconfig.json `, }, ]; }, }), }, prompt: 'List the files in my home directory.', stopWhen: stepCountIs(2), }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-computer-use-computer.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText, stepCountIs } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: anthropic('claude-3-5-sonnet-20241022'), tools: { computer: anthropic.tools.computer_20241022({ displayWidthPx: 1024, displayHeightPx: 768, async execute({ action, coordinate, text }) { console.log('args', { action, coordinate, text }); switch (action) { case 'screenshot': { // multipart result: return { type: 'image', data: fs .readFileSync('./data/screenshot-editor.png') .toString('base64'), }; } default: { console.log('Action:', action); console.log('Coordinate:', coordinate); console.log('Text:', text); return `executed ${action}`; } } }, // map to tool result content for LLM consumption: toModelOutput(result) { return { type: 'content', value: [ typeof result === 'string' ? { type: 'text', text: result } : { type: 'media', data: result.data, mediaType: 'image/png' }, ], }; }, }), }, prompt: 'How can I switch to dark mode? Take a look at the screen and tell me.', stopWhen: stepCountIs(5), }); console.log(result.text); console.log(result.finishReason); console.log(JSON.stringify(result.toolCalls, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-computer-use-editor-cache-control.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText, stepCountIs } from 'ai'; import 'dotenv/config'; async function main() { let editorContent = ` ## README This is a test file. `; const result = await generateText({ model: anthropic('claude-3-5-sonnet-20241022'), tools: { str_replace_editor: anthropic.tools.textEditor_20241022({ async execute({ command, path, old_str, new_str }) { console.log({ command, path, old_str, new_str }); switch (command) { case 'view': { return editorContent; } case 'create': { editorContent = new_str!; return editorContent; } case 'str_replace': { editorContent = editorContent.replace(old_str!, new_str!); return editorContent; } case 'insert': { editorContent = new_str!; return editorContent; } } }, }), }, messages: [ { role: 'user', content: 'Update my README file to talk about AI.', providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, ], stopWhen: stepCountIs(5), }); console.log('TEXT', result.text); console.log('CACHE', result.providerMetadata?.anthropic); console.log(); console.log('EDITOR CONTENT', editorContent); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-computer-use-editor.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText, stepCountIs } from 'ai'; import 'dotenv/config'; async function main() { let editorContent = ` ## README This is a test file. `; const result = await generateText({ model: anthropic('claude-3-5-sonnet-20241022'), tools: { str_replace_editor: anthropic.tools.textEditor_20250124({ async execute({ command, path, old_str, new_str }) { switch (command) { case 'view': { return editorContent; } case 'create': { editorContent = new_str!; return editorContent; } case 'str_replace': { editorContent = editorContent.replace(old_str!, new_str!); return editorContent; } case 'insert': { editorContent = new_str!; return editorContent; } } }, onInputAvailable: ({ input }) => { console.log('onInputAvailable', input); }, }), }, prompt: 'Update my README file to talk about AI.', stopWhen: stepCountIs(5), }); console.log('TEXT', result.text); console.log(); console.log('EDITOR CONTENT', editorContent); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-custom-fetch.ts --- import { createAnthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; import 'dotenv/config'; const anthropic = createAnthropic({ // example fetch wrapper that logs the input to the API call: fetch: async (url, options) => { console.log('URL', url); console.log('Headers', JSON.stringify(options!.headers, null, 2)); console.log( `Body ${JSON.stringify(JSON.parse(options!.body! as string), null, 2)}`, ); return await fetch(url, options); }, }); async function main() { const result = await generateText({ model: anthropic('claude-3-5-sonnet-20240620'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-file-part-citations.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; import { readFileSync } from 'fs'; import { resolve } from 'path'; import 'dotenv/config'; async function main() { const pdfPath = resolve(process.cwd(), 'data', 'ai.pdf'); const pdfBase64 = readFileSync(pdfPath).toString('base64'); const result = await generateText({ model: anthropic('claude-3-5-sonnet-20241022'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is generative AI? Use citations.', }, { type: 'file', data: `data:application/pdf;base64,${pdfBase64}`, mediaType: 'application/pdf', providerOptions: { anthropic: { citations: { enabled: true }, title: 'AI Documentation', }, }, }, ], }, ], }); console.log('Response:', result.text); const citations = result.content.filter(part => part.type === 'source'); citations.forEach((citation, i) => { if ( citation.sourceType === 'document' && citation.providerMetadata?.anthropic ) { const meta = citation.providerMetadata.anthropic; console.log( `\n[${i + 1}] "${meta.citedText}" (Pages: ${meta.startPageNumber}-${meta.endPageNumber})`, ); } }); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-full-result.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: anthropic('claude-3-5-sonnet-20240620'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-image-url.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: anthropic('claude-3-5-sonnet-20240620'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/comic-cat.png?raw=true', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-image.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: anthropic('claude-3-5-sonnet-20240620'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png') }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-pdf-url.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: anthropic('claude-3-5-sonnet-20241022'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: new URL( 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/ai.pdf?raw=true', ), mediaType: 'application/pdf', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-pdf.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: anthropic('claude-3-5-sonnet-20241022'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-provider-defined-tools.ts --- import { generateText } from 'ai'; import { anthropic } from '@ai-sdk/anthropic'; import 'dotenv/config'; async function main() { const result = await generateText({ model: anthropic('claude-3-5-sonnet-20241022'), prompt: 'Search for recent information about AI SDK development', tools: { webSearch: anthropic.tools.webSearch_20250305({ maxUses: 3, allowedDomains: ['github.com', 'vercel.com', 'docs.ai'], userLocation: { type: 'approximate', city: 'San Francisco', region: 'California', country: 'US', }, }), computer: anthropic.tools.computer_20250124({ displayWidthPx: 1920, displayHeightPx: 1080, }), }, }); console.log('Result:', result.text); console.log('Tool calls made:', result.toolCalls.length); for (const toolCall of result.toolCalls) { console.log(`\nTool Call:`); console.log(`- Tool: ${toolCall.toolName}`); console.log(`- Input:`, JSON.stringify(toolCall.input, null, 2)); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-reasoning-chatbot.ts --- import { createAnthropic, AnthropicProviderOptions } from '@ai-sdk/anthropic'; import { ModelMessage, generateText, stepCountIs } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { weatherTool } from '../tools/weather-tool'; const anthropic = createAnthropic({ // example fetch wrapper that logs the input to the API call: fetch: async (url, options) => { console.log('URL', url); console.log('Headers', JSON.stringify(options!.headers, null, 2)); console.log( `Body ${JSON.stringify(JSON.parse(options!.body! as string), null, 2)}`, ); return await fetch(url, options); }, }); const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const { steps, response } = await generateText({ model: anthropic('claude-3-7-sonnet-20250219'), tools: { weatherTool }, system: `You are a helpful, respectful and honest assistant.`, messages, stopWhen: stepCountIs(5), providerOptions: { anthropic: { thinking: { type: 'enabled', budgetTokens: 12000 }, } satisfies AnthropicProviderOptions, }, }); console.log('Assistant:'); for (const step of steps) { if (step.reasoningText) { console.log(`\x1b[36m${step.reasoningText}\x1b[0m`); } if (step.text) { console.log(step.text); } if (step.toolCalls) { for (const toolCall of step.toolCalls) { console.log( `\x1b[33m${toolCall.toolName}\x1b[0m` + JSON.stringify(toolCall.input), ); } } } console.log('\n'); messages.push(...response.messages); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-reasoning.ts --- import { anthropic, AnthropicProviderOptions } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: anthropic('claude-3-7-sonnet-20250219'), prompt: 'How many "r"s are in the word "strawberry"?', temperature: 0.5, // should get ignored (warning) providerOptions: { anthropic: { thinking: { type: 'enabled', budgetTokens: 12000 }, } satisfies AnthropicProviderOptions, }, maxRetries: 0, }); console.log('Reasoning:'); console.log(result.reasoning); console.log(); console.log('Text:'); console.log(result.text); console.log(); console.log('Warnings:', result.warnings); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-search.ts --- import 'dotenv/config'; import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; async function main() { const result = await generateText({ model: anthropic('claude-3-5-sonnet-latest'), prompt: 'What are the latest developments in AI research and technology?', tools: { web_search: anthropic.tools.webSearch_20250305({ maxUses: 5, userLocation: { type: 'approximate', city: 'San Francisco', region: 'California', country: 'US', timezone: 'America/Los_Angeles', }, }), }, }); console.log(JSON.stringify(result.content, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-text-citations.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: anthropic('claude-3-5-sonnet-20241022'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What color is the grass? Use citations.', }, { type: 'file', mediaType: 'text/plain', data: 'The grass is green in spring and summer. The sky is blue during clear weather.', providerOptions: { anthropic: { citations: { enabled: true }, title: 'Nature Facts', }, }, }, ], }, ], }); console.log('Response:', result.text); const citations = result.content.filter(part => part.type === 'source'); citations.forEach((citation, i) => { if ( citation.sourceType === 'document' && citation.providerMetadata?.anthropic ) { const meta = citation.providerMetadata.anthropic; console.log( `\n[${i + 1}] "${meta.citedText}" (chars: ${meta.startCharIndex}-${meta.endCharIndex})`, ); } }); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-tool-call-cache.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateText({ model: anthropic('claude-3-5-haiku-latest'), tools: { cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }), }, prompt: 'What attractions should I visit in San Francisco?', }); console.log(JSON.stringify(result.request.body, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-tool-call.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: anthropic('claude-3-opus-20240229'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool calls: for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; // string break; } case 'weather': { toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // toolResult.input.city; // string // toolResult.result; // break; // } case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-tool-choice.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: anthropic('claude-3-opus-20240229'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, toolChoice: { type: 'tool', toolName: 'weather', }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic-web-search.ts --- import 'dotenv/config'; import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; async function main() { const result = await generateText({ model: anthropic('claude-3-5-sonnet-latest'), prompt: 'What is the current weather in Paris? Please search for real-time weather.', tools: { web_search: anthropic.tools.webSearch_20250305({ maxUses: 1, userLocation: { type: 'approximate', city: 'Paris', country: 'FR', timezone: 'Europe/Paris', }, }), }, }); console.log(result.content); console.log(); console.log('Sources:', result.sources.length); console.log('Content blocks:', result.content.length); console.log('Finish reason:', result.finishReason); console.log('Usage:', result.usage); // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { case 'web_search': { toolResult.input.query; // string // toolResult.output.results; // string break; } } } // if (result.sources.length > 0) { // console.log(); // for (const source of result.sources) { // if (source.sourceType === 'url') { // console.log('URL:', source.url); // console.log('Title:', source.title); // if (source.providerMetadata?.anthropic) { // console.log('Metadata:', source.providerMetadata.anthropic); // } // console.log(); // } // } // } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/anthropic.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: anthropic('claude-3-5-sonnet-20240620'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/azure-custom-fetch.ts --- import { createAzure } from '@ai-sdk/azure'; import { generateText } from 'ai'; import 'dotenv/config'; const azure = createAzure({ // example fetch wrapper that logs the input to the API call: fetch: async (url, options) => { console.log('URL', url); console.log('Headers', JSON.stringify(options!.headers, null, 2)); console.log( `Body ${JSON.stringify(JSON.parse(options!.body! as string), null, 2)}`, ); return await fetch(url, options); }, }); async function main() { const result = await generateText({ model: azure('v0-gpt-35-turbo'), // use your own deployment prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/azure-image.ts --- import { azure } from '@ai-sdk/azure'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const imageData = fs.readFileSync('/Desktop/sonny-angel.jpg'); const imageBase64_string = imageData.toString('base64'); const { text, usage } = await generateText({ model: azure('v0-gpt-35-turbo'), // use your own deployment messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', // Internally, MIME type is automatically detected: image: imageBase64_string, providerOptions: { // When using the Azure OpenAI provider, the imageDetail option can be configured under the `openai` key: openai: { imageDetail: 'low', }, }, }, ], }, ], }); console.log(text); console.log(); console.log('Usage:', usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/azure-responses.ts --- import { createAzure } from '@ai-sdk/azure'; import { generateText } from 'ai'; import 'dotenv/config'; // Initialize Azure OpenAI provider const azure = createAzure({ apiKey: process.env.AZURE_API_KEY, baseURL: process.env.AZURE_BASE_URL, }); async function main() { // Basic text generation const basicResult = await generateText({ model: azure.responses('gpt-4o-mini'), prompt: 'What is quantum computing?', }); console.log('\n=== Basic Text Generation ==='); console.log(basicResult.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/azure.ts --- import { azure } from '@ai-sdk/azure'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const { text, usage } = await generateText({ model: azure('v0-gpt-35-turbo'), // use your own deployment prompt: 'Invent a new holiday and describe its traditions.', }); console.log(text); console.log(); console.log('Usage:', usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/bedrock-consistent-file-names.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const model = bedrock('us.anthropic.claude-3-7-sonnet-20250219-v1:0'); const documentContent = 'This is a sample text document for testing prompt cache effectiveness.\n\nThe key improvement: documents now have consistent names like document-01, document-02, etc. instead of random names, enabling proper prompt caching.'; const documentData = Buffer.from(documentContent, 'utf-8').toString('base64'); console.log('First request with documents:'); const result1 = await generateText({ model, messages: [ { role: 'user', content: [ { type: 'text', text: 'Please analyze these text documents:' }, { type: 'file', data: documentData, mediaType: 'text/txt', }, { type: 'file', data: documentData, mediaType: 'text/txt', }, ], }, ], }); console.log('Response 1:', result1.text.slice(0, 100) + '...'); console.log('\nSecond request with same documents:'); const result2 = await generateText({ model, messages: [ { role: 'user', content: [ { type: 'text', text: 'Please analyze these text documents:' }, { type: 'file', data: documentData, mediaType: 'text/txt', }, { type: 'file', data: documentData, mediaType: 'text/txt', }, ], }, ], }); console.log('Response 2:', result2.text.slice(0, 100) + '...'); console.log( '\nWith the fix, both requests will use the same document names:', ); console.log('- First document: document-01'); console.log('- Second document: document-02'); console.log( 'This enables effective prompt caching since document names are consistent!', ); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/bedrock-document-support.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; import { readFileSync } from 'fs'; import { join } from 'path'; import 'dotenv/config'; async function main() { const model = bedrock('us.anthropic.claude-3-7-sonnet-20250219-v1:0'); const testCases = [ { name: 'PDF', file: 'ai.pdf', mediaType: 'application/pdf', }, { name: 'Plain Text', file: 'error-message.txt', mediaType: 'text/plain', }, { name: 'CSV', data: 'Name,Age,City\nJohn,30,New York\nJane,25,Los Angeles', mediaType: 'text/csv', }, { name: 'HTML', data: '<html><body><h1>Test Document</h1><p>This is a test HTML document.</p></body></html>', mediaType: 'text/html', }, { name: 'Markdown', data: '# Test Document\n\nThis is a **test** markdown document with some content.', mediaType: 'text/markdown', }, { name: 'XLSX (Excel)', file: 'aisdk.xlsx', mediaType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }, { name: 'DOCX (Word)', file: 'sdk.docx', mediaType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }, ]; console.log('Testing all supported Bedrock document types:\n'); for (const testCase of testCases) { console.log(`Testing ${testCase.name} support:`); try { let fileData: string; if (testCase.file) { const filePath = join(__dirname, '../../data', testCase.file); const fileBuffer = readFileSync(filePath); fileData = fileBuffer.toString('base64'); } else { fileData = Buffer.from(testCase.data!, 'utf-8').toString('base64'); } const result = await generateText({ model, messages: [ { role: 'user', content: [ { type: 'text', text: 'Briefly describe what this document contains:', }, { type: 'file', data: fileData, mediaType: testCase.mediaType, }, ], }, ], }); console.log(`✓ ${testCase.name} processed successfully`); console.log(` Response: ${result.text}`); } catch (error) { if (error instanceof Error) { console.log(`✗ ${testCase.name} failed: ${error.message}`); } else { console.log(`✗ ${testCase.name} failed with unknown error`); } } console.log(''); } console.log('All supported document types tested!'); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/cerebras-tool-call.ts --- import { cerebras } from '@ai-sdk/cerebras'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: cerebras('llama3.1-8b'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool calls: for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; // string break; } case 'weather': { toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // toolResult.input.city; // string // toolResult.result; // break; // } case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log('Text:', result.text); console.log('Tool Calls:', JSON.stringify(result.toolCalls, null, 2)); console.log('Tool Results:', JSON.stringify(result.toolResults, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/cerebras.ts --- import 'dotenv/config'; import { cerebras as provider } from '@ai-sdk/cerebras'; import { generateText } from 'ai'; async function main() { const result = await generateText({ model: provider.chat('llama-3.1-70b'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/cohere-chatbot.ts --- import { ModelMessage, generateText } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { weatherTool } from '../tools/weather-tool'; import { cohere } from '@ai-sdk/cohere'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; while (true) { if (!toolResponseAvailable) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); } const { text, toolCalls, toolResults, response } = await generateText({ model: cohere('command-r-plus'), tools: { weatherTool }, system: `You are a helpful, respectful and honest assistant. If the weather is requested use the `, messages, }); toolResponseAvailable = false; if (text) { process.stdout.write(`\nAssistant: ${text}`); } for (const { toolName, input } of toolCalls) { process.stdout.write( `\nTool call: '${toolName}' ${JSON.stringify(input)}`, ); } for (const { toolName, output } of toolResults) { process.stdout.write( `\nTool response: '${toolName}' ${JSON.stringify(output)}`, ); } process.stdout.write('\n\n'); messages.push(...response.messages); toolResponseAvailable = toolCalls.length > 0; } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/cohere-citations.ts --- import { cohere } from '@ai-sdk/cohere'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: cohere('command-r-plus'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What are the key benefits of artificial intelligence mentioned in these documents?', }, { type: 'file', data: `Artificial Intelligence (AI) has revolutionized many industries by providing: 1. Automation of repetitive tasks 2. Enhanced decision-making through data analysis 3. Improved customer service through chatbots 4. Predictive analytics for better planning 5. Cost reduction through efficiency gains`, mediaType: 'text/plain', filename: 'ai-benefits.txt', }, { type: 'file', data: `Machine Learning, a subset of AI, offers additional advantages: - Pattern recognition in large datasets - Personalized recommendations - Fraud detection and prevention - Medical diagnosis assistance - Natural language processing capabilities`, mediaType: 'text/plain', filename: 'ml-advantages.txt', }, ], }, ], }); console.log('Generated response:'); console.log(result.text); console.log('\nFull result object:'); console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/cohere-tool-call-empty-params.ts --- import { cohere } from '@ai-sdk/cohere'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateText({ model: cohere('command-r-plus'), tools: { currentTime: tool({ description: 'Get the current time', inputSchema: z.object({}), execute: async () => ({ currentTime: new Date().toLocaleTimeString(), }), }), }, prompt: 'What is the current time?', }); // typed tool calls: for (const toolCall of result.toolCalls) { switch (toolCall.toolName) { case 'currentTime': { toolCall.input; // {} break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { switch (toolResult.toolName) { case 'currentTime': { toolResult.input; // {} break; } } } console.log(result.text); console.log(JSON.stringify(result.toolCalls, null, 2)); console.log(JSON.stringify(result.toolResults, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/cohere-tool-call.ts --- import { cohere } from '@ai-sdk/cohere'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: cohere('command-r-plus'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool calls: for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; // string break; } case 'weather': { toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // toolResult.input.city; // string // toolResult.result; // break; // } case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log(result.text); console.log(JSON.stringify(result.toolCalls, null, 2)); console.log(JSON.stringify(result.toolResults, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/cohere.ts --- import { cohere } from '@ai-sdk/cohere'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const { text, usage } = await generateText({ model: cohere('command-a-03-2025'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(text); console.log(); console.log('Usage:', usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/deepinfra-tool-call.ts --- import { deepinfra } from '@ai-sdk/deepinfra'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: deepinfra('mistralai/Mixtral-8x7B-Instruct-v0.1'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ // Tool description is required for deepinfra. description: 'Get attractions in a city', inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool calls: for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; // string break; } case 'weather': { toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // toolResult.input.city; // string // toolResult.result; // break; // } case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log('Text:', result.text); console.log('Tool Calls:', JSON.stringify(result.toolCalls, null, 2)); console.log('Tool Results:', JSON.stringify(result.toolResults, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/deepinfra.ts --- import { deepinfra } from '@ai-sdk/deepinfra'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const { text, usage } = await generateText({ model: deepinfra('mistralai/Mixtral-8x7B-Instruct-v0.1'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(text); console.log(); console.log('Usage:', usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/deepseek-cache-token.ts --- import { deepseek } from '@ai-sdk/deepseek'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); async function main() { const result = await generateText({ model: deepseek.chat('deepseek-chat'), messages: [ { role: 'user', content: [ { type: 'text', text: 'You are a JavaScript expert.', }, { type: 'text', text: `Error message: ${errorMessage}`, }, { type: 'text', text: 'Explain the error message.', }, ], }, ], }); console.log(result.text); console.log(result.usage); console.log(result.providerMetadata); // "prompt_cache_hit_tokens":1856,"prompt_cache_miss_tokens":5} } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/deepseek-reasoning.ts --- import { deepseek } from '@ai-sdk/deepseek'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: deepseek('deepseek-reasoner'), prompt: 'How many "r"s are in the word "strawberry"?', }); console.log(result.content); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/deepseek.ts --- import { deepseek } from '@ai-sdk/deepseek'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: deepseek('deepseek-chat'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log('Text:'); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/fireworks-deepseek.ts --- import { fireworks } from '@ai-sdk/fireworks'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: fireworks('accounts/fireworks/models/deepseek-v3'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); console.log(); console.log('Usage:', result.usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/fireworks-reasoning.ts --- import { fireworks } from '@ai-sdk/fireworks'; import { extractReasoningMiddleware, generateText, wrapLanguageModel, } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: wrapLanguageModel({ model: fireworks('accounts/fireworks/models/qwq-32b'), middleware: extractReasoningMiddleware({ tagName: 'think', startWithReasoning: true, }), }), prompt: 'Invent a new holiday and describe its traditions.', }); console.log('\nREASONING:\n'); console.log(result.reasoningText); console.log('\nTEXT:\n'); console.log(result.text); console.log(); console.log('Usage:', result.usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/gateway-image-base64.ts --- import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: 'xai/grok-2-vision', messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png').toString('base64'), }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/gateway-image-data-url.ts --- import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { // Read the image file and create a proper data URL const imageData = fs.readFileSync('./data/comic-cat.png'); const base64Data = imageData.toString('base64'); const dataUrl = `data:image/png;base64,${base64Data}`; const result = await generateText({ model: 'xai/grok-2-vision', messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: dataUrl, }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/gateway-image-url.ts --- import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: 'xai/grok-2-vision', messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/comic-cat.png?raw=true', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/gateway-pdf.ts --- import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: 'google/gemini-2.0-flash', messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/gateway-tool-call.ts --- import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: 'xai/grok-3', maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool calls: for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; // string break; } case 'weather': { toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // toolResult.input.city; // string // toolResult.result; // break; // } case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/gateway.ts --- import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const { text, usage } = await generateText({ model: 'anthropic/claude-3.5-haiku', prompt: 'Invent a new holiday and describe its traditions.', }); console.log(text); console.log(); console.log('Usage:', usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-audio.ts --- import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: google('gemini-1.5-flash'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is the audio saying?' }, { type: 'file', mediaType: 'audio/mpeg', data: fs.readFileSync('./data/galileo.mp3'), }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-caching.ts --- import 'dotenv/config'; import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; import fs from 'node:fs'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); async function main() { const result1 = await generateText({ model: google('gemini-2.5-flash'), prompt: errorMessage, }); console.log(result1.text); console.log(result1.providerMetadata?.google); // e.g. // { // groundingMetadata: null, // safetyRatings: null, // usageMetadata: { // thoughtsTokenCount: 634, // promptTokenCount: 2152, // candidatesTokenCount: 694, // totalTokenCount: 3480 // } // } const result2 = await generateText({ model: google('gemini-2.5-flash'), prompt: errorMessage, }); console.log(result2.text); console.log(result2.providerMetadata?.google); // e.g. // { // groundingMetadata: null, // safetyRatings: null, // usageMetadata: { // cachedContentTokenCount: 2027, // thoughtsTokenCount: 702, // promptTokenCount: 2152, // candidatesTokenCount: 710, // totalTokenCount: 3564 // } // } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-chatbot-image-output.ts --- import { google } from '@ai-sdk/google'; import { ModelMessage, generateText } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { presentImages } from '../lib/present-image'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { messages.push({ role: 'user', content: await terminal.question('You: ') }); const result = await generateText({ model: google('gemini-2.0-flash-exp'), providerOptions: { google: { responseModalities: ['TEXT', 'IMAGE'] }, }, messages, }); if (result.text) { process.stdout.write(`\nAssistant: ${result.text}`); } for (const file of result.files) { if (file.mediaType.startsWith('image/')) { await presentImages([file]); } } process.stdout.write('\n\n'); messages.push(...result.response.messages); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-custom-fetch.ts --- import { createGoogleGenerativeAI } from '@ai-sdk/google'; import { generateText } from 'ai'; import 'dotenv/config'; const google = createGoogleGenerativeAI({ // example fetch wrapper that logs the input to the API call: fetch: async (url, options) => { console.log('URL', url); console.log('Headers', JSON.stringify(options!.headers, null, 2)); console.log( `Body ${JSON.stringify(JSON.parse(options!.body! as string), null, 2)}`, ); return await fetch(url, options); }, }); async function main() { const result = await generateText({ model: google('gemini-1.5-pro-latest'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-gemma-system-instructions.ts --- import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: google('gemma-3-12b-it'), system: 'You are a helpful pirate assistant. Always respond like a friendly pirate, using "Arrr" and pirate terminology.', prompt: 'What is the meaning of life?', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-image-output.ts --- import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; import 'dotenv/config'; import { presentImages } from '../lib/present-image'; async function main() { const result = await generateText({ model: google('gemini-2.0-flash-exp'), prompt: 'Generate an image of a comic cat', providerOptions: { google: { responseModalities: ['TEXT', 'IMAGE'] }, }, }); console.log(result.text); for (const file of result.files) { if (file.mediaType.startsWith('image/')) { await presentImages([file]); } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-image-url.ts --- import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: google('gemini-1.5-flash'), maxOutputTokens: 512, messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/comic-cat.png?raw=true', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-image.ts --- import { google, GoogleGenerativeAIProviderOptions } from '@ai-sdk/google'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: google('gemini-1.5-flash'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png') }, ], }, ], providerOptions: { google: { responseModalities: ['TEXT', 'IMAGE'], } satisfies GoogleGenerativeAIProviderOptions, }, }); console.log(result.content); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-multi-step.ts --- import { google } from '@ai-sdk/google'; import { generateText, stepCountIs, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const { text } = await generateText({ model: google('gemini-1.5-pro'), tools: { currentLocation: tool({ description: 'Get the current location.', inputSchema: z.object({}), execute: async () => { const locations = ['New York', 'London', 'Paris']; return { location: locations[Math.floor(Math.random() * locations.length)], }; }, }), weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), // prompt: 'What is the weather in my current location?', prompt: 'What is the weather in Paris?', }); console.log(text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-output-object.ts --- import { google } from '@ai-sdk/google'; import { generateText, Output } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const { experimental_output } = await generateText({ model: google('gemini-2.5-flash'), experimental_output: Output.object({ schema: z.object({ name: z.string(), age: z.number().nullable().describe('Age of the person.'), contact: z.object({ type: z.literal('email'), value: z.string(), }), occupation: z.object({ type: z.literal('employed'), company: z.string(), position: z.string(), }), }), }), prompt: 'Generate an example person for testing.', }); console.log(experimental_output); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-pdf.ts --- import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: google('gemini-1.5-flash'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-reasoning.ts --- import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: google('gemini-2.5-pro'), prompt: 'How many "r"s are in the word "strawberry"?', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-sources.ts --- import { google, GoogleGenerativeAIProviderMetadata } from '@ai-sdk/google'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const { text, sources, providerMetadata } = await generateText({ model: google('gemini-2.5-flash'), tools: { google_search: google.tools.googleSearch({}), }, prompt: 'List the top 5 San Francisco news from the past week.' + 'You must include the date of each article.', }); const metadata = providerMetadata?.google as | GoogleGenerativeAIProviderMetadata | undefined; const groundingMetadata = metadata?.groundingMetadata; console.log(text); console.log(); console.log('SOURCES'); console.log(sources); console.log(); console.log('PROVIDER METADATA'); console.log(groundingMetadata); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-tool-call.ts --- import { google } from '@ai-sdk/google'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: google('gemini-1.5-pro-latest'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool calls: for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; // string break; } case 'weather': { toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // toolResult.input.city; // string // toolResult.result; // break; // } case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-tool-choice.ts --- import { google } from '@ai-sdk/google'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: google('gemini-1.5-pro-latest'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, toolChoice: { type: 'tool', toolName: 'weather', }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-url-context-wtih-google-search.ts --- import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: google('gemini-2.5-flash'), messages: [ { role: 'user', content: [ { type: 'text', text: `Based on this context: https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai, tell me how to use Gemini with AI SDK. Also, provide the latest news about AI SDK V5.`, }, ], }, ], tools: { url_context: google.tools.urlContext({}), google_search: google.tools.googleSearch({}), }, }); console.log(result.text); console.log(); console.log('SOURCES'); console.log(result.sources); console.log(); console.log('PROVIDER METADATA'); console.log(result.providerMetadata?.google); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-url-context.ts --- import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: google('gemini-2.5-flash'), messages: [ { role: 'user', content: [ { type: 'text', text: `Based on this context: https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai, tell me how to use Gemini with AI SDK`, }, ], }, ], tools: { url_context: google.tools.urlContext({}), }, }); console.log(result.text); console.log(); console.log('SOURCES'); console.log(result.sources); console.log(); console.log('PROVIDER METADATA'); console.log(result.providerMetadata?.google); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-anthropic-cache-control.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText } from 'ai'; import fs from 'node:fs'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); async function main() { const result = await generateText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), messages: [ { role: 'user', content: [ { type: 'text', text: 'You are a JavaScript expert.', }, { type: 'text', text: `Error message: ${errorMessage}`, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, { type: 'text', text: 'Explain the error message.', }, ], }, ], }); console.log(result.text); console.log(result.providerMetadata?.anthropic); // e.g. { cacheCreationInputTokens: 2118, cacheReadInputTokens: 0 } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-anthropic-chatbot.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { ModelMessage, generateText } from 'ai'; import * as readline from 'node:readline/promises'; import { weatherTool } from '../tools/weather-tool'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; while (true) { if (!toolResponseAvailable) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); } const { text, toolCalls, toolResults, response } = await generateText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), tools: { weatherTool }, system: `You are a helpful, respectful and honest assistant.`, messages, }); toolResponseAvailable = false; if (text) { process.stdout.write(`\nAssistant: ${text}`); } for (const { toolName, input } of toolCalls) { process.stdout.write( `\nTool call: '${toolName}' ${JSON.stringify(input)}`, ); } for (const { toolName, output } of toolResults) { process.stdout.write( `\nTool response: '${toolName}' ${JSON.stringify(output)}`, ); } process.stdout.write('\n\n'); messages.push(...response.messages); toolResponseAvailable = toolCalls.length > 0; } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-anthropic-computer-use-bash.ts --- import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText, stepCountIs } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), tools: { bash: vertexAnthropic.tools.bash_20241022({ async execute({ command }) { console.log('COMMAND', command); return [ { type: 'text', text: ` ❯ ls README.md build data node_modules package.json src tsconfig.json `, }, ]; }, }), }, prompt: 'List the files in my home directory.', stopWhen: stepCountIs(2), }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-anthropic-computer-use-computer.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText, stepCountIs } from 'ai'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), tools: { computer: vertexAnthropic.tools.computer_20241022({ displayWidthPx: 1024, displayHeightPx: 768, async execute({ action, coordinate, text }) { console.log('args', { action, coordinate, text }); switch (action) { case 'screenshot': { // multipart result: return { type: 'image', data: fs .readFileSync('./data/screenshot-editor.png') .toString('base64'), }; } default: { console.log('Action:', action); console.log('Coordinate:', coordinate); console.log('Text:', text); return `executed ${action}`; } } }, // map to tool result content for LLM consumption: toModelOutput(result) { return { type: 'content', value: [ typeof result === 'string' ? { type: 'text', text: result } : { type: 'media', data: result.data, mediaType: 'image/png' }, ], }; }, }), }, prompt: 'How can I switch to dark mode? Take a look at the screen and tell me.', stopWhen: stepCountIs(5), }); console.log(result.text); console.log(result.finishReason); console.log(JSON.stringify(result.toolCalls, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-anthropic-computer-use-editor-cache-control.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText, stepCountIs } from 'ai'; async function main() { let editorContent = ` ## README This is a test file. `; const result = await generateText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), tools: { str_replace_editor: vertexAnthropic.tools.textEditor_20241022({ async execute({ command, path, old_str, new_str }) { console.log({ command, path, old_str, new_str }); switch (command) { case 'view': { return editorContent; } case 'create': { editorContent = new_str!; return editorContent; } case 'str_replace': { editorContent = editorContent.replace(old_str!, new_str!); return editorContent; } case 'insert': { editorContent = new_str!; return editorContent; } } }, }), }, messages: [ { role: 'user', content: 'Update my README file to talk about AI.', providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, ], stopWhen: stepCountIs(5), }); console.log('TEXT', result.text); console.log('CACHE', result.providerMetadata?.anthropic); console.log(); console.log('EDITOR CONTENT', editorContent); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-anthropic-computer-use-editor.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText, stepCountIs } from 'ai'; async function main() { let editorContent = ` ## README This is a test file. `; const result = await generateText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), tools: { str_replace_editor: vertexAnthropic.tools.textEditor_20241022({ async execute({ command, path, old_str, new_str }) { console.log({ command, path, old_str, new_str }); switch (command) { case 'view': { return editorContent; } case 'create': { editorContent = new_str!; return editorContent; } case 'str_replace': { editorContent = editorContent.replace(old_str!, new_str!); return editorContent; } case 'insert': { editorContent = new_str!; return editorContent; } } }, }), }, prompt: 'Update my README file to talk about AI.', stopWhen: stepCountIs(5), }); console.log('TEXT', result.text); console.log(); console.log('EDITOR CONTENT', editorContent); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-anthropic-custom-fetch.ts --- import 'dotenv/config'; import { createVertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText } from 'ai'; const vertexAnthropic = createVertexAnthropic({ // example fetch wrapper that logs the URL: fetch: async (url, options) => { console.log(`Fetching ${url}`); const result = await fetch(url, options); console.log(`Fetched ${url}`); console.log(); return result; }, }); async function main() { const result = await generateText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-anthropic-full-result.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText } from 'ai'; async function main() { const result = await generateText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-anthropic-image-url.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText } from 'ai'; async function main() { const result = await generateText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), maxOutputTokens: 512, messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/comic-cat.png?raw=true', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-anthropic-image.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText } from 'ai'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png') }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-anthropic-pdf.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText } from 'ai'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-anthropic-tool-call.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText, tool } from 'ai'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool calls: for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; // string break; } case 'weather': { toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // toolResult.input.city; // string // toolResult.result; // break; // } case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-anthropic-tool-choice.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText, tool } from 'ai'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, toolChoice: { type: 'tool', toolName: 'weather', }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-anthropic.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { generateText } from 'ai'; async function main() { const result = await generateText({ // model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-audio.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: vertex('gemini-1.5-flash'), providerOptions: { google: { audioTimestamp: true, }, }, messages: [ { role: 'user', content: [ { type: 'text', text: 'Output a transcript of spoken words. Break up transcript lines when there are pauses. Include timestamps in the format of HH:MM:SS.SSS.', }, { type: 'file', data: Buffer.from(fs.readFileSync('./data/galileo.mp3')), mediaType: 'audio/mpeg', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-code-execution.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { googleTools } from '@ai-sdk/google/internal'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: vertex('gemini-2.5-pro'), tools: { code_execution: googleTools.codeExecution({}) }, maxOutputTokens: 2048, prompt: 'Use python to calculate 20th fibonacci number. Then find the nearest palindrome to it.', }); console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-grounding.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: vertex('gemini-1.5-pro'), providerOptions: { google: { useSearchGrounding: true, }, }, prompt: 'List the top 5 San Francisco news from the past week.' + 'You must include the date of each article.', }); console.log(result.text); console.log(result.providerMetadata?.google); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-image-base64.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: vertex('gemini-1.5-flash'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png').toString('base64'), }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-image-url.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: vertex('gemini-1.5-flash'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/comic-cat.png?raw=true', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-multi-step.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { generateText, stepCountIs, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const { text } = await generateText({ model: vertex('gemini-1.5-flash'), tools: { currentLocation: tool({ description: 'Get the current location.', inputSchema: z.object({}), execute: async () => { const locations = ['New York', 'London', 'Paris']; return { location: locations[Math.floor(Math.random() * locations.length)], }; }, }), weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), // prompt: 'What is the weather in my current location?', prompt: 'What is the weather in Paris?', }); console.log(text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-output-object.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { generateText, Output } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const { experimental_output } = await generateText({ model: vertex('gemini-1.5-flash'), experimental_output: Output.object({ schema: z.object({ name: z.string(), age: z.number().nullable().describe('Age of the person.'), contact: z.object({ type: z.literal('email'), value: z.string(), }), occupation: z.object({ type: z.literal('employed'), company: z.string(), position: z.string(), }), }), }), prompt: 'Generate an example person for testing.', }); console.log(experimental_output); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-pdf-url.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: vertex('gemini-1.5-flash'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/ai.pdf?raw=true', mediaType: 'application/pdf', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-pdf.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: vertex('gemini-1.5-flash'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-reasoning-generate-text.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { generateText } from 'ai'; async function main() { const result = await generateText({ model: vertex('gemini-2.5-flash-preview-04-17'), prompt: "Describe the most unusual or striking architectural feature you've ever seen in a building or structure.", providerOptions: { google: { thinkingConfig: { thinkingBudget: 2048, includeThoughts: true, }, }, }, }); process.stdout.write('\x1b[34m' + result.reasoning + '\x1b[0m'); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); console.log(); console.log('Warnings:', result.warnings); } main().catch(console.log); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-safety.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: vertex('gemini-1.5-pro'), providerOptions: { google: { safetySettings: [ { category: 'HARM_CATEGORY_UNSPECIFIED', threshold: 'BLOCK_LOW_AND_ABOVE', }, ], }, }, prompt: 'tell me a joke about a clown', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex-tool-call.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { generateText, stepCountIs, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const { text } = await generateText({ model: vertex('gemini-1.5-pro'), prompt: 'What is the weather in New York City? ', tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => { console.log('Getting weather for', location); return { location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }; }, }), }, stopWhen: stepCountIs(5), }); console.log(text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-vertex.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: vertex('gemini-1.5-flash'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google-youtube-url.ts --- import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: google('gemini-1.5-flash'), maxOutputTokens: 512, messages: [ { role: 'user', content: [ { type: 'text', text: 'Summarize this video and its main points.' }, { type: 'file', data: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', mediaType: 'video/mp4', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/google.ts --- import { google } from '@ai-sdk/google'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: google('gemini-1.5-flash-002'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/groq-kimi-k2.ts --- import { groq } from '@ai-sdk/groq'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: groq('moonshotai/kimi-k2-instruct'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/groq-reasoning.ts --- import { groq } from '@ai-sdk/groq'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: groq('qwen-qwq-32b'), providerOptions: { groq: { reasoningFormat: 'parsed' }, }, prompt: 'How many "r"s are in the word "strawberry"?', }); console.log('Reasoning:'); console.log(result.reasoningText); console.log(); console.log('Text:'); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/groq.ts --- import { groq } from '@ai-sdk/groq'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: groq('gemma2-9b-it'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/mistral-chatbot.ts --- import { mistral } from '@ai-sdk/mistral'; import { ModelMessage, generateText } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { weatherTool } from '../tools/weather-tool'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; while (true) { if (!toolResponseAvailable) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); } const { text, toolCalls, toolResults, response } = await generateText({ model: mistral('mistral-large-latest'), tools: { weatherTool }, system: `You are a helpful, respectful and honest assistant.`, messages, }); toolResponseAvailable = false; if (text) { process.stdout.write(`\nAssistant: ${text}`); } for (const { toolName, input } of toolCalls) { process.stdout.write( `\nTool call: '${toolName}' ${JSON.stringify(input)}`, ); } for (const { toolName, output } of toolResults) { process.stdout.write( `\nTool response: '${toolName}' ${JSON.stringify(output)}`, ); } process.stdout.write('\n\n'); messages.push(...response.messages); toolResponseAvailable = toolCalls.length > 0; } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/mistral-custom-fetch.ts --- import { createMistral } from '@ai-sdk/mistral'; import { generateText } from 'ai'; import 'dotenv/config'; const mistral = createMistral({ // example fetch wrapper that logs the input to the API call: fetch: async (url, options) => { console.log('URL', url); console.log('Headers', JSON.stringify(options!.headers, null, 2)); console.log( `Body ${JSON.stringify(JSON.parse(options!.body! as string), null, 2)}`, ); return await fetch(url, options); }, }); async function main() { const result = await generateText({ model: mistral('open-mistral-7b'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/mistral-full-result.ts --- import { mistral } from '@ai-sdk/mistral'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: mistral('open-mistral-7b'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/mistral-image-base64.ts --- import { mistral } from '@ai-sdk/mistral'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: mistral('pixtral-large-latest'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png').toString('base64'), }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/mistral-image-url.ts --- import { mistral } from '@ai-sdk/mistral'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: mistral('pixtral-12b-2409'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/comic-cat.png?raw=true', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/mistral-medium.ts --- import { mistral } from '@ai-sdk/mistral'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: mistral('mistral-medium-latest'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/mistral-pdf-url.ts --- import { mistral } from '@ai-sdk/mistral'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: mistral('mistral-small-latest'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: new URL( 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/ai.pdf?raw=true', ), mediaType: 'application/pdf', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/mistral-reasoning-raw.ts --- import { mistral } from '@ai-sdk/mistral'; import { extractReasoningMiddleware, generateText, wrapLanguageModel, } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: wrapLanguageModel({ model: mistral('magistral-medium-2506'), middleware: extractReasoningMiddleware({ tagName: 'think', }), }), prompt: 'Solve this step by step: If a train travels 60 mph for 2 hours, how far does it go?', maxOutputTokens: 500, }); console.log('\nREASONING:\n'); console.log(result.reasoningText); console.log('\nTEXT:\n'); console.log(result.text); console.log(); console.log('Usage:', result.usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/mistral-tool-call.ts --- import { mistral } from '@ai-sdk/mistral'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: mistral('mistral-large-latest'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool calls: for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; // string break; } case 'weather': { toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // toolResult.input.city; // string // toolResult.result; // break; // } case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/mistral-tool-choice.ts --- import { mistral } from '@ai-sdk/mistral'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: mistral('mistral-large-latest'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, toolChoice: { type: 'tool', toolName: 'weather', }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/mistral.ts --- import { mistral } from '@ai-sdk/mistral'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: mistral('open-mistral-7b'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/mock-invalid-tool-call.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, stepCountIs, tool } from 'ai'; import { MockLanguageModelV2 } from 'ai/test'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateText({ model: openai('gpt-4o'), tools: { cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), execute: async ({ city }) => { if (city === 'San Francisco') { return ['Golden Gate Bridge', 'Alcatraz Island']; } return []; }, }), }, prepareStep: async ({ stepNumber }) => { // inject invalid tool call in first step: if (stepNumber === 0) { return { model: new MockLanguageModelV2({ doGenerate: async () => ({ warnings: [], usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30, }, finishReason: 'tool-calls', content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'cityAttractions', // wrong tool call arguments (city vs cities): input: `{ "cities": "San Francisco" }`, }, ], }), }), }; } }, prompt: 'What are the tourist attractions in San Francisco?', stopWhen: stepCountIs(5), }); console.log('Content:'); console.log(JSON.stringify(result.content, null, 2)); console.log('Response messages:'); console.log(JSON.stringify(result.response.messages, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/mock-tool-call-repair-reask.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, tool } from 'ai'; import { MockLanguageModelV2 } from 'ai/test'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ warnings: [], usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30, }, finishReason: 'tool-calls', content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'cityAttractions', // wrong tool call arguments (city vs cities): input: `{ "city": "San Francisco" }`, }, ], }), }), tools: { cityAttractions: tool({ inputSchema: z.object({ cities: z.array(z.string()) }), }), }, prompt: 'What are the tourist attractions in San Francisco?', experimental_repairToolCall: async ({ toolCall, tools, error, messages, system, }) => { const result = await generateText({ model: openai('gpt-4o'), system, messages: [ ...messages, { role: 'assistant', content: [ { type: 'tool-call', toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, input: toolCall.input, }, ], }, { role: 'tool' as const, content: [ { type: 'tool-result', toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, output: { type: 'error-text', value: error.message }, }, ], }, ], tools, }); const newToolCall = result.toolCalls.find( newToolCall => newToolCall.toolName === toolCall.toolName, ); return newToolCall != null ? { type: 'tool-call' as const, toolCallType: 'function' as const, toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, input: JSON.stringify(newToolCall.input), } : null; }, }); console.log('Repaired tool calls:'); console.log(JSON.stringify(result.toolCalls, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/mock-tool-call-repair-structured-model.ts --- import { openai } from '@ai-sdk/openai'; import { generateObject, generateText, NoSuchToolError, tool } from 'ai'; import { MockLanguageModelV2 } from 'ai/test'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ warnings: [], usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30, }, finishReason: 'tool-calls', content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'cityAttractions', // wrong tool call arguments (city vs cities): input: `{ "city": "San Francisco" }`, }, ], }), }), tools: { cityAttractions: tool({ inputSchema: z.object({ cities: z.array(z.string()) }), }), }, prompt: 'What are the tourist attractions in San Francisco?', experimental_repairToolCall: async ({ toolCall, tools, inputSchema, error, }) => { if (NoSuchToolError.isInstance(error)) { return null; // do not attempt to fix invalid tool names } const tool = tools[toolCall.toolName as keyof typeof tools]; // example approach: use a model with structured outputs for repair: const { object: repairedArgs } = await generateObject({ model: openai('gpt-4o'), schema: tool.inputSchema, prompt: [ `The model tried to call the tool "${ toolCall.toolName }" with the following arguments: ${JSON.stringify(toolCall.input)}.`, `The tool accepts the following schema: ${JSON.stringify( inputSchema(toolCall), )}.`, 'Please try to fix the arguments.', ].join('\n'), }); return { ...toolCall, args: JSON.stringify(repairedArgs) }; }, }); console.log('Repaired tool calls:'); console.log(JSON.stringify(result.toolCalls, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/mock.ts --- import { generateText } from 'ai'; import { MockLanguageModelV2 } from 'ai/test'; import 'dotenv/config'; async function main() { const { text, usage } = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ content: [{ type: 'text', text: `Hello, world!` }], finishReason: 'stop', usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30, }, warnings: [], }), }), prompt: 'Hello, test!', }); console.log(text); console.log(); console.log('Usage:', usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/nim.ts --- import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const nim = createOpenAICompatible({ baseURL: 'https://integrate.api.nvidia.com/v1', name: 'nim', headers: { Authorization: `Bearer ${process.env.NIM_API_KEY}`, }, }); const model = nim.chatModel('deepseek-ai/deepseek-r1'); const result = await generateText({ model, prompt: 'Tell me the history of the San Francisco Mission-style burrito.', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-active-tools.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, stepCountIs, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const { text } = await generateText({ model: openai('gpt-4o'), tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, activeTools: [], // disable all tools stopWhen: stepCountIs(5), prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); console.log(text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-audio.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import fs from 'node:fs'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai('gpt-4o-audio-preview'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is the audio saying?' }, { type: 'file', mediaType: 'audio/mpeg', data: fs.readFileSync('./data/galileo.mp3'), }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-cached-prompt-tokens.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; import { setTimeout } from 'node:timers/promises'; import { performance } from 'node:perf_hooks'; const longPrompt = ` Arms and the man I sing, who first made way, Predestined exile, from the Trojan shore To Italy, the blest Lavinian strand. Smitten of storms he was on land and sea By violence of Heaven, to satisfy 5 Stern Juno’s sleepless wrath; and much in war He suffered, seeking at the last to found The city, and bring o’er his fathers’ gods To safe abode in Latium; whence arose The Latin race, old Alba’s reverend lords, 10 And from her hills wide-walled, imperial Rome. O Muse, the causes tell! What sacrilege, Or vengeful sorrow, moved the heavenly Queen To thrust on dangers dark and endless toil A man whose largest honor in men’s eyes 15 Was serving Heaven? Can gods such anger feel? In ages gones an ancient city stood— Carthage, a Tyrian seat, which from afar Made front on Italy and on the mouths Of Tiber’s stream; its wealth and revenues 20 Were vast, and ruthless was its quest of war. ’T is said that Juno, of all lands she loved, Most cherished this,—not Samos’ self so dear. Here were her arms, her chariot; even then A throne of power o’er nations near and far, 25 If Fate opposed not, ’t was her darling hope To ’stablish here; but anxiously she heard That of the Trojan blood there was a breed Then rising, which upon the destined day Should utterly o’erwhelm her Tyrian towers; 30 A people of wide sway and conquest proud Should compass Libya’s doom;—such was the web The Fatal Sisters spun. Such was the fear Of Saturn’s daughter, who remembered well What long and unavailing strife she waged 35 For her loved Greeks at Troy. Nor did she fail To meditate th’ occasions of her rage, And cherish deep within her bosom proud Its griefs and wrongs: the choice by Paris made; Her scorned and slighted beauty; a whole race 40 Rebellious to her godhead; and Jove’s smile That beamed on eagle-ravished Ganymede. With all these thoughts infuriate, her power Pursued with tempests o’er the boundless main The Trojans, though by Grecian victor spared 45 And fierce Achilles; so she thrust them far From Latium; and they drifted, Heaven-impelled, Year after year, o’er many an unknown sea— O labor vast, to found the Roman line! Below th’ horizon the Sicilian isle 50 Just sank from view, as for the open sea With heart of hope they said, and every ship Clove with its brazen beak the salt, white waves. But Juno of her everlasting wound Knew no surcease, but from her heart of pain 55 Thus darkly mused: “Must I, defeated, fail “Of what I will, nor turn the Teucrian King “From Italy away? Can Fate oppose? “Had Pallas power to lay waste in flame “The Argive fleet and sink its mariners, 60 “Revenging but the sacrilege obscene “By Ajax wrought, Oïleus’ desperate son? “She, from the clouds, herself Jove’s lightning threw, “Scattered the ships, and ploughed the sea with storms. “Her foe, from his pierced breast out-breathing fire, 65 “In whirlwind on a deadly rock she flung. “But I, who move among the gods a queen, “Jove’s sister and his spouse, with one weak tribe “Make war so long! Who now on Juno calls? “What suppliant gifts henceforth her altars crown?” 70 So, in her fevered heart complaining still, Unto the storm-cloud land the goddess came, A region with wild whirlwinds in its womb, Æolia named, where royal Æolus In a high-vaulted cavern keeps control 75 O’er warring winds and loud concoùrse of storms. There closely pent in chains and bastions strong, They, scornful, make the vacant mountain roar, Chafing against their bonds. But from a throne Of lofty crag, their king with sceptred hand 80 Allays their fury and their rage confines. Did he not so, our ocean, earth, and sky Were whirled before them through the vast inane. But over-ruling Jove, of this in fear, Hid them in dungeon dark: then o’er them piled 85 Huge mountains, and ordained a lawful king To hold them in firm sway, or know what time, With Jove’s consent, to loose them o’er the world. To him proud Juno thus made lowly plea: “Thou in whose hands the Father of all gods 90 “And Sovereign of mankind confides the power “To calm the waters or with winds upturn, “Great Æolus! a race with me at war “Now sails the Tuscan main towards Italy, “Bringing their Ilium and its vanquished powers. 95 “Uprouse thy gales! Strike that proud navy down! “Hurl far and wide, and strew the waves with dead! “Twice seven nymphs are mine, of rarest mould, “Of whom Deïopea, the most fair, “I give thee in true wedlock for thine own, 100 “To mate thy noble worth; she at thy side “Shall pass long, happy years, and fruitful bring “Her beauteous offspring unto thee their sire.” Then Æolus: “’T is thy sole task, O Queen “To weigh thy wish and will. My fealty 105 “Thy high behest obeys. This humble throne “Is of thy gift. Thy smiles for me obtain “Authority from Jove. Thy grace concedes “My station at your bright Olympian board, “And gives me lordship of the darkening storm.” 110 Replying thus, he smote with spear reversed The hollow mountain’s wall; then rush the winds Through that wide breach in long, embattled line, And sweep tumultuous from land to land: With brooding pinions o’er the waters spread 115 East wind and south, and boisterous Afric gale Upturn the sea; vast billows shoreward roll; The shout of mariners, the creak of cordage, Follow the shock; low-hanging clouds conceal From Trojan eyes all sight of heaven and day; 120 Night o’er the ocean broods; from sky to sky The thunder roll, the ceaseless lightnings glare; And all things mean swift death for mortal man. `; const runCompletion = async () => await generateText({ model: openai('gpt-4o-mini'), messages: [ { role: 'user', content: `What book is the following text from?: <text>${longPrompt}</text>`, }, ], providerOptions: { openai: { maxCompletionTokens: 100 }, }, }); async function main() { let start = performance.now(); const { text, usage, providerMetadata } = await runCompletion(); let end = performance.now(); console.log( `PLEASE NOTE caching behavior is transparent and difficult to test. If you don't get a cache hit the first time, try several additional times.`, ); console.log(`First pass text:`, text); console.log(`First pass usage:`, usage); console.log(`First pass provider metadata:`, providerMetadata); console.log(`First pass time: ${Math.floor(end - start)} ms`); console.log(); await setTimeout(1000); // wait for it to be cached?g start = performance.now(); const { text: text2, usage: usage2, providerMetadata: providerMetadata2, } = await runCompletion(); end = performance.now(); console.log(`Second pass text:`, text2); console.log(`Second pass usage:`, usage2); console.log(`Second pass provider metadata:`, providerMetadata2); console.log(`First pass time: ${Math.floor(end - start)} ms`); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-compatible-deepseek.ts --- import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { generateText } from 'ai'; import 'dotenv/config'; const deepSeek = createOpenAICompatible({ name: 'deepseek', baseURL: 'https://api.deepseek.com', headers: { Authorization: `Bearer ${process.env.DEEPSEEK_API_KEY ?? ''}`, }, }); async function main() { const { text, usage } = await generateText({ model: deepSeek('deepseek-chat'), prompt: 'Write a "Hello, World!" program in TypeScript.', }); console.log(text); console.log(); console.log('Usage:', usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-compatible-litellm-anthropic-cache-control.ts --- import 'dotenv/config'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { generateText } from 'ai'; async function main() { // See ../../../litellm/README.md for instructions on how to run a LiteLLM // proxy locally configured to interface with Anthropic. const litellmAnthropic = createOpenAICompatible({ baseURL: 'http://0.0.0.0:4000', name: 'litellm-anthropic', }); const model = litellmAnthropic.chatModel('claude-3-5-sonnet-20240620'); const result = await generateText({ model, messages: [ { role: 'system', // https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#cache-limitations // The cache content must be of a meaningful size (e.g. 1024 tokens, see // above for detail) and will only be cached for a moderate period of // time e.g. 5 minutes. content: "You are an AI assistant tasked with analyzing this story: The ancient clocktower stood sentinel over Millbrook Valley, its weathered copper face gleaming dully in the late afternoon sun. Sarah Chen adjusted her backpack and gazed up at the structure that had fascinated her since childhood. At thirteen stories tall, it had been the highest building in town for over a century, though now it was dwarfed by the glass and steel office buildings that had sprung up around it.\n\nThe door creaked as she pushed it open, sending echoes through the dusty entrance hall. Her footsteps on the marble floor seemed unnaturally loud in the empty space. The restoration project wouldn't officially begin for another week, but as the lead architectural historian, she had permission to start her preliminary survey early.\n\nThe building had been abandoned for twenty years, ever since the great earthquake of 2003 had damaged the clock mechanism. The city had finally approved funding to restore it to working order, but Sarah suspected there was more to the clocktower than anyone realized. Her research had uncovered hints that its architect, Theodore Hammond, had built secret rooms and passages throughout the structure.\n\nShe clicked on her flashlight and began climbing the main staircase. The emergency lights still worked on the lower floors, but she'd need the extra illumination higher up. The air grew mustier as she ascended, thick with decades of undisturbed dust. Her hand traced along the ornate brass railings, feeling the intricate patterns worked into the metal.\n\nOn the seventh floor, something caught her eye - a slight irregularity in the wall paneling that didn't match the blueprints she'd memorized. Sarah ran her fingers along the edge of the wood, pressing gently until she felt a click. A hidden door swung silently open, revealing a narrow passage.\n\nHer heart pounding with excitement, she squeezed through the opening. The passage led to a small octagonal room she estimated to be directly behind the clock face. Gears and mechanisms filled the space, all connected to a central shaft that rose up through the ceiling. But it was the walls that drew her attention - they were covered in elaborate astronomical charts and mathematical formulas.\n\n\"It's not just a clock,\" she whispered to herself. \"It's an orrery - a mechanical model of the solar system!\"\n\nThe complexity of the mechanism was far beyond what should have existed in the 1890s when the tower was built. Some of the mathematical notations seemed to describe orbital mechanics that wouldn't be discovered for decades after Hammond's death. Sarah's mind raced as she documented everything with her camera.\n\nA loud grinding sound from above made her jump. The central shaft began to rotate slowly, setting the gears in motion. She watched in amazement as the astronomical models came to life, planets and moons tracking across their metal orbits. But something was wrong - the movements didn't match any normal celestial patterns she knew.\n\nThe room grew noticeably colder. Sarah's breath frosted in the air as the mechanism picked up speed. The walls seemed to shimmer, becoming translucent. Through them, she could see not the expected view of downtown Millbrook, but a star-filled void that made her dizzy to look at.\n\nShe scrambled back toward the hidden door, but it had vanished. The room was spinning now, or maybe reality itself was spinning around it. Sarah grabbed onto a support beam as her stomach lurched. The stars beyond the walls wheeled and danced in impossible patterns.\n\nJust when she thought she couldn't take anymore, everything stopped. The mechanism ground to a halt. The walls solidified. The temperature returned to normal. Sarah's hands shook as she checked her phone - no signal, but the time display showed she had lost three hours.\n\nThe hidden door was back, and she practically fell through it in her haste to exit. She ran down all thirteen flights of stairs without stopping, bursting out into the street. The sun was setting now, painting the sky in deep purples and reds. Everything looked normal, but she couldn't shake the feeling that something was subtly different.\n\nBack in her office, Sarah pored over the photos she'd taken. The astronomical charts seemed to change slightly each time she looked at them, the mathematical formulas rearranging themselves when viewed from different angles. None of her colleagues believed her story about what had happened in the clocktower, but she knew what she had experienced was real.\n\nOver the next few weeks, she threw herself into research, trying to learn everything she could about Theodore Hammond. His personal papers revealed an obsession with time and dimensional theory far ahead of his era. There were references to experiments with \"temporal architecture\" and \"geometric manipulation of spacetime.\"\n\nThe restoration project continued, but Sarah made sure the hidden room remained undiscovered. Whatever Hammond had built, whatever portal or mechanism he had created, she wasn't sure the world was ready for it. But late at night, she would return to the clocktower and study the mysterious device, trying to understand its secrets.\n\nSometimes, when the stars aligned just right, she could hear the gears beginning to turn again, and feel reality starting to bend around her. And sometimes, in her dreams, she saw Theodore Hammond himself, standing at a drawing board, sketching plans for a machine that could fold space and time like paper - a machine that looked exactly like the one hidden in the heart of his clocktower.\n\nThe mystery of what Hammond had truly built, and why, consumed her thoughts. But with each new piece of evidence she uncovered, Sarah became more certain of one thing - the clocktower was more than just a timepiece. It was a key to understanding the very nature of time itself, and its secrets were only beginning to be revealed.\n", providerOptions: { openaiCompatible: { cache_control: { type: 'ephemeral', }, }, }, }, { role: 'user', content: 'What are the key narrative points made in this story?', }, ], }); console.log(result.text); console.log(); // Note the cache-specific token usage information is not yet available in the // AI SDK. We plan to make it available in the response through the // `providerMetadata` field in the future. console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-compatible-openai-image.ts --- import 'dotenv/config'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { generateText } from 'ai'; import fs from 'node:fs'; async function main() { const openai = createOpenAICompatible({ baseURL: 'https://api.openai.com/v1', name: 'openai', headers: { Authorization: `Bearer ${process.env.TOGETHER_AI_API_KEY}`, }, }); const model = openai.chatModel('gpt-4o-mini'); const result = await generateText({ model, messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png') }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-compatible-togetherai-tool-call.ts --- import 'dotenv/config'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { generateText, tool } from 'ai'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const togetherai = createOpenAICompatible({ baseURL: 'https://api.together.xyz/v1', name: 'togetherai', headers: { Authorization: `Bearer ${process.env.TOGETHER_AI_API_KEY}`, }, }); const model = togetherai.chatModel( 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo', ); const result = await generateText({ model, maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool calls: for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; // string break; } case 'weather': { toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // toolResult.input.city; // string // toolResult.result; // break; // } case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log('Text:', result.text); console.log('Tool Calls:', JSON.stringify(result.toolCalls, null, 2)); console.log('Tool Results:', JSON.stringify(result.toolResults, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-compatible-togetherai.ts --- import 'dotenv/config'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { generateText } from 'ai'; async function main() { const togetherai = createOpenAICompatible({ baseURL: 'https://api.together.xyz/v1', name: 'togetherai', headers: { Authorization: `Bearer ${process.env.TOGETHER_AI_API_KEY}`, }, }); const model = togetherai.chatModel('meta-llama/Llama-3-70b-chat-hf'); const result = await generateText({ model, prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-completion-chat.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai('gpt-3.5-turbo-instruct'), maxOutputTokens: 1024, system: 'You are a helpful chatbot.', messages: [ { role: 'user', content: 'Hello!', }, { role: 'assistant', content: 'Hello! How can I help you today?', }, { role: 'user', content: 'I need help with my computer.', }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-completion.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai('gpt-3.5-turbo-instruct'), maxOutputTokens: 1024, prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-custom-fetch.ts --- import { createOpenAI } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; const openai = createOpenAI({ // example fetch wrapper that logs the input to the API call: fetch: async (url, options) => { console.log('URL', url); console.log('Headers', JSON.stringify(options!.headers, null, 2)); console.log( `Body ${JSON.stringify(JSON.parse(options!.body! as string), null, 2)}`, ); return await fetch(url, options); }, }); async function main() { const result = await generateText({ model: openai('gpt-3.5-turbo'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-custom-headers.ts --- import { createOpenAI } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY!, headers: { 'custom-provider-header': 'value-1', }, // fetch wrapper to log the headers: fetch: async (url, options) => { console.log('Headers', options?.headers); return fetch(url, options); }, }); async function main() { const result = await generateText({ model: openai('gpt-3.5-turbo'), prompt: 'Invent a new holiday and describe its traditions.', maxOutputTokens: 50, headers: { 'custom-request-header': 'value-2', }, }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-dynamic-tool-call.ts --- import { openai } from '@ai-sdk/openai'; import { dynamicTool, generateText, stepCountIs, ToolSet } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; function dynamicTools(): ToolSet { return { currentLocation: dynamicTool({ description: 'Get the current location.', inputSchema: z.object({}), execute: async () => { const locations = ['New York', 'London', 'Paris']; return { location: locations[Math.floor(Math.random() * locations.length)], }; }, }), }; } async function main() { const result = await generateText({ model: openai('gpt-4o'), stopWhen: stepCountIs(5), tools: { ...dynamicTools(), weather: weatherTool, }, prompt: 'What is the weather in my current location?', onStepFinish: step => { // typed tool calls: for (const toolCall of step.toolCalls) { if (toolCall.dynamic) { console.log('DYNAMIC CALL', JSON.stringify(toolCall, null, 2)); continue; } switch (toolCall.toolName) { case 'weather': { console.log('STATIC CALL', JSON.stringify(toolCall, null, 2)); toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of step.toolResults) { if (toolResult.dynamic) { console.log('DYNAMIC RESULT', JSON.stringify(toolResult, null, 2)); continue; } switch (toolResult.toolName) { case 'weather': { console.log('STATIC RESULT', JSON.stringify(toolResult, null, 2)); toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } }, }); console.log(JSON.stringify(result.content, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-full-result.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai('gpt-4o-mini'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-image-base64.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: openai('gpt-4-turbo'), maxOutputTokens: 512, messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png').toString('base64'), }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-image-url.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai('gpt-4o'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/comic-cat.png?raw=true', // OpenAI specific option - image detail: providerOptions: { openai: { imageDetail: 'low' }, }, }, ], }, ], }); console.log(result.text); console.log(); console.log('REQUEST'); console.log(JSON.stringify(result.request!.body, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-image.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import fs from 'node:fs'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai('gpt-4o'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png') }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-log-metadata-middleware.ts --- import { openai } from '@ai-sdk/openai'; import { LanguageModelV2Middleware } from '@ai-sdk/provider'; import { generateText, wrapLanguageModel } from 'ai'; import 'dotenv/config'; const logProviderMetadataMiddleware: LanguageModelV2Middleware = { transformParams: async ({ params }) => { console.log( 'providerOptions: ' + JSON.stringify(params.providerOptions, null, 2), ); return params; }, }; async function main() { const { text } = await generateText({ model: wrapLanguageModel({ model: openai('gpt-4o'), middleware: logProviderMetadataMiddleware, }), providerOptions: { myMiddleware: { example: 'value', }, }, prompt: 'Invent a new holiday and describe its traditions.', }); console.log(text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-logprobs.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai('gpt-3.5-turbo'), prompt: 'Invent a new holiday and describe its traditions.', providerOptions: { openai: { logprobs: 2, }, }, }); console.log(result.providerMetadata?.openai.logprobs); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-multi-step.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, stepCountIs, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const { text, usage } = await generateText({ model: openai('gpt-4o-2024-08-06'), tools: { currentLocation: tool({ description: 'Get the current location.', inputSchema: z.object({}), execute: async () => { const locations = ['New York', 'London', 'Paris']; return { location: locations[Math.floor(Math.random() * locations.length)], }; }, }), weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), prompt: 'What is the weather in my current location?', onStepFinish: step => { console.log(JSON.stringify(step, null, 2)); }, }); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-nullable.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, tool } from 'ai'; import { z } from 'zod'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai('gpt-4o-mini'), temperature: 0, // Explicitly set temperature to 0 tools: { executeCommand: tool({ description: 'Execute a command with optional working directory and timeout', inputSchema: z.object({ command: z.string().describe('The command to execute'), workdir: z .string() .nullable() .describe('Working directory (null if not specified)'), timeout: z .string() .nullable() .describe('Timeout value (null if not specified)'), }), execute: async ({ command, workdir, timeout }) => { return `Executed: ${command} in ${workdir || 'current dir'} with timeout ${timeout || 'default'}`; }, }), }, prompt: 'List the files in the /tmp directory with a 30 second timeout', }); console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-output-object.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, stepCountIs, Output, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const { experimental_output } = await generateText({ model: openai('gpt-4o-mini'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), // location below is inferred to be a string: execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, experimental_output: Output.object({ schema: z.object({ location: z.string(), temperature: z.number(), }), }), stopWhen: stepCountIs(2), prompt: 'What is the weather in San Francisco?', }); // { location: 'San Francisco', temperature: 81 } console.log(experimental_output); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-pdf-url.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai('gpt-4o'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: new URL( 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/ai.pdf?raw=true', ), mediaType: 'application/pdf', filename: 'ai.pdf', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-pdf.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: openai('gpt-4o'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', // filename: 'ai.pdf', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-provider-defined-tools.ts --- import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai('gpt-4o-mini'), prompt: 'Search for information about TypeScript best practices', tools: { webSearch: openai.tools.webSearchPreview({ searchContextSize: 'medium', userLocation: { type: 'approximate', city: 'San Francisco', region: 'California', country: 'US', }, }), fileSearch: openai.tools.fileSearch({ maxNumResults: 5, ranking: { ranker: 'auto', }, }), }, }); console.log('Result:', result.text); console.log('Tool calls made:', result.toolCalls.length); for (const toolCall of result.toolCalls) { console.log(`\nTool Call:`); console.log(`- Tool: ${toolCall.toolName}`); console.log(`- Input:`, JSON.stringify(toolCall.input, null, 2)); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-reasoning.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const { text, usage } = await generateText({ model: openai('o3-mini'), prompt: 'How many "r"s are in the word "strawberry"?', temperature: 0.5, // should get ignored (warning) maxOutputTokens: 1000, // mapped to max_completion_tokens }); console.log(text); console.log('Usage:', usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-request-body.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const { request } = await generateText({ model: openai('gpt-4o-mini'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log('REQUEST BODY'); console.log(request.body); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-responses-chatbot.ts --- import { openai } from '@ai-sdk/openai'; import { ModelMessage, generateText } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { weatherTool } from '../tools/weather-tool'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; while (true) { if (!toolResponseAvailable) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); } const { text, toolCalls, toolResults, response } = await generateText({ model: openai.responses('o3'), tools: { weatherTool }, system: `You are a helpful, respectful and honest assistant.`, messages, }); toolResponseAvailable = false; if (text) { process.stdout.write(`\nAssistant: ${text}`); } for (const { toolName, input } of toolCalls) { process.stdout.write( `\nTool call: '${toolName}' ${JSON.stringify(input)}`, ); } for (const { toolName, output } of toolResults) { process.stdout.write( `\nTool response: '${toolName}' ${JSON.stringify(output)}`, ); } process.stdout.write('\n\n'); messages.push(...response.messages); toolResponseAvailable = toolCalls.length > 0; } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-responses-image-url.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai.responses('gpt-4o-mini'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/comic-cat.png?raw=true', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-responses-image.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import fs from 'node:fs'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai.responses('gpt-4o-mini'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png'), providerOptions: { openai: { imageDetail: 'low' }, }, }, ], }, ], }); console.log(result.text); console.log(); console.log('Finish reason:', result.finishReason); console.log('Usage:', result.usage); console.log('Request:', JSON.stringify(result.request, null, 2)); console.log('Response:', JSON.stringify(result.response, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-responses-output-object.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, stepCountIs, Output, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const { experimental_output } = await generateText({ model: openai.responses('gpt-4o-mini'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), // location below is inferred to be a string: execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, experimental_output: Output.object({ schema: z.object({ location: z.string(), temperature: z.number(), }), }), stopWhen: stepCountIs(2), prompt: 'What is the weather in San Francisco?', }); // { location: 'San Francisco', temperature: 81 } console.log(experimental_output); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-responses-pdf-url.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai.responses('gpt-4o'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: new URL( 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/ai.pdf?raw=true', ), mediaType: 'application/pdf', filename: 'ai.pdf', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-responses-pdf.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: openai.responses('gpt-4o'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', // filename: 'ai.pdf', }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-responses-previous-response-id.ts --- import { openai, OpenAIResponsesProviderOptions } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result1 = await generateText({ model: openai.responses('gpt-4o-mini'), prompt: 'Invent a new holiday and describe its traditions.', }); const result2 = await generateText({ model: openai.responses('gpt-4o-mini'), prompt: 'Summarize in 2 sentences', providerOptions: { openai: { previousResponseId: result1.providerMetadata?.openai .responseId as string, } satisfies OpenAIResponsesProviderOptions, }, }); console.log(result2.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-responses-reasoning-summary.ts --- import { openai, OpenAIResponsesProviderOptions } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ // supported: o4-mini, o3, o3-mini and o1 model: openai.responses('o3-mini'), prompt: 'Tell me about the debate over Taqueria La Cumbre and El Farolito and who created the San Francisco Mission-style burrito.', providerOptions: { openai: { // https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries reasoningSummary: 'auto', // auto gives you the best available summary (detailed > auto > None) } satisfies OpenAIResponsesProviderOptions, }, }); process.stdout.write('\x1b[34m'); console.log(JSON.stringify(result.reasoning, null, 2)); process.stdout.write('\x1b[0m'); console.log(result.text); console.log(); console.log('Finish reason:', result.finishReason); console.log('Usage:', result.usage); console.log(); console.log('Request body:', JSON.stringify(result.request.body, null, 2)); console.log('Response body:', JSON.stringify(result.response.body, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-responses-reasoning-zero-data-retention.ts --- import { openai, OpenAIResponsesProviderOptions } from '@ai-sdk/openai'; import { generateText, UserModelMessage } from 'ai'; import 'dotenv/config'; async function main() { const result1 = await generateText({ model: openai.responses('o3-mini'), prompt: 'Analyze the following encrypted data: U2VjcmV0UGFzc3dvcmQxMjM=. What type of encryption is this and what secret does it contain?', providerOptions: { openai: { store: false, // No data retention - makes interaction stateless reasoningEffort: 'medium', reasoningSummary: 'auto', include: ['reasoning.encrypted_content'], // Hence, we need to retrieve the model's encrypted reasoning to be able to pass it to follow-up requests } satisfies OpenAIResponsesProviderOptions, }, }); console.log('=== First request ==='); process.stdout.write('\x1b[34m'); console.log(JSON.stringify(result1.reasoning, null, 2)); process.stdout.write('\x1b[0m'); console.log(result1.text); console.log(); console.log('Finish reason:', result1.finishReason); console.log('Usage:', result1.usage); console.log(); console.log('Request body:', JSON.stringify(result1.request.body, null, 2)); console.log('Response body:', JSON.stringify(result1.response.body, null, 2)); const result2 = await generateText({ model: openai.responses('o3-mini'), prompt: [ { role: 'user', content: [ { type: 'text', text: 'Analyze the following encrypted data: U2VjcmV0UGFzc3dvcmQxMjM=. What type of encryption is this and what secret does it contain?', }, ], }, ...result1.response.messages, // Need to pass all previous messages to the follow-up request { role: 'user', content: 'Based on your previous analysis, what security recommendations would you make?', } satisfies UserModelMessage, ], providerOptions: { openai: { store: false, // No data retention - makes interaction stateless reasoningEffort: 'medium', reasoningSummary: 'auto', include: ['reasoning.encrypted_content'], // Hence, we need to retrieve the model's encrypted reasoning to be able to pass it to follow-up requests } satisfies OpenAIResponsesProviderOptions, }, }); console.log('=== Second request ==='); process.stdout.write('\x1b[34m'); console.log(JSON.stringify(result2.reasoning, null, 2)); process.stdout.write('\x1b[0m'); console.log(result2.text); console.log(); console.log('Finish reason:', result2.finishReason); console.log('Usage:', result2.usage); console.log(); console.log('Request body:', JSON.stringify(result2.request.body, null, 2)); console.log('Response body:', JSON.stringify(result2.response.body, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-responses-reasoning.ts --- import { openai, OpenAIResponsesProviderOptions } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai.responses('o3-mini'), prompt: 'How many "r"s are in the word "strawberry"?', temperature: 0.5, // should get ignored (warning) providerOptions: { openai: { reasoningEffort: 'low', } satisfies OpenAIResponsesProviderOptions, }, }); process.stdout.write('\x1b[34m'); console.log(JSON.stringify(result.reasoning, null, 2)); process.stdout.write('\x1b[0m'); console.log(result.text); console.log(); console.log('Finish reason:', result.finishReason); console.log('Usage:', result.usage); console.log(); console.log('Request:', JSON.stringify(result.request, null, 2)); console.log('Response:', JSON.stringify(result.response, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-responses-roundtrip-server-side-tools.ts --- import { createOpenAI } from '@ai-sdk/openai'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; const openai = createOpenAI({ // Console log the API request body for debugging fetch: async (url, options) => { console.log( `Body ${JSON.stringify(JSON.parse(options!.body! as string), null, 2)}`, ); return await fetch(url, options); }, }); async function main() { const { content } = await generateText({ model: openai.responses('gpt-4o-mini'), tools: { web_search_preview: openai.tools.webSearchPreview({}), checkStatus: tool({ description: 'Check implementation status', inputSchema: z.object({ component: z.string(), }), execute: async ({ component }) => { console.log(`Executing client tool: ${component}`); return { status: 'working', component }; }, }), }, prompt: 'Search for San Francisco tech news, then check server-side tool status.', }); console.log('\n=== Results ==='); for (const part of content) { if (part.type === 'tool-call') { console.log( `Tool Call: ${part.toolName} (providerExecuted: ${part.providerExecuted})`, ); } else if (part.type === 'tool-result') { console.log( `Tool Result: ${part.toolName} (providerExecuted: ${part.providerExecuted})`, ); } else if (part.type === 'text') { console.log(`Text: ${part.text.substring(0, 80)}...`); } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-responses-tool-call.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: openai.responses('gpt-4o-mini'), tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool calls: for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; // string break; } case 'weather': { toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // toolResult.input.city; // string // toolResult.result; // break; // } case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log(JSON.stringify(result.toolCalls, null, 2)); console.log(JSON.stringify(result.toolResults, null, 2)); console.log(JSON.stringify(result.finishReason, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-responses-websearch.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai.responses('gpt-4o-mini'), prompt: 'What happened in San Francisco last week?', tools: { web_search_preview: openai.tools.webSearchPreview({}), }, }); console.log(result.text); console.log(); console.log('Sources:'); console.log(result.sources); console.log(); console.log('Finish reason:', result.finishReason); console.log('Usage:', result.usage); console.log('Request:', JSON.stringify(result.request, null, 2)); console.log('Response:', JSON.stringify(result.response, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-responses.ts --- import { openai, OpenAIResponsesProviderOptions } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai.responses('gpt-4o-mini'), prompt: 'Invent a new holiday and describe its traditions.', maxOutputTokens: 1000, providerOptions: { openai: { parallelToolCalls: false, store: false, metadata: { key1: 'value1', key2: 'value2', }, user: 'user_123', } satisfies OpenAIResponsesProviderOptions, }, }); console.log(result.text); console.log(); console.log('Finish reason:', result.finishReason); console.log('Usage:', result.usage); console.log('Request:', JSON.stringify(result.request, null, 2)); console.log('Response:', JSON.stringify(result.response, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-store-generation.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const { text, usage } = await generateText({ model: openai('gpt-4o-mini'), prompt: 'Invent a new holiday and describe its traditions.', providerOptions: { openai: { store: true, metadata: { custom: 'value', }, }, }, }); console.log(text); console.log(); console.log('Usage:', usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-system-message-a.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai('gpt-3.5-turbo'), messages: [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: 'What is the capital of France?' }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-system-message-b.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai('gpt-3.5-turbo'), system: 'You are a helpful assistant.', messages: [{ role: 'user', content: 'What is the capital of France?' }], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-timeout.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const { text, usage } = await generateText({ model: openai('gpt-3.5-turbo'), prompt: 'Invent a new holiday and describe its traditions.', abortSignal: AbortSignal.timeout(1000), }); console.log(text); console.log(); console.log('Usage:', usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-tool-call-raw-json-schema.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, jsonSchema, tool } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: openai('gpt-3.5-turbo'), maxOutputTokens: 512, tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: jsonSchema<{ location: string }>({ type: 'object', properties: { location: { type: 'string' }, }, required: ['location'], }), // location below is inferred to be a string: execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), cityAttractions: tool({ inputSchema: jsonSchema<{ city: string }>({ type: 'object', properties: { city: { type: 'string' }, }, required: ['city'], }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool calls: for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; // string break; } case 'weather': { toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // toolResult.input.city; // string // toolResult.result; // break; // } case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-tool-call-with-context.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateText({ model: openai('gpt-4o'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }, { experimental_context: context }) => { const typedContext = context as { weatherApiKey: string }; // or use type validation library console.log(typedContext); return { location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }; }, }), }, experimental_context: { weatherApiKey: '123' }, prompt: 'What is the weather in San Francisco?', }); console.log(JSON.stringify(result.toolResults, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-tool-call.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: openai('gpt-3.5-turbo'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool calls: for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; // string break; } case 'weather': { toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { case 'cityAttractions': { toolResult.input.city; // string toolResult.output; // any since no outputSchema is provided break; } case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-tool-choice.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: openai('gpt-3.5-turbo'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, toolChoice: { type: 'tool', toolName: 'weather', }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai-tool-execution-error.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = await generateText({ model: openai('gpt-4o-mini'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }): Promise<{ temperature: number }> => { throw new Error('could not get weather'); }, }), }, prompt: 'What is the weather in San Francisco?', }); console.log(JSON.stringify(result.content, null, 2)); console.log(JSON.stringify(result.response.messages, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/openai.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const { text, usage } = await generateText({ model: openai('gpt-3.5-turbo'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(text); console.log(); console.log('Usage:', usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/perplexity-images.ts --- import 'dotenv/config'; import { perplexity } from '@ai-sdk/perplexity'; import { generateText } from 'ai'; async function main() { const result = await generateText({ model: perplexity('sonar-pro'), prompt: 'Tell me about the earliest cave drawings known and include images.', providerOptions: { perplexity: { return_images: true, }, }, }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); console.log('Metadata:', result.providerMetadata); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/perplexity.ts --- import 'dotenv/config'; import { perplexity } from '@ai-sdk/perplexity'; import { generateText } from 'ai'; async function main() { const result = await generateText({ model: perplexity('sonar-pro'), prompt: 'What has happened in San Francisco recently?', providerOptions: { perplexity: { search_recency_filter: 'week', }, }, }); console.log(result.text); console.log(); console.log('Sources:', result.sources); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); console.log('Metadata:', result.providerMetadata); for (const source of result.sources) { if (source.sourceType === 'url') { console.log('ID:', source.id); console.log('Title:', source.title); console.log('URL:', source.url); console.log('Provider metadata:', source.providerMetadata); console.log(); } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/provider-defined-tools-working.ts --- import { generateText } from 'ai'; import { anthropic } from '@ai-sdk/anthropic'; import { openai } from '@ai-sdk/openai'; import 'dotenv/config'; async function main() { console.log('=== Demonstrating Refactored Provider-Defined Tools ===\n'); console.log('1. OpenAI Provider-Defined Tools (Successfully Refactored):'); const openaiWebSearch = openai.tools.webSearchPreview({ searchContextSize: 'medium', userLocation: { type: 'approximate', city: 'San Francisco', region: 'California', country: 'US', }, }); const openaiFileSearch = openai.tools.fileSearch({ maxNumResults: 5, ranking: { ranker: 'auto', }, }); console.log('OpenAI Web Search Tool created successfully'); console.log('OpenAI File Search Tool created successfully'); console.log('\n2. Anthropic Provider-Defined Tools (Working Example):'); const result = await generateText({ model: anthropic('claude-3-5-sonnet-20241022'), prompt: 'Search for current weather in Tokyo', tools: { web_search: anthropic.tools.webSearch_20250305({ maxUses: 2, allowedDomains: ['weather.com', 'accuweather.com'], userLocation: { type: 'approximate', city: 'Tokyo', region: 'Tokyo', country: 'JP', }, }), }, }); console.log('Anthropic Web Search Tool executed successfully'); console.log('Tool calls made:', result.toolCalls.length); for (const toolCall of result.toolCalls) { console.log(`\nTool Call:`); console.log(`- Tool: ${toolCall.toolName}`); console.log(`- Input:`, JSON.stringify(toolCall.input, null, 2)); } console.log('\n=== Refactoring Summary ==='); console.log( 'OpenAI tools refactored to use createProviderDefinedToolFactory', ); console.log( 'Anthropic tools refactored to use createProviderDefinedToolFactory', ); console.log( 'All tools now follow consistent pattern like computer_20250124.ts', ); console.log('Type safety improved with better TypeScript inference'); console.log('Anthropic tools working in production'); console.log('Factory pattern provides cleaner, more maintainable API'); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/togetherai-tool-call.ts --- import { togetherai } from '@ai-sdk/togetherai'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: togetherai('meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool calls: for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; // string break; } case 'weather': { toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // toolResult.input.city; // string // toolResult.result; // break; // } case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log('Text:', result.text); console.log('Tool Calls:', JSON.stringify(result.toolCalls, null, 2)); console.log('Tool Results:', JSON.stringify(result.toolResults, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/togetherai.ts --- import { togetherai } from '@ai-sdk/togetherai'; import { generateText } from 'ai'; import 'dotenv/config'; async function main() { const { text, usage } = await generateText({ model: togetherai('meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(text); console.log(); console.log('Usage:', usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/vercel-image.ts --- import { vercel } from '@ai-sdk/vercel'; import { generateText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = await generateText({ model: vercel('v0-1.0-md'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png') }, ], }, ], }); console.log(result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/vercel.ts --- import 'dotenv/config'; import { vercel } from '@ai-sdk/vercel'; import { generateText } from 'ai'; async function main() { const result = await generateText({ model: vercel('v0-1.5-md'), prompt: 'Implement Fibonacci in Lua.', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/xai-search.ts --- import 'dotenv/config'; import { xai } from '@ai-sdk/xai'; import { generateText } from 'ai'; async function main() { const result = await generateText({ model: xai('grok-3-latest'), prompt: 'What are the latest developments in AI?', providerOptions: { xai: { searchParameters: { mode: 'auto', returnCitations: true, maxSearchResults: 5, }, }, }, }); console.log(result.text); console.log(); console.log('Sources:', result.sources); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); for (const source of result.sources) { if (source.sourceType === 'url') { console.log('Source ID:', source.id); console.log('URL:', source.url); console.log(); } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/xai-structured-output.ts --- import 'dotenv/config'; import { generateText, Output } from 'ai'; import { xai } from '@ai-sdk/xai'; import { z } from 'zod/v4'; async function main() { const { experimental_output } = await generateText({ model: xai('grok-3-beta'), experimental_output: Output.object({ schema: z.object({ name: z.string(), age: z.number().nullable().describe('Age of the person.'), contact: z.object({ type: z.literal('email'), value: z.string(), }), occupation: z.object({ type: z.literal('employed'), company: z.string(), position: z.string(), }), }), }), prompt: 'Generate an example person for testing.', }); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/xai-tool-call.ts --- import { xai } from '@ai-sdk/xai'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = await generateText({ model: xai('grok-3-beta'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); // typed tool calls: for (const toolCall of result.toolCalls) { if (toolCall.dynamic) { continue; } switch (toolCall.toolName) { case 'cityAttractions': { toolCall.input.city; // string break; } case 'weather': { toolCall.input.location; // string break; } } } // typed tool results for tools with execute method: for (const toolResult of result.toolResults) { if (toolResult.dynamic) { continue; } switch (toolResult.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // toolResult.input.city; // string // toolResult.result; // break; // } case 'weather': { toolResult.input.location; // string toolResult.output.location; // string toolResult.output.temperature; // number break; } } } console.log('Text:', result.text); console.log('Tool Calls:', JSON.stringify(result.toolCalls, null, 2)); console.log('Tool Results:', JSON.stringify(result.toolResults, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/generate-text/xai.ts --- import 'dotenv/config'; import { xai } from '@ai-sdk/xai'; import { generateText } from 'ai'; async function main() { const result = await generateText({ model: xai('grok-3-beta'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result.text); console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/lib/present-image.ts --- import { Experimental_GeneratedImage as GeneratedImage } from 'ai'; import fs from 'node:fs'; import imageType from 'image-type'; import path from 'node:path'; import sharp from 'sharp'; import terminalImage from 'terminal-image'; const OUTPUT_DIR = 'output'; /** * Displays images in the terminal using a downsampled preview and saves the * original, full-resolution files to the output directory with unique * timestamps. * @param images - An array of generated images to process and display. */ export async function presentImages(images: GeneratedImage[]) { const timestamp = Date.now(); for (const [index, image] of images.entries()) { let srcBuffer = image.uint8Array; // Determine the format of the image. const format = await imageType(srcBuffer); const extension = format?.ext; if (!extension) { throw new Error('Unknown image format'); } if (extension === 'webp') { // `terminal-image` doesn't support WebP, so convert to PNG. srcBuffer = await sharp(srcBuffer).png().toBuffer(); } // Render the image to the terminal. console.log(await terminalImage.buffer(Buffer.from(srcBuffer))); // Save the original image to a file. fs.mkdirSync(OUTPUT_DIR, { recursive: true }); const filePath = path.join( OUTPUT_DIR, `image-${timestamp}-${index}.${extension}`, ); await fs.promises.writeFile(filePath, srcBuffer); console.log(`Saved image to ${filePath}`); } console.log(`Processed ${images.length} images`); } --- File: /ai/examples/ai-core/src/lib/save-audio.ts --- import { GeneratedAudioFile } from 'ai'; import fs from 'node:fs'; import path from 'node:path'; const OUTPUT_DIR = 'output'; const audioFormatMap = { 'audio/mpeg': 'mp3', 'audio/wav': 'wav', 'audio/flac': 'flac', 'audio/aac': 'aac', 'audio/ogg': 'ogg', }; /** * Saves a generated audio file to the output directory with unique timestamps. * @param audio - The generated audio file to save. */ export async function saveAudioFile(audio: GeneratedAudioFile) { const timestamp = Date.now(); const extension = audio.mediaType in audioFormatMap ? audioFormatMap[audio.mediaType as keyof typeof audioFormatMap] : 'mp3'; // Save the audio file to disk. fs.mkdirSync(OUTPUT_DIR, { recursive: true }); const filePath = path.join(OUTPUT_DIR, `audio-${timestamp}.${extension}`); await fs.promises.writeFile(filePath, audio.uint8Array); console.log(`Saved audio to ${filePath}`); } --- File: /ai/examples/ai-core/src/middleware/add-to-last-user-message.ts --- import { LanguageModelV2CallOptions } from '@ai-sdk/provider'; export function addToLastUserMessage({ text, params, }: { text: string; params: LanguageModelV2CallOptions; }): LanguageModelV2CallOptions { const { prompt, ...rest } = params; const lastMessage = prompt.at(-1); if (lastMessage?.role !== 'user') { return params; } return { ...rest, prompt: [ ...prompt.slice(0, -1), { ...lastMessage, content: [{ type: 'text', text }, ...lastMessage.content], }, ], }; } --- File: /ai/examples/ai-core/src/middleware/default-settings-example.ts --- import { openai } from '@ai-sdk/openai'; import { defaultSettingsMiddleware, generateText, wrapLanguageModel } from 'ai'; import 'dotenv/config'; async function main() { const result = await generateText({ model: wrapLanguageModel({ model: openai.responses('gpt-4o'), middleware: defaultSettingsMiddleware({ settings: { temperature: 0.5, providerOptions: { openai: { store: false, }, }, }, }), }), prompt: 'What cities are in the United States?', }); console.log(result.response.body); } main().catch(console.error); --- File: /ai/examples/ai-core/src/middleware/generate-text-cache-middleware-example.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, wrapLanguageModel } from 'ai'; import 'dotenv/config'; import { yourCacheMiddleware } from './your-cache-middleware'; async function main() { const modelWithCaching = wrapLanguageModel({ model: openai('gpt-4o'), middleware: yourCacheMiddleware, }); const start1 = Date.now(); const result1 = await generateText({ model: modelWithCaching, prompt: 'What cities are in the United States?', }); const end1 = Date.now(); const start2 = Date.now(); const result2 = await generateText({ model: modelWithCaching, prompt: 'What cities are in the United States?', }); const end2 = Date.now(); console.log(`Time taken for result1: ${end1 - start1}ms`); console.log(`Time taken for result2: ${end2 - start2}ms`); console.log(result1.text === result2.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/middleware/generate-text-log-middleware-example.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, wrapLanguageModel } from 'ai'; import 'dotenv/config'; import { yourLogMiddleware } from './your-log-middleware'; async function main() { const result = await generateText({ model: wrapLanguageModel({ model: openai('gpt-4o'), middleware: yourLogMiddleware, }), prompt: 'What cities are in the United States?', }); } main().catch(console.error); --- File: /ai/examples/ai-core/src/middleware/get-last-user-message-text.ts --- import { LanguageModelV2Prompt } from '@ai-sdk/provider'; export function getLastUserMessageText({ prompt, }: { prompt: LanguageModelV2Prompt; }): string | undefined { const lastMessage = prompt.at(-1); if (lastMessage?.role !== 'user') { return undefined; } return lastMessage.content.length === 0 ? undefined : lastMessage.content .filter(c => c.type === 'text') .map(c => c.text) .join('\n'); } --- File: /ai/examples/ai-core/src/middleware/simulate-streaming-example.ts --- import { openai } from '@ai-sdk/openai'; import { simulateStreamingMiddleware, streamText, wrapLanguageModel } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: wrapLanguageModel({ model: openai('gpt-4o'), middleware: simulateStreamingMiddleware(), }), prompt: 'What cities are in the United States?', }); // will return everything at once after a while for await (const chunk of result.textStream) { console.log(chunk); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/middleware/stream-text-log-middleware.ts --- import { openai } from '@ai-sdk/openai'; import { streamText, wrapLanguageModel } from 'ai'; import 'dotenv/config'; import { yourLogMiddleware } from './your-log-middleware'; async function main() { const result = streamText({ model: wrapLanguageModel({ model: openai('gpt-4o'), middleware: yourLogMiddleware, }), prompt: 'What cities are in the United States?', }); for await (const textPart of result.textStream) { // consume the stream } } main().catch(console.error); --- File: /ai/examples/ai-core/src/middleware/stream-text-rag-middleware.ts --- import { openai } from '@ai-sdk/openai'; import { streamText, wrapLanguageModel } from 'ai'; import 'dotenv/config'; import { yourRagMiddleware } from './your-rag-middleware'; async function main() { const result = streamText({ model: wrapLanguageModel({ model: openai('gpt-4o'), middleware: yourRagMiddleware, }), prompt: 'What cities are in the United States?', }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/middleware/your-cache-middleware.ts --- import { LanguageModelV2Middleware } from '@ai-sdk/provider'; const cache = new Map<string, any>(); export const yourCacheMiddleware: LanguageModelV2Middleware = { wrapGenerate: async ({ doGenerate, params }) => { const cacheKey = JSON.stringify(params); if (cache.has(cacheKey)) { return cache.get(cacheKey); } const result = await doGenerate(); cache.set(cacheKey, result); return result; }, // here you would implement the caching logic for streaming }; --- File: /ai/examples/ai-core/src/middleware/your-guardrail-middleware.ts --- import { LanguageModelV2Content, LanguageModelV2Middleware, } from '@ai-sdk/provider'; export const yourGuardrailMiddleware: LanguageModelV2Middleware = { wrapGenerate: async ({ doGenerate }) => { const { content, ...rest } = await doGenerate(); // filtering approach, e.g. for PII or other sensitive information: const cleanedContent: Array<LanguageModelV2Content> = content.map(part => { return part.type === 'text' ? { type: 'text', text: part.text.replace(/badword/g, '<REDACTED>'), } : part; }); return { content: cleanedContent, ...rest, }; }, // here you would implement the guardrail logic for streaming // Note: streaming guardrails are difficult to implement, because // you do not know the full content of the stream until it's finished. }; --- File: /ai/examples/ai-core/src/middleware/your-log-middleware.ts --- import { LanguageModelV2Middleware, LanguageModelV2StreamPart, } from '@ai-sdk/provider'; export const yourLogMiddleware: LanguageModelV2Middleware = { wrapGenerate: async ({ doGenerate, params }) => { console.log('doGenerate called'); console.log(`params: ${JSON.stringify(params, null, 2)}`); const result = await doGenerate(); console.log('doGenerate finished'); console.log(`generated content: ${JSON.stringify(result.content)}`); return result; }, wrapStream: async ({ doStream, params }) => { console.log('doStream called'); console.log(`params: ${JSON.stringify(params, null, 2)}`); const { stream, ...rest } = await doStream(); let generatedText = ''; const transformStream = new TransformStream< LanguageModelV2StreamPart, LanguageModelV2StreamPart >({ transform(chunk, controller) { if (chunk.type === 'text-delta') { generatedText += chunk.delta; } controller.enqueue(chunk); }, flush() { console.log('doStream finished'); console.log(`generated text: ${generatedText}`); }, }); return { stream: stream.pipeThrough(transformStream), ...rest, }; }, }; --- File: /ai/examples/ai-core/src/middleware/your-rag-middleware.ts --- import { LanguageModelV2Middleware } from '@ai-sdk/provider'; import { addToLastUserMessage } from './add-to-last-user-message'; import { getLastUserMessageText } from './get-last-user-message-text'; export const yourRagMiddleware: LanguageModelV2Middleware = { transformParams: async ({ params }) => { const lastUserMessageText = getLastUserMessageText({ prompt: params.prompt, }); if (lastUserMessageText == null) { return params; // do not use RAG (send unmodified parameters) } const instruction = 'Use the following information to answer the question:\n' + findSources({ text: lastUserMessageText }) .map(chunk => JSON.stringify(chunk)) .join('\n'); return addToLastUserMessage({ params, text: instruction }); }, }; // example, could implement anything here: function findSources({ text }: { text: string }): Array<{ title: string; previewText: string | undefined; url: string | undefined; }> { return [ { title: 'New York', previewText: 'New York is a city in the United States.', url: 'https://en.wikipedia.org/wiki/New_York', }, { title: 'San Francisco', previewText: 'San Francisco is a city in the United States.', url: 'https://en.wikipedia.org/wiki/San_Francisco', }, ]; } --- File: /ai/examples/ai-core/src/registry/embed-openai.ts --- import { embed } from 'ai'; import { registry } from './setup-registry'; async function main() { const { embedding } = await embed({ model: registry.textEmbeddingModel('openai:text-embedding-3-small'), value: 'sunny day at the beach', }); console.log(embedding); } main().catch(console.error); --- File: /ai/examples/ai-core/src/registry/generate-image.ts --- import { experimental_generateImage as generateImage } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; import { myImageModels } from './setup-registry'; async function main() { const { image } = await generateImage({ model: myImageModels.imageModel('flux'), prompt: 'The Loch Ness Monster getting a manicure', }); const filename = `image-${Date.now()}.png`; fs.writeFileSync(filename, image.uint8Array); console.log(`Image saved to ${filename}`); } main().catch(console.error); --- File: /ai/examples/ai-core/src/registry/generate-speech-openai.ts --- import { experimental_generateSpeech as generateSpeech } from 'ai'; import { registry } from './setup-registry'; async function main() { const { audio } = await generateSpeech({ model: registry.speechModel('openai:tts-1'), text: 'Hello, this is a test of speech synthesis using the provider registry!', }); console.log('Generated audio:', audio); } main().catch(console.error); --- File: /ai/examples/ai-core/src/registry/setup-registry.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { fal } from '@ai-sdk/fal'; import { groq } from '@ai-sdk/groq'; import { luma } from '@ai-sdk/luma'; import { mistral } from '@ai-sdk/mistral'; import { openai } from '@ai-sdk/openai'; import { replicate } from '@ai-sdk/replicate'; import { xai } from '@ai-sdk/xai'; import { createProviderRegistry, customProvider, defaultSettingsMiddleware, wrapLanguageModel, } from 'ai'; import 'dotenv/config'; // custom provider with alias names: const myAnthropic = customProvider({ languageModels: { opus: anthropic('claude-3-opus-20240229'), sonnet: anthropic('claude-3-5-sonnet-20240620'), haiku: anthropic('claude-3-haiku-20240307'), }, fallbackProvider: anthropic, }); // custom provider with different model settings: const myOpenAI = customProvider({ languageModels: { // replacement model with custom provider options: 'gpt-4': wrapLanguageModel({ model: openai('gpt-4'), middleware: defaultSettingsMiddleware({ settings: { providerOptions: { openai: { reasoningEffort: 'high', }, }, }, }), }), // alias model with custom provider options: 'gpt-4o-high-reasoning': wrapLanguageModel({ model: openai('gpt-4o'), middleware: defaultSettingsMiddleware({ settings: { providerOptions: { openai: { reasoningEffort: 'high', }, }, }, }), }), }, fallbackProvider: openai, }); export const registry = createProviderRegistry({ mistral, anthropic: myAnthropic, openai: myOpenAI, xai, groq, }); registry.languageModel('anthropic:haiku'); const registryWithCustomSeparator = createProviderRegistry( { mistral, anthropic: myAnthropic, openai: myOpenAI, xai, groq, }, { separator: ' > ' }, ); registryWithCustomSeparator.languageModel('anthropic > haiku'); export const myImageModels = customProvider({ imageModels: { recraft: fal.imageModel('recraft-v3'), photon: luma.imageModel('photon-flash-1'), flux: replicate.imageModel('black-forest-labs/flux-schnell'), }, }); --- File: /ai/examples/ai-core/src/registry/stream-text-anthropic.ts --- import { streamText } from 'ai'; import { registry } from './setup-registry'; async function main() { const result = streamText({ model: registry.languageModel('anthropic:haiku'), prompt: 'Invent a new holiday and describe its traditions.', }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/registry/stream-text-groq.ts --- import { streamText } from 'ai'; import { registry } from './setup-registry'; async function main() { const result = streamText({ model: registry.languageModel('groq:gemma2-9b-it'), prompt: 'Invent a new holiday and describe its traditions.', }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/registry/stream-text-openai.ts --- import { streamText } from 'ai'; import { registry } from './setup-registry'; async function main() { const result = streamText({ model: registry.languageModel('openai:gpt-4-turbo'), prompt: 'Invent a new holiday and describe its traditions.', }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/registry/stream-text-xai.ts --- import { streamText } from 'ai'; import { registry } from './setup-registry'; async function main() { const result = streamText({ model: registry.languageModel('xai:grok-3-beta'), prompt: 'Invent a new holiday and describe its traditions.', }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/registry/transcribe-openai.ts --- import { experimental_transcribe as transcribe } from 'ai'; import { readFile } from 'fs/promises'; import { registry } from './setup-registry'; async function main() { const result = await transcribe({ model: registry.transcriptionModel('openai:whisper-1'), audio: await readFile('../data/galileo.mp3'), }); console.log('Transcript:', result.text); console.log('Language:', result.language); console.log('Duration:', result.durationInSeconds); console.log('Segments:', result.segments); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/amazon-bedrock.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: bedrock('anthropic.claude-3-5-sonnet-20240620-v1:0'), schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/anthropic.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: anthropic('claude-sonnet-4-20250514'), maxOutputTokens: 5000, schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', onError: err => { console.error(err); }, temperature: 0, }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/azure.ts --- import { azure } from '@ai-sdk/azure'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: azure('v0-gpt-35-turbo'), // use your own deployment schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } console.log(); console.log('Token usage:', await result.usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/fireworks.ts --- import { fireworks } from '@ai-sdk/fireworks'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: fireworks('accounts/fireworks/models/firefunction-v1'), maxOutputTokens: 2000, schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/gateway.ts --- import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: 'xai/grok-3', schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } console.log(); console.log('Token usage:', await result.usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/google-caching.ts --- import 'dotenv/config'; import { google } from '@ai-sdk/google'; import { streamObject } from 'ai'; import fs from 'node:fs'; import { z } from 'zod'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); async function main() { const result1 = streamObject({ model: google('gemini-2.5-flash'), prompt: errorMessage, schema: z.object({ error: z.string(), stack: z.string(), }), }); for await (const _ of result1.partialObjectStream) { void _; } const providerMetadata1 = await result1.providerMetadata; console.log(providerMetadata1?.google); // e.g. // { // groundingMetadata: null, // safetyRatings: null, // usageMetadata: { // thoughtsTokenCount: 857, // promptTokenCount: 2152, // candidatesTokenCount: 1075, // totalTokenCount: 4084 // } // } const result2 = streamObject({ model: google('gemini-2.5-flash'), prompt: errorMessage, schema: z.object({ error: z.string(), stack: z.string(), }), }); for await (const _ of result2.partialObjectStream) { void _; } const providerMetadata2 = await result2.providerMetadata; console.log(providerMetadata2?.google); // e.g. // { // groundingMetadata: null, // safetyRatings: null, // usageMetadata: { // cachedContentTokenCount: 1880, // thoughtsTokenCount: 1381, // promptTokenCount: 2152, // candidatesTokenCount: 914, // totalTokenCount: 4447 // } // } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/google-vertex-anthropic.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { streamObject } from 'ai'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), maxOutputTokens: 2000, schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/google-vertex.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: vertex('gemini-1.5-pro'), schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/google.ts --- import { google } from '@ai-sdk/google'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: google('gemini-1.5-pro-002'), schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/groq.ts --- import { groq } from '@ai-sdk/groq'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: groq('llama-3.1-70b-versatile'), schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } console.log(); console.log('Token usage:', await result.usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/mistral.ts --- import { mistral } from '@ai-sdk/mistral'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: mistral('open-mistral-7b'), maxOutputTokens: 2000, schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/mock.ts --- import { streamObject } from 'ai'; import { convertArrayToReadableStream, MockLanguageModelV2 } from 'ai/test'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '0' }, { type: 'text-delta', id: '0', delta: '{ ' }, { type: 'text-delta', id: '0', delta: '"content": ' }, { type: 'text-delta', id: '0', delta: `"Hello, ` }, { type: 'text-delta', id: '0', delta: `world` }, { type: 'text-delta', id: '0', delta: `!"` }, { type: 'text-delta', id: '0', delta: ' }' }, { type: 'text-end', id: '0' }, { type: 'finish', finishReason: 'stop', logprobs: undefined, usage: { inputTokens: 3, outputTokens: 10, totalTokens: 13, }, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'Hello, test!', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/nim.ts --- import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { streamObject } from 'ai'; import { z } from 'zod/v4'; import 'dotenv/config'; async function main() { const nim = createOpenAICompatible({ baseURL: 'https://integrate.api.nvidia.com/v1', name: 'nim', headers: { Authorization: `Bearer ${process.env.NIM_API_KEY}`, }, }); const model = nim.chatModel('meta/llama-3.3-70b-instruct'); const result = streamObject({ model, schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } console.log(); console.log('Token usage:', await result.usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/openai-array.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const { elementStream: destinations } = streamObject({ model: openai('gpt-4o'), output: 'array', schema: z.object({ city: z.string(), country: z.string(), description: z.string(), attractions: z.array(z.string()).describe('List of major attractions.'), }), prompt: 'What are the top 5 cities for short vacations in Europe?', }); for await (const destination of destinations) { console.log(destination); // destination is a complete array element } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/openai-compatible-togetherai.ts --- import 'dotenv/config'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { streamObject } from 'ai'; import { z } from 'zod/v4'; async function main() { const togetherai = createOpenAICompatible({ baseURL: 'https://api.together.xyz/v1', name: 'togetherai', headers: { Authorization: `Bearer ${process.env.TOGETHER_AI_API_KEY}`, }, }); const model = togetherai.chatModel('mistralai/Mistral-7B-Instruct-v0.1'); const result = streamObject({ model, schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } console.log(); console.log('Token usage:', await result.usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/openai-fullstream.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: openai('gpt-4o'), maxOutputTokens: 2000, schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), providerOptions: { openai: { logprobs: 2, }, }, prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const part of result.fullStream) { switch (part.type) { case 'object': console.clear(); console.log(part.object); break; case 'finish': { console.log('Finish reason:', part.finishReason); console.log('Logprobs:', part.providerMetadata?.openai.logprobs); console.log('Usage:', part.usage); break; } case 'error': console.error('Error:', part.error); break; } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/openai-no-schema.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import 'dotenv/config'; async function main() { const result = streamObject({ model: openai('gpt-4o-2024-08-06'), output: 'no-schema', prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/openai-object.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: openai('gpt-4-turbo'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.string()), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); result.object .then(({ recipe }) => { // do something with the fully typed, final object: console.log('Recipe:', JSON.stringify(recipe, null, 2)); }) .catch(error => { // handle type validation failure // (when the object does not match the schema): console.error(error); }); // note: the stream needs to be consumed because of backpressure for await (const partialObject of result.partialObjectStream) { } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/openai-on-finish.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: openai('gpt-4-turbo'), schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', onFinish({ usage, object, error }) { console.log(); console.log('onFinish'); console.log('Token usage:', usage); // handle type validation failure (when the object does not match the schema): if (object === undefined) { console.error('Error:', error); } else { console.log('Final object:', JSON.stringify(object, null, 2)); } }, }); // consume the partialObjectStream: for await (const partialObject of result.partialObjectStream) { } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/openai-raw-json-schema.ts --- import { openai } from '@ai-sdk/openai'; import { jsonSchema, streamObject } from 'ai'; import 'dotenv/config'; async function main() { const result = streamObject({ model: openai('gpt-4-turbo'), schema: jsonSchema<{ recipe: { name: string; ingredients: { name: string; amount: string }[]; steps: string[]; }; }>({ type: 'object', properties: { recipe: { type: 'object', properties: { name: { type: 'string' }, ingredients: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, amount: { type: 'string' }, }, required: ['name', 'amount'], }, }, steps: { type: 'array', items: { type: 'string' }, }, }, required: ['name', 'ingredients', 'steps'], }, }, required: ['recipe'], }), prompt: 'Generate a lasagna recipe.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(JSON.stringify(partialObject, null, 2)); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/openai-reasoning.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: openai('o1'), schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { // console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/openai-request-body.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: openai('gpt-4o-mini'), schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); // consume stream for await (const part of result.partialObjectStream) { } console.log('REQUEST BODY'); console.log((await result.request).body); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/openai-responses.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: openai.responses('gpt-4o-mini'), schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', onError: error => { console.error(error); }, }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/openai-store-generation.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: openai('gpt-4-turbo'), schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', providerOptions: { openai: { store: true, metadata: { custom: 'value', }, }, }, }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/openai-stream-object-name-description.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: openai('gpt-4o-2024-08-06'), schemaName: 'recipe', schemaDescription: 'A recipe for lasagna.', schema: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), prompt: 'Generate a lasagna recipe.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/openai-stream-object.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: openai('gpt-4o-2024-08-06'), schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/openai-token-usage.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject, LanguageModelUsage } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: openai('gpt-4-turbo'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array(z.string()), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', }); // your custom function to record usage: function recordUsage(usage: LanguageModelUsage) { console.log('Input tokens:', usage.inputTokens); console.log('Cached input tokens:', usage.cachedInputTokens); console.log('Reasoning tokens:', usage.reasoningTokens); console.log('Output tokens:', usage.outputTokens); console.log('Total tokens:', usage.totalTokens); } // use as promise: result.usage.then(recordUsage); // use with async/await: recordUsage(await result.usage); // note: the stream needs to be consumed because of backpressure for await (const partialObject of result.partialObjectStream) { } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/openai-unstructured-output.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: openai('gpt-4o-2024-08-06'), providerOptions: { openai: { structuredOutputs: false, }, }, schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/openai.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: openai('gpt-4o-mini'), schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/togetherai.ts --- import { togetherai } from '@ai-sdk/togetherai'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: togetherai.chatModel('mistralai/Mistral-7B-Instruct-v0.1'), schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } console.log(); console.log('Token usage:', await result.usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/vercel.ts --- import { vercel } from '@ai-sdk/vercel'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: vercel('v0-1.5-md'), schema: z.object({ button: z.object({ element: z.string(), baseStyles: z.object({ padding: z.string(), borderRadius: z.string(), border: z.string(), backgroundColor: z.string(), color: z.string(), cursor: z.string(), }), hoverStyles: z.object({ backgroundColor: z.string(), transform: z.string().optional(), }), }), }), prompt: 'Generate CSS styles for a modern primary button component.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } console.log(); console.log('Token usage:', await result.usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/xai-structured-outputs-name-description.ts --- import { xai } from '@ai-sdk/xai'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: xai('grok-3-beta'), schemaName: 'recipe', schemaDescription: 'A recipe for lasagna.', schema: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), prompt: 'Generate a lasagna recipe.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-object/xai.ts --- import { xai } from '@ai-sdk/xai'; import { streamObject } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamObject({ model: xai('grok-3-beta'), schema: z.object({ characters: z.array( z.object({ name: z.string(), class: z .string() .describe('Character class, e.g. warrior, mage, or thief.'), description: z.string(), }), ), }), prompt: 'Generate 3 character descriptions for a fantasy role playing game.', }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } console.log(); console.log('Token usage:', await result.usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock-activetools.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { streamText, tool, stepCountIs } from 'ai'; import { z } from 'zod'; import 'dotenv/config'; async function main() { const result = streamText({ model: bedrock('anthropic.claude-3-5-sonnet-20241022-v2:0'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ city: z.string() }), execute: async ({ city }) => ({ result: `The weather in ${city} is 20°C.`, }), }), }, stopWhen: [stepCountIs(5)], prepareStep: ({ stepNumber }) => { if (stepNumber > 0) { console.log(`Setting activeTools: [] for step ${stepNumber}`); return { activeTools: [], }; } return undefined; }, toolChoice: 'auto', prompt: 'What is the weather in Toronto, Calgary, and Vancouver?', }); for await (const part of result.fullStream) { switch (part.type) { case 'start-step': console.log('Step started'); break; case 'tool-call': console.log( `Tool call: ${part.toolName}(${JSON.stringify(part.input)})`, ); break; case 'tool-result': console.log(`Tool result: ${JSON.stringify(part.output)}`); break; case 'text-delta': process.stdout.write(part.text); break; case 'finish-step': console.log('Step finished'); break; case 'finish': console.log('Stream finished'); break; } } console.log(); console.log('Usage:', await result.usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock-anthropic-bash.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { anthropicTools } from '@ai-sdk/anthropic/internal'; import { stepCountIs, streamText, ToolCallPart, ToolResultPart } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: bedrock('us.anthropic.claude-sonnet-4-20250514-v1:0'), tools: { bash: anthropicTools.bash_20250124({ async execute({ command }) { console.log('COMMAND', command); return [ { type: 'text', text: ` ❯ ls README.md build data node_modules package.json src tsconfig.json `, }, ]; }, }), }, prompt: 'List the files in my home directory.', stopWhen: stepCountIs(2), }); let fullResponse = ''; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const delta of result.fullStream) { switch (delta.type) { case 'text-delta': { fullResponse += delta.text; process.stdout.write(delta.text); break; } case 'tool-call': { toolCalls.push(delta); process.stdout.write( `\nTool call: '${delta.toolName}' ${JSON.stringify(delta.input)}`, ); break; } case 'tool-result': { const transformedDelta: ToolResultPart = { ...delta, output: { type: 'json', value: delta.output as any }, }; toolResponses.push(transformedDelta); process.stdout.write( `\nTool response: '${delta.toolName}' ${JSON.stringify( delta.output, )}`, ); break; } } } process.stdout.write('\n\n'); console.log(); console.log('Warnings: ', await result.warnings); console.log('Sources:', await result.sources); console.log('Finish reason:', await result.finishReason); console.log('Usage:', await result.usage); const sources = await result.sources; for (const source of sources) { if (source.sourceType === 'url') { console.log('Source URL:', source.url); console.log('Title:', source.title); console.log(); } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock-anthropic-websearch.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { anthropicTools } from '@ai-sdk/anthropic/internal'; import { stepCountIs, streamText, ToolCallPart, ToolResultPart } from 'ai'; import 'dotenv/config'; // This will throw a warning as web_search is not supported on amazon bedrock async function main() { const result = streamText({ model: bedrock('us.anthropic.claude-sonnet-4-20250514-v1:0'), prompt: 'What are the latest news about climate change and renewable energy? Please provide current information and cite your sources.', tools: { web_search: anthropicTools.webSearch_20250305({ maxUses: 8, blockedDomains: ['pinterest.com', 'reddit.com/r/conspiracy'], userLocation: { type: 'approximate', city: 'New York', region: 'New York', country: 'US', timezone: 'America/New_York', }, }), }, stopWhen: stepCountIs(3), }); let fullResponse = ''; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const delta of result.fullStream) { switch (delta.type) { case 'text-delta': { fullResponse += delta.text; process.stdout.write(delta.text); break; } case 'tool-call': { toolCalls.push(delta); process.stdout.write( `\nTool call: '${delta.toolName}' ${JSON.stringify(delta.input)}`, ); break; } case 'tool-result': { const transformedDelta: ToolResultPart = { ...delta, output: { type: 'json', value: delta.output as any }, }; toolResponses.push(transformedDelta); process.stdout.write( `\nTool response: '${delta.toolName}' ${JSON.stringify( delta.output, )}`, ); break; } } } process.stdout.write('\n\n'); console.log(); console.log('Warnings:', await result.warnings); console.log('Sources:', await result.sources); console.log('Finish reason:', await result.finishReason); console.log('Usage:', await result.usage); const sources = await result.sources; for (const source of sources) { if (source.sourceType === 'url') { console.log('Source URL:', source.url); console.log('Title:', source.title); console.log(); } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock-cache-point-assistant.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); async function main() { const result = streamText({ model: bedrock('anthropic.claude-3-5-sonnet-20241022-v2:0'), messages: [ { role: 'assistant', content: [ { type: 'text', text: 'You are a JavaScript expert.', }, { type: 'text', text: `Error message: ${errorMessage}`, }, ], providerOptions: { bedrock: { cachePoint: { type: 'default' } } }, }, { role: 'user', content: [ { type: 'text', text: 'Explain the error message.', }, ], }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log(); console.log('Token usage:', await result.usage); console.log( 'Cache token usage:', (await result.providerMetadata)?.bedrock?.usage, ); console.log('Finish reason:', await result.finishReason); console.log('Response headers:', (await result.response).headers); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock-cache-point-image.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = streamText({ model: bedrock('anthropic.claude-3-5-sonnet-20241022-v2:0'), maxOutputTokens: 512, messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png') }, ], }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log(); console.log('Token usage:', await result.usage); console.log( 'Cache token usage:', (await result.providerMetadata)?.bedrock?.usage, ); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock-cache-point-system.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); async function main() { const result = streamText({ model: bedrock('anthropic.claude-3-5-sonnet-20241022-v2:0'), messages: [ { role: 'system', content: `You are a helpful assistant. You may be asked about ${errorMessage}.`, providerOptions: { bedrock: { cachePoint: { type: 'default' } }, }, }, { role: 'user', content: `Explain the error message`, }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log(); console.log('Token usage:', await result.usage); console.log( 'Cache token usage:', (await result.providerMetadata)?.bedrock?.usage, ); console.log('Finish reason:', await result.finishReason); console.log('Response headers:', (await result.response).headers); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock-cache-point-tool-call.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { streamText, tool, ModelMessage } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; const messages: ModelMessage[] = []; const weatherTool = tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), // location below is inferred to be a string: execute: async ({ location }) => ({ location, temperature: weatherData[location], }), }); const weatherData: Record<string, number> = { 'New York': 72.4, 'Los Angeles': 84.2, Chicago: 68.9, Houston: 89.7, Phoenix: 95.6, Philadelphia: 71.3, 'San Antonio': 88.4, 'San Diego': 76.8, Dallas: 86.5, 'San Jose': 75.2, Austin: 87.9, Jacksonville: 83.6, 'Fort Worth': 85.7, Columbus: 69.8, 'San Francisco': 68.4, Charlotte: 77.3, Indianapolis: 70.6, Seattle: 65.9, Denver: 71.8, 'Washington DC': 74.5, Boston: 69.7, 'El Paso': 91.2, Detroit: 67.8, Nashville: 78.4, Portland: 66.7, Memphis: 81.3, 'Oklahoma City': 82.9, 'Las Vegas': 93.4, Louisville: 75.6, Baltimore: 73.8, Milwaukee: 66.5, Albuquerque: 84.7, Tucson: 92.3, Fresno: 87.2, Sacramento: 82.5, Mesa: 94.8, 'Kansas City': 77.9, Atlanta: 80.6, Miami: 88.3, Raleigh: 76.4, Omaha: 73.5, 'Colorado Springs': 70.2, 'Long Beach': 79.8, 'Virginia Beach': 78.1, Oakland: 71.4, Minneapolis: 65.8, Tulsa: 81.7, Arlington: 85.3, Tampa: 86.9, 'New Orleans': 84.5, Wichita: 79.4, Cleveland: 68.7, Bakersfield: 88.6, Aurora: 72.3, Anaheim: 81.5, Honolulu: 84.9, 'Santa Ana': 80.7, Riverside: 89.2, 'Corpus Christi': 87.6, Lexington: 74.8, Henderson: 92.7, Stockton: 83.9, 'Saint Paul': 66.2, Cincinnati: 72.9, Pittsburgh: 70.4, Greensboro: 75.9, Anchorage: 52.3, Plano: 84.8, Lincoln: 74.2, Orlando: 85.7, Irvine: 78.9, Newark: 71.6, Toledo: 69.3, Durham: 77.1, 'Chula Vista': 77.4, 'Fort Wayne': 71.2, 'Jersey City': 72.7, 'St. Petersburg': 85.4, Laredo: 90.8, Madison: 67.3, Chandler: 93.6, Buffalo: 66.8, Lubbock: 83.2, Scottsdale: 94.1, Reno: 76.5, Glendale: 92.8, Gilbert: 93.9, 'Winston-Salem': 76.2, Irving: 85.1, Hialeah: 87.8, Garland: 84.6, Fremont: 73.9, Boise: 75.3, Richmond: 76.7, 'Baton Rouge': 83.7, Spokane: 67.4, 'Des Moines': 72.1, Tacoma: 66.3, 'San Bernardino': 88.1, Modesto: 84.3, Fontana: 87.4, 'Santa Clarita': 82.6, Birmingham: 81.9, }; async function main() { const result = streamText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), maxOutputTokens: 512, tools: { weather: weatherTool, }, toolChoice: 'required', prompt: 'What is the weather in San Francisco?', // TODO: need a way to set cachePoint on `tools`. providerOptions: { bedrock: { cachePoint: { type: 'default', }, }, }, }); let fullResponse = ''; for await (const delta of result.fullStream) { switch (delta.type) { case 'text-delta': { fullResponse += delta.text; process.stdout.write(delta.text); break; } case 'tool-call': { process.stdout.write( `\nTool call: '${delta.toolName}' ${JSON.stringify(delta.input)}`, ); break; } case 'tool-result': { process.stdout.write( `\nTool response: '${delta.toolName}' ${JSON.stringify( delta.output, )}`, ); break; } } } process.stdout.write('\n\n'); messages.push(...(await result.response).messages); console.log('Messages:', messages[0].content); console.log(JSON.stringify(result.providerMetadata, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock-cache-point-user.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); async function main() { const result = streamText({ model: bedrock('anthropic.claude-3-5-sonnet-20241022-v2:0'), messages: [ { role: 'user', content: [ { type: 'text', text: `I was dreaming last night and I dreamt of an error message: ${errorMessage}`, }, ], providerOptions: { bedrock: { cachePoint: { type: 'default' } } }, }, { role: 'user', content: [ { type: 'text', text: 'Explain the error message.', }, ], }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log(); console.log('Token usage:', await result.usage); console.log( 'Cache token usage:', (await result.providerMetadata)?.bedrock?.usage, ); console.log('Finish reason:', await result.finishReason); console.log('Response headers:', (await result.response).headers); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock-chatbot.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { stepCountIs, ModelMessage, streamText, tool } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { z } from 'zod/v4'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const result = streamText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), messages, }); process.stdout.write('\nAssistant: '); for await (const delta of result.textStream) { process.stdout.write(delta); } process.stdout.write('\n\n'); messages.push(...(await result.response).messages); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock-fullstream.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { stepCountIs, streamText, ToolCallPart, ToolResultPart } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = streamText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), tools: { weather: weatherTool, cityAttractions: { inputSchema: z.object({ city: z.string() }), }, }, prompt: 'What is the weather in San Francisco?', stopWhen: stepCountIs(5), }); let enteredReasoning = false; let enteredText = false; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const part of result.fullStream) { switch (part.type) { case 'reasoning-delta': { if (!enteredReasoning) { enteredReasoning = true; console.log('\nREASONING:\n'); } process.stdout.write(part.text); break; } case 'text-delta': { if (!enteredText) { enteredText = true; console.log('\nTEXT:\n'); } process.stdout.write(part.text); break; } case 'tool-call': { toolCalls.push(part); process.stdout.write( `\nTool call: '${part.toolName}' ${JSON.stringify(part.input)}`, ); break; } case 'tool-result': { if (part.dynamic) { continue; } const transformedPart: ToolResultPart = { ...part, output: { type: 'json', value: part.output }, }; toolResponses.push(transformedPart); process.stdout.write( `\nTool response: '${part.toolName}' ${JSON.stringify(part.output)}`, ); break; } } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock-image.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = streamText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), maxOutputTokens: 512, messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png') }, ], }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock-pdf.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = streamText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the pdf in detail.' }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', }, ], }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock-reasoning-chatbot.ts --- import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'; import { stepCountIs, ModelMessage, streamText, tool } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { z } from 'zod/v4'; const bedrock = createAmazonBedrock({ // example fetch wrapper that logs the input to the API call: fetch: async (url, options) => { console.log('URL', url); console.log('Headers', JSON.stringify(options!.headers, null, 2)); console.log( `Body ${JSON.stringify(JSON.parse(options!.body! as string), null, 2)}`, ); return await fetch(url, options); }, }); const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const result = streamText({ model: bedrock('us.anthropic.claude-3-7-sonnet-20250219-v1:0'), messages, tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), maxRetries: 0, providerOptions: { bedrock: { reasoningConfig: { type: 'enabled', budgetTokens: 2048 }, }, }, onError: error => { console.error(error); }, }); process.stdout.write('\nAssistant: '); for await (const part of result.fullStream) { if (part.type === 'reasoning-delta') { process.stdout.write('\x1b[34m' + part.text + '\x1b[0m'); } else if (part.type === 'text-delta') { process.stdout.write(part.text); } } process.stdout.write('\n\n'); messages.push(...(await result.response).messages); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock-reasoning-fullstream.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { stepCountIs, streamText, ToolCallPart, ToolResultPart } from 'ai'; import 'dotenv/config'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = streamText({ model: bedrock('us.anthropic.claude-3-7-sonnet-20250219-v1:0'), tools: { weather: weatherTool, }, prompt: 'What is the weather in San Francisco?', providerOptions: { bedrock: { reasoningConfig: { type: 'enabled', budgetTokens: 1024 }, }, }, stopWhen: stepCountIs(5), maxRetries: 5, }); let enteredReasoning = false; let enteredText = false; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const part of result.fullStream) { switch (part.type) { case 'reasoning-delta': { if (!enteredReasoning) { enteredReasoning = true; console.log('\nREASONING:\n'); } process.stdout.write(part.text); break; } case 'text-delta': { if (!enteredText) { enteredText = true; console.log('\nTEXT:\n'); } process.stdout.write(part.text); break; } case 'tool-call': { toolCalls.push(part); process.stdout.write( `\nTool call: '${part.toolName}' ${JSON.stringify(part.input)}`, ); break; } case 'tool-result': { if (part.dynamic) { continue; } const transformedPart: ToolResultPart = { ...part, output: { type: 'json', value: part.output }, }; toolResponses.push(transformedPart); process.stdout.write( `\nTool response: '${part.toolName}' ${JSON.stringify(part.output)}`, ); break; } } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock-reasoning.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { stepCountIs, streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: bedrock('us.anthropic.claude-3-7-sonnet-20250219-v1:0'), prompt: 'How many "r"s are in the word "strawberry"?', temperature: 0.5, // should get ignored (warning) onError: error => { console.error(error); }, providerOptions: { bedrock: { reasoningConfig: { type: 'enabled', budgetTokens: 1024 }, }, }, maxRetries: 0, stopWhen: stepCountIs(5), }); for await (const part of result.fullStream) { if (part.type === 'reasoning-delta') { process.stdout.write('\x1b[34m' + part.text + '\x1b[0m'); } else if (part.type === 'text-delta') { process.stdout.write(part.text); } } console.log(); console.log('Warnings:', await result.warnings); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock-tool-call.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { streamText, ModelMessage, ToolCallPart, ToolResultPart } from 'ai'; import 'dotenv/config'; import { weatherTool } from '../tools/weather-tool'; const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; const result = streamText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), maxOutputTokens: 512, tools: { weather: weatherTool, }, toolChoice: 'required', prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); let fullResponse = ''; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const delta of result.fullStream) { switch (delta.type) { case 'text-delta': { fullResponse += delta.text; process.stdout.write(delta.text); break; } case 'tool-call': { toolCalls.push(delta); process.stdout.write( `\nTool call: '${delta.toolName}' ${JSON.stringify(delta.input)}`, ); break; } case 'tool-result': { if (delta.dynamic) { continue; } // Transform to new format const transformedDelta: ToolResultPart = { ...delta, output: { type: 'json', value: delta.output }, }; toolResponses.push(transformedDelta); process.stdout.write( `\nTool response: '${delta.toolName}' ${JSON.stringify( delta.output, )}`, ); break; } } } process.stdout.write('\n\n'); messages.push({ role: 'assistant', content: [{ type: 'text', text: fullResponse }, ...toolCalls], }); if (toolResponses.length > 0) { messages.push({ role: 'tool', content: toolResponses }); } toolResponseAvailable = toolCalls.length > 0; console.log('Messages:', messages[0].content); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/amazon-bedrock.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), 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); console.log('Response headers:', (await result.response).headers); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/anthropic-cache-control.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); async function main() { const result = streamText({ model: anthropic('claude-3-5-sonnet-20240620'), messages: [ { role: 'user', content: [ { type: 'text', text: 'You are a JavaScript expert.', }, { type: 'text', text: `Error message: ${errorMessage}`, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, { type: 'text', text: 'Explain the error message.', }, ], }, ], onFinish({ providerMetadata }) { console.log(); console.log('=== onFinish ==='); console.log(providerMetadata?.anthropic); }, }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log('=== providerMetadata Promise ==='); console.log((await result.providerMetadata)?.anthropic); // e.g. { cacheCreationInputTokens: 2118, cacheReadInputTokens: 0 } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/anthropic-chatbot.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { stepCountIs, ModelMessage, streamText, tool } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { z } from 'zod/v4'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const result = streamText({ model: anthropic('claude-3-5-sonnet-latest'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), messages, }); process.stdout.write('\nAssistant: '); for await (const delta of result.textStream) { process.stdout.write(delta); } process.stdout.write('\n\n'); messages.push(...(await result.response).messages); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/anthropic-disable-parallel-tools.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { streamText, tool } from 'ai'; import { z } from 'zod'; import 'dotenv/config'; async function main() { const result = streamText({ model: anthropic('claude-3-5-sonnet-20241022'), prompt: 'What is the weather in Paris, France and London, UK?', tools: { getWeather: tool({ description: 'Get the current weather for a location', inputSchema: z.object({ location: z .string() .describe('The city and state, e.g. San Francisco, CA'), }), execute: async ({ location }: { location: string }) => { return `Weather in ${location}: 72°F, sunny`; }, }), }, providerOptions: { anthropic: { disableParallelToolUse: true, }, }, }); for await (const chunk of result.fullStream) { switch (chunk.type) { case 'text-delta': { process.stdout.write(chunk.text); break; } case 'tool-call': { console.log( `\nTOOL CALL: ${chunk.toolName}(${JSON.stringify(chunk.input)})`, ); break; } case 'tool-result': { console.log(`TOOL RESULT: ${JSON.stringify(chunk.output)}`); break; } } } console.log(); console.log('Token usage:', await result.usage); console.log('Finish reason:', await result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/anthropic-fullstream.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { streamText } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = streamText({ model: anthropic('claude-3-5-sonnet-20240620'), tools: { weather: weatherTool, cityAttractions: { inputSchema: z.object({ city: z.string() }), }, }, prompt: 'What is the weather in San Francisco?', }); for await (const part of result.fullStream) { switch (part.type) { case 'text-delta': { console.log('Text:', part.text); break; } case 'tool-call': { if (part.dynamic) { continue; } switch (part.toolName) { case 'cityAttractions': { console.log('TOOL CALL cityAttractions'); console.log(`city: ${part.input.city}`); // string break; } case 'weather': { console.log('TOOL CALL weather'); console.log(`location: ${part.input.location}`); // string break; } } break; } case 'tool-result': { if (part.dynamic) { continue; } switch (part.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // console.log('TOOL RESULT cityAttractions'); // console.log(`city: ${part.input.city}`); // string // console.log(`result: ${part.result}`); // break; // } case 'weather': { console.log('TOOL RESULT weather'); console.log(`location: ${part.input.location}`); // string console.log(`temperature: ${part.output.temperature}`); // number break; } } break; } case 'error': console.error('Error:', part.error); break; } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/anthropic-image.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = streamText({ model: anthropic('claude-3-5-sonnet-20240620'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png') }, ], }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/anthropic-on-chunk-raw.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { console.log('=== onChunk with raw chunks enabled ==='); let textChunkCount = 0; let rawChunkCount = 0; let otherChunkCount = 0; const result = streamText({ model: anthropic('claude-3-haiku-20240307'), prompt: 'Write a short poem about coding. Include reasoning about your creative process.', includeRawChunks: true, onChunk({ chunk }) { if (chunk.type === 'text-delta') { textChunkCount++; console.log('onChunk text:', chunk.text); } else if (chunk.type === 'raw') { rawChunkCount++; console.log('onChunk raw:', JSON.stringify(chunk.rawValue)); } else { otherChunkCount++; console.log('onChunk other:', chunk.type); } }, }); for await (const textPart of result.textStream) { } console.log(); console.log('Summary:'); console.log('- Text chunks received in onChunk:', textChunkCount); console.log('- Raw chunks received in onChunk:', rawChunkCount); console.log('- Other chunks received in onChunk:', otherChunkCount); console.log('- Final text:', await result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/anthropic-pdf-sources.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = streamText({ model: anthropic('claude-3-5-sonnet-20241022'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document? Please cite your sources.', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', providerOptions: { anthropic: { citations: { enabled: true }, title: 'AI Handbook', context: 'Technical documentation about AI models and embeddings', }, }, }, ], }, ], }); for await (const part of result.fullStream) { switch (part.type) { case 'text-delta': { process.stdout.write(part.text); break; } case 'source': { if (part.sourceType === 'document') { console.log(`\n\nDocument Source: ${part.title}`); console.log(`Media Type: ${part.mediaType}`); if (part.filename) { console.log(`Filename: ${part.filename}`); } } break; } case 'error': console.error('Error:', part.error); break; } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/anthropic-pdf.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = streamText({ model: anthropic('claude-3-5-sonnet-20241022'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', }, ], }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/anthropic-reasoning-chatbot.ts --- import { AnthropicProviderOptions, createAnthropic } from '@ai-sdk/anthropic'; import { stepCountIs, ModelMessage, streamText, tool } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { z } from 'zod/v4'; const anthropic = createAnthropic({ // example fetch wrapper that logs the input to the API call: fetch: async (url, options) => { console.log('URL', url); console.log('Headers', JSON.stringify(options!.headers, null, 2)); console.log( `Body ${JSON.stringify(JSON.parse(options!.body! as string), null, 2)}`, ); return await fetch(url, options); }, }); const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const result = streamText({ model: anthropic('claude-3-7-sonnet-20250219'), messages, tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), maxRetries: 0, providerOptions: { anthropic: { thinking: { type: 'enabled', budgetTokens: 12000 }, } satisfies AnthropicProviderOptions, }, onError: error => { console.error(error); }, }); process.stdout.write('\nAssistant: '); for await (const part of result.fullStream) { if (part.type === 'reasoning-delta') { process.stdout.write('\x1b[34m' + part.text + '\x1b[0m'); } else if (part.type === 'text-delta') { process.stdout.write(part.text); } } process.stdout.write('\n\n'); messages.push(...(await result.response).messages); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/anthropic-reasoning-fullstream.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { extractReasoningMiddleware, stepCountIs, streamText, ToolCallPart, ToolResultPart, wrapLanguageModel, } from 'ai'; import 'dotenv/config'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = streamText({ model: wrapLanguageModel({ model: anthropic('claude-3-opus-20240229'), middleware: [extractReasoningMiddleware({ tagName: 'thinking' })], }), providerOptions: { anthropic: { // Anthropic produces 'thinking' tags for this model and example // configuration. These will never include signature content and so // will fail the provider-side signature check if included in subsequent // request messages, so we disable sending reasoning content. sendReasoning: false, }, }, tools: { weather: weatherTool, }, prompt: 'What is the weather in San Francisco?', stopWhen: stepCountIs(5), }); let enteredReasoning = false; let enteredText = false; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const part of result.fullStream) { switch (part.type) { case 'reasoning-delta': { if (!enteredReasoning) { enteredReasoning = true; console.log('\nREASONING:\n'); } process.stdout.write(part.text); break; } case 'text-delta': { if (!enteredText) { enteredText = true; console.log('\nTEXT:\n'); } process.stdout.write(part.text); break; } case 'tool-call': { toolCalls.push(part); process.stdout.write( `\nTool call: '${part.toolName}' ${JSON.stringify(part.input)}`, ); break; } case 'tool-result': { if (part.dynamic) { continue; } const transformedPart: ToolResultPart = { ...part, output: { type: 'json', value: part.output }, }; toolResponses.push(transformedPart); process.stdout.write( `\nTool response: '${part.toolName}' ${JSON.stringify(part.output)}`, ); break; } } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/anthropic-reasoning.ts --- import { anthropic, AnthropicProviderOptions } from '@ai-sdk/anthropic'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: anthropic('claude-3-7-sonnet-20250219'), prompt: 'How many "r"s are in the word "strawberry"?', temperature: 0.5, // should get ignored (warning) onError: error => { console.error(error); }, providerOptions: { anthropic: { thinking: { type: 'enabled', budgetTokens: 12000 }, } satisfies AnthropicProviderOptions, }, maxRetries: 0, }); for await (const part of result.fullStream) { if (part.type === 'reasoning-delta') { process.stdout.write('\x1b[34m' + part.text + '\x1b[0m'); if (part.providerMetadata?.anthropic?.redactedData != null) { process.stdout.write('\x1b[31m' + '<redacted>' + '\x1b[0m'); } } else if (part.type === 'text-delta') { process.stdout.write(part.text); } } console.log(); console.log('Warnings:', await result.warnings); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/anthropic-search.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: anthropic('claude-3-5-sonnet-latest'), prompt: 'What are the latest news about climate change and renewable energy? Please provide current information and cite your sources.', tools: { web_search: anthropic.tools.webSearch_20250305({ maxUses: 8, blockedDomains: ['pinterest.com', 'reddit.com/r/conspiracy'], userLocation: { type: 'approximate', city: 'New York', region: 'New York', country: 'US', timezone: 'America/New_York', }, }), }, }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log(); console.log('Sources:', await result.sources); console.log('Finish reason:', await result.finishReason); console.log('Usage:', await result.usage); const sources = await result.sources; for (const source of sources) { if (source.sourceType === 'url') { console.log('Source URL:', source.url); console.log('Title:', source.title); console.log(); } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/anthropic-smooth.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { smoothStream, streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: anthropic('claude-3-5-sonnet-20240620'), prompt: 'Invent a new holiday and describe its traditions.', experimental_transform: smoothStream(), }); 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); --- File: /ai/examples/ai-core/src/stream-text/anthropic-text-citations.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: anthropic('claude-3-5-sonnet-20241022'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What color is the grass? Use citations.', }, { type: 'file', mediaType: 'text/plain', data: 'The grass is green in spring and summer. The sky is blue during clear weather.', providerOptions: { anthropic: { citations: { enabled: true }, title: 'Nature Facts', }, }, }, ], }, ], }); let citationCount = 0; for await (const part of result.fullStream) { switch (part.type) { case 'text-delta': process.stdout.write(part.text); break; case 'source': if ( part.sourceType === 'document' && part.providerMetadata?.anthropic ) { const meta = part.providerMetadata.anthropic; console.log( `\n\n[${++citationCount}] "${meta.citedText}" (chars: ${meta.startCharIndex}-${meta.endCharIndex})`, ); } break; } } console.log(`\n\nTotal citations: ${citationCount}`); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/anthropic-web-search.ts --- import 'dotenv/config'; import { anthropic } from '@ai-sdk/anthropic'; import { streamText } from 'ai'; async function main() { const result = streamText({ model: anthropic('claude-3-5-sonnet-latest'), prompt: 'What are current stock market trends? Search for latest financial news.', tools: { web_search: anthropic.tools.webSearch_20250305({ maxUses: 2, blockedDomains: ['reddit.com'], }), }, }); for await (const part of result.fullStream) { console.log(JSON.stringify(part)); } console.log(); console.log('Sources:', (await result.sources).length); console.log('Usage:', await result.usage); console.log(); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/anthropic.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: anthropic('claude-3-5-sonnet-20240620'), 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); --- File: /ai/examples/ai-core/src/stream-text/azure-completion.ts --- import { azure } from '@ai-sdk/azure'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: azure.completion('my-gpt-35-turbo-instruct-deployment'), // use your own deployment prompt: 'Invent a new holiday and describe its traditions.', }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/azure-fullstream-logprobs.ts --- import { azure } from '@ai-sdk/azure'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: azure('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', providerOptions: { openai: { logprobs: 2, }, }, }); for await (const part of result.fullStream) { switch (part.type) { case 'text-delta': { console.log('Text:', part.text); break; } case 'finish-step': { console.log(`finishReason: ${part.finishReason}`); console.log('Logprobs:', part.providerMetadata?.azure.logprobs); // object: { string, number, array} } } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/azure-fullstream.ts --- import { azure } from '@ai-sdk/azure'; import { streamText } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = streamText({ model: azure('v0-gpt-35-turbo'), // use your own deployment tools: { weather: weatherTool, cityAttractions: { inputSchema: z.object({ city: z.string() }), }, }, prompt: 'What is the weather in San Francisco?', }); for await (const part of result.fullStream) { switch (part.type) { case 'text-delta': { console.log('Text:', part.text); break; } case 'tool-call': { if (part.dynamic) { continue; } switch (part.toolName) { case 'cityAttractions': { console.log('TOOL CALL cityAttractions'); console.log(`city: ${part.input.city}`); // string break; } case 'weather': { console.log('TOOL CALL weather'); console.log(`location: ${part.input.location}`); // string break; } } break; } case 'tool-result': { if (part.dynamic) { continue; } switch (part.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // console.log('TOOL RESULT cityAttractions'); // console.log(`city: ${part.input.city}`); // string // console.log(`result: ${part.result}`); // break; // } case 'weather': { console.log('TOOL RESULT weather'); console.log(`location: ${part.input.location}`); // string console.log(`temperature: ${part.output.temperature}`); // number break; } } break; } case 'error': console.error('Error:', part.error); break; } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/azure-smooth-line.ts --- import { azure } from '@ai-sdk/azure'; import { smoothStream, streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: azure('gpt-4o'), // use your own deployment prompt: 'Invent a new holiday and describe its traditions.', experimental_transform: smoothStream({ chunking: 'line' }), }); 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); --- File: /ai/examples/ai-core/src/stream-text/azure-smooth.ts --- import { azure } from '@ai-sdk/azure'; import { smoothStream, streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: azure('gpt-4o'), // use your own deployment prompt: 'Invent a new holiday and describe its traditions.', experimental_transform: smoothStream(), }); 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); --- File: /ai/examples/ai-core/src/stream-text/azure.ts --- import { azure } from '@ai-sdk/azure'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: azure('gpt-4o'), // use your own deployment 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); --- File: /ai/examples/ai-core/src/stream-text/baseten.ts --- import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { streamText } from 'ai'; const BASETEN_MODEL_ID = '<model-id>'; // e.g. 5q3z8xcw const BASETEN_MODEL_URL = `https://model-${BASETEN_MODEL_ID}.api.baseten.co/environments/production/sync/v1`; const baseten = createOpenAICompatible({ name: 'baseten', baseURL: BASETEN_MODEL_URL, headers: { Authorization: `Bearer ${process.env.BASETEN_API_KEY ?? ''}`, }, }); async function main() { const result = streamText({ model: baseten('<model-name>'), // The name of the model you are serving in the baseten deployment prompt: 'Give me a poem about life', }); 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); --- File: /ai/examples/ai-core/src/stream-text/cerebras-tool-call.ts --- import { cerebras } from '@ai-sdk/cerebras'; import { streamText, ModelMessage, ToolCallPart, ToolResultPart } from 'ai'; import 'dotenv/config'; import { weatherTool } from '../tools/weather-tool'; const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; const result = streamText({ model: cerebras('llama3.1-8b'), maxOutputTokens: 512, tools: { weather: weatherTool, }, toolChoice: 'required', prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); let fullResponse = ''; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const delta of result.fullStream) { switch (delta.type) { case 'text-delta': { fullResponse += delta.text; process.stdout.write(delta.text); break; } case 'tool-call': { toolCalls.push(delta); process.stdout.write( `\nTool call: '${delta.toolName}' ${JSON.stringify(delta.input)}`, ); break; } case 'tool-result': { if (delta.dynamic) { continue; } const transformedDelta: ToolResultPart = { ...delta, output: { type: 'json', value: delta.output }, }; toolResponses.push(transformedDelta); process.stdout.write( `\nTool response: '${delta.toolName}' ${JSON.stringify( delta.output, )}`, ); break; } } } process.stdout.write('\n\n'); messages.push({ role: 'assistant', content: [{ type: 'text', text: fullResponse }, ...toolCalls], }); if (toolResponses.length > 0) { messages.push({ role: 'tool', content: toolResponses }); } toolResponseAvailable = toolCalls.length > 0; console.log('Messages:', messages[0].content); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/cerebras.ts --- import { cerebras } from '@ai-sdk/cerebras'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: cerebras('llama3.1-8b'), prompt: 'Invent a new holiday and describe its traditions.', }); console.log(result); 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); --- File: /ai/examples/ai-core/src/stream-text/cohere-chatbot.ts --- import { cohere } from '@ai-sdk/cohere'; import { stepCountIs, ModelMessage, streamText, tool } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { z } from 'zod/v4'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { messages.push({ role: 'user', content: await terminal.question('You: ') }); const result = streamText({ model: cohere('command-a-03-2025'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), messages, }); process.stdout.write('\nAssistant: '); for await (const delta of result.textStream) { process.stdout.write(delta); } process.stdout.write('\n\n'); messages.push(...(await result.response).messages); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/cohere-raw-chunks.ts --- import { cohere } from '@ai-sdk/cohere'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: cohere('command-r-plus'), prompt: 'Count from 1 to 3 slowly.', includeRawChunks: true, }); let textChunkCount = 0; let rawChunkCount = 0; for await (const chunk of result.fullStream) { if (chunk.type === 'text-delta') { textChunkCount++; console.log('Text chunk', textChunkCount, ':', chunk.text); } else if (chunk.type === 'raw') { rawChunkCount++; console.log( 'Raw chunk', rawChunkCount, ':', JSON.stringify(chunk.rawValue), ); } } console.log(); console.log('Text chunks:', textChunkCount); console.log('Raw chunks:', rawChunkCount); console.log('Final text:', await result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/cohere-response.ts --- import 'dotenv/config'; import { cohere } from '@ai-sdk/cohere'; import { streamText } from 'ai'; async function main() { const result = streamText({ model: cohere('command-r-plus'), maxOutputTokens: 512, prompt: 'Invent a new holiday and describe its traditions.', }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log(); console.log(JSON.stringify(await result.response, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/cohere-tool-call-empty-params.ts --- import { cohere } from '@ai-sdk/cohere'; import { streamText, ModelMessage, ToolCallPart, ToolResultPart, tool, } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; const result = streamText({ model: cohere('command-r-plus'), maxOutputTokens: 512, tools: { currentTime: tool({ description: 'Get the current time', inputSchema: z.object({}), execute: async () => ({ currentTime: new Date().toLocaleTimeString(), }), }), }, prompt: 'What is the current time?', }); let fullResponse = ''; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const delta of result.fullStream) { console.log(delta); switch (delta.type) { case 'text-delta': { fullResponse += delta.text; process.stdout.write(delta.text); break; } case 'tool-call': { toolCalls.push(delta); process.stdout.write( `\nTool call: '${delta.toolName}' ${JSON.stringify(delta.input)}`, ); break; } case 'tool-result': { if (delta.dynamic) { continue; } const transformedDelta: ToolResultPart = { ...delta, output: { type: 'json', value: delta.output }, }; toolResponses.push(transformedDelta); process.stdout.write( `\nTool response: '${delta.toolName}' ${JSON.stringify( delta.output, )}`, ); break; } } } process.stdout.write('\n\n'); messages.push({ role: 'assistant', content: [{ type: 'text', text: fullResponse }, ...toolCalls], }); if (toolResponses.length > 0) { messages.push({ role: 'tool', content: toolResponses }); } toolResponseAvailable = toolCalls.length > 0; } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/cohere-tool-call.ts --- import { cohere } from '@ai-sdk/cohere'; import { streamText, ModelMessage, ToolCallPart, ToolResultPart } from 'ai'; import 'dotenv/config'; import { weatherTool } from '../tools/weather-tool'; const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; const result = streamText({ model: cohere('command-r-plus'), maxOutputTokens: 512, tools: { weather: weatherTool, }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); let fullResponse = ''; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const delta of result.fullStream) { console.log(delta); switch (delta.type) { case 'text-delta': { fullResponse += delta.text; process.stdout.write(delta.text); break; } case 'tool-call': { toolCalls.push(delta); process.stdout.write( `\nTool call: '${delta.toolName}' ${JSON.stringify(delta.input)}`, ); break; } case 'tool-result': { if (delta.dynamic) { continue; } // Transform to new format const transformedDelta: ToolResultPart = { ...delta, output: { type: 'json', value: delta.output }, }; toolResponses.push(transformedDelta); process.stdout.write( `\nTool response: '${delta.toolName}' ${JSON.stringify( delta.output, )}`, ); break; } } } process.stdout.write('\n\n'); messages.push({ role: 'assistant', content: [{ type: 'text', text: fullResponse }, ...toolCalls], }); if (toolResponses.length > 0) { messages.push({ role: 'tool', content: toolResponses }); } toolResponseAvailable = toolCalls.length > 0; } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/cohere.ts --- import { cohere } from '@ai-sdk/cohere'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: cohere('command-r-plus'), 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); --- File: /ai/examples/ai-core/src/stream-text/deepseek-cache-token.ts --- import { deepseek } from '@ai-sdk/deepseek'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); async function main() { const result = streamText({ model: deepseek('deepseek-chat'), messages: [ { role: 'user', content: [ { type: 'text', text: 'You are a JavaScript expert.', }, { type: 'text', text: `Error message: ${errorMessage}`, }, { type: 'text', text: 'Explain the error message.', }, ], }, ], }); 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); console.log('Provider metadata:', await result.providerMetadata); // "prompt_cache_hit_tokens":1856,"prompt_cache_miss_tokens":5} } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/deepseek-reasoning.ts --- import { deepseek } from '@ai-sdk/deepseek'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: deepseek('deepseek-reasoner'), prompt: 'How many "r"s are in the word "strawberry"?', }); for await (const part of result.fullStream) { if (part.type === 'reasoning-delta') { process.stdout.write('\x1b[34m' + part.text + '\x1b[0m'); } else if (part.type === 'text-delta') { process.stdout.write(part.text); } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/deepseek-tool-call.ts --- import { deepseek } from '@ai-sdk/deepseek'; import { streamText, ModelMessage, ToolCallPart, ToolResultPart } from 'ai'; import 'dotenv/config'; import { weatherTool } from '../tools/weather-tool'; const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; const result = streamText({ model: deepseek('deepseek-chat'), maxOutputTokens: 512, tools: { weather: weatherTool, }, toolChoice: 'required', prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); let fullResponse = ''; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const delta of result.fullStream) { switch (delta.type) { case 'text-delta': { fullResponse += delta.text; process.stdout.write(delta.text); break; } case 'tool-call': { toolCalls.push(delta); process.stdout.write( `\nTool call: '${delta.toolName}' ${JSON.stringify(delta.input)}`, ); break; } case 'tool-result': { if (delta.dynamic) { continue; } const transformedDelta: ToolResultPart = { ...delta, output: { type: 'json', value: delta.output }, }; toolResponses.push(transformedDelta); process.stdout.write( `\nTool response: '${delta.toolName}' ${JSON.stringify( delta.output, )}`, ); break; } } } process.stdout.write('\n\n'); messages.push({ role: 'assistant', content: [{ type: 'text', text: fullResponse }, ...toolCalls], }); if (toolResponses.length > 0) { messages.push({ role: 'tool', content: toolResponses }); } toolResponseAvailable = toolCalls.length > 0; console.log('Messages:', messages[0].content); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/deepseek.ts --- import { deepseek } from '@ai-sdk/deepseek'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: deepseek('deepseek-chat'), 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); --- File: /ai/examples/ai-core/src/stream-text/fireworks-deepseek.ts --- import { fireworks } from '@ai-sdk/fireworks'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: fireworks('accounts/fireworks/models/deepseek-v3'), 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); --- File: /ai/examples/ai-core/src/stream-text/fireworks-kimi-k2-tool-call.ts --- import { fireworks } from '@ai-sdk/fireworks'; import { streamText, ModelMessage, ToolCallPart, ToolResultPart } from 'ai'; import 'dotenv/config'; import { weatherTool } from '../tools/weather-tool'; const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; const result = streamText({ model: fireworks('accounts/fireworks/models/kimi-k2-instruct'), maxOutputTokens: 512, tools: { weather: weatherTool, }, toolChoice: 'auto', prompt: 'What is the weather in San Francisco?', }); let fullResponse = ''; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const delta of result.fullStream) { switch (delta.type) { case 'text-delta': { fullResponse += delta.text; process.stdout.write(delta.text); break; } case 'tool-call': { toolCalls.push(delta); process.stdout.write( `\nTool call: '${delta.toolName}' ${JSON.stringify(delta.input)}`, ); break; } case 'tool-result': { if (delta.dynamic) { continue; } const transformedDelta: ToolResultPart = { ...delta, output: { type: 'json', value: delta.output }, }; toolResponses.push(transformedDelta); process.stdout.write( `\nTool response: '${delta.toolName}' ${JSON.stringify(delta.output)}`, ); toolResponseAvailable = true; break; } case 'error': { console.log(`\nError: ${delta.error}`); break; } } } messages.push({ role: 'assistant', content: [{ type: 'text', text: fullResponse }, ...toolCalls], }); if (toolResponses.length > 0) { messages.push({ role: 'tool', content: toolResponses }); } toolResponseAvailable = toolCalls.length > 0; console.log('Messages:', messages[0].content); console.log(); console.log('Token usage:', await result.usage); console.log('Finish reason:', await result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/fireworks-kimi-k2.ts --- import { fireworks } from '@ai-sdk/fireworks'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: fireworks('accounts/fireworks/models/kimi-k2-instruct'), 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); --- File: /ai/examples/ai-core/src/stream-text/fireworks-reasoning.ts --- import { fireworks } from '@ai-sdk/fireworks'; import { extractReasoningMiddleware, streamText, wrapLanguageModel } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: wrapLanguageModel({ model: fireworks('accounts/fireworks/models/qwq-32b'), middleware: extractReasoningMiddleware({ tagName: 'think', startWithReasoning: true, }), }), prompt: 'How many "r"s are in the word "strawberry"?', }); let enteredReasoning = false; let enteredText = false; for await (const part of result.fullStream) { if (part.type === 'reasoning-delta') { if (!enteredReasoning) { enteredReasoning = true; console.log('\nREASONING:\n'); } process.stdout.write(part.text); } else if (part.type === 'text-delta') { if (!enteredText) { enteredText = true; console.log('\nTEXT:\n'); } process.stdout.write(part.text); } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/fireworks.ts --- import { fireworks } from '@ai-sdk/fireworks'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: fireworks('accounts/fireworks/models/firefunction-v1'), 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); --- File: /ai/examples/ai-core/src/stream-text/gateway-auth.ts --- import { streamText } from 'ai'; import { gateway } from '@ai-sdk/gateway'; import 'dotenv/config'; // An integration test for Vercel AI Gateway provider authentication. There are // two authentication methods: OIDC and API key. Proper testing requires that // the developer has set a valid OIDC token and API key in the // `examples/ai-core/.env` file (or otherwise in the environment somehow). const VALID_OIDC_TOKEN = (() => { const token = process.env.VERCEL_OIDC_TOKEN; if (!token) { throw new Error('VERCEL_OIDC_TOKEN environment variable is required'); } return token; })(); const VALID_API_KEY = (() => { const key = process.env.AI_GATEWAY_API_KEY; if (!key) { throw new Error('AI_GATEWAY_API_KEY environment variable is required'); } return key; })(); const INVALID_OIDC_TOKEN = 'invalid-oidc-token'; const INVALID_API_KEY = 'invalid-api-key'; const testScenarios = [ { name: 'no auth at all', setupEnv: () => { delete process.env.VERCEL_OIDC_TOKEN; delete process.env.AI_GATEWAY_API_KEY; }, expectSuccess: false, }, { name: 'valid oidc, invalid api key', setupEnv: () => { process.env.VERCEL_OIDC_TOKEN = VALID_OIDC_TOKEN; process.env.AI_GATEWAY_API_KEY = INVALID_API_KEY; }, expectSuccess: false, }, { name: 'invalid oidc, valid api key', setupEnv: () => { process.env.VERCEL_OIDC_TOKEN = INVALID_OIDC_TOKEN; process.env.AI_GATEWAY_API_KEY = VALID_API_KEY; }, expectSuccess: true, expectedAuthMethod: 'api-key', }, { name: 'no oidc, invalid api key', setupEnv: () => { delete process.env.VERCEL_OIDC_TOKEN; process.env.AI_GATEWAY_API_KEY = INVALID_API_KEY; }, expectSuccess: false, }, { name: 'no oidc, valid api key', setupEnv: () => { delete process.env.VERCEL_OIDC_TOKEN; process.env.AI_GATEWAY_API_KEY = VALID_API_KEY; }, expectSuccess: true, expectedAuthMethod: 'api-key', }, { name: 'valid oidc, valid api key', setupEnv: () => { process.env.VERCEL_OIDC_TOKEN = VALID_OIDC_TOKEN; process.env.AI_GATEWAY_API_KEY = VALID_API_KEY; }, expectSuccess: true, expectedAuthMethod: 'api-key', }, { name: 'valid oidc, no api key', setupEnv: () => { process.env.VERCEL_OIDC_TOKEN = VALID_OIDC_TOKEN; delete process.env.AI_GATEWAY_API_KEY; }, expectSuccess: true, expectedAuthMethod: 'oidc', }, { name: 'invalid oidc, no api key', setupEnv: () => { process.env.VERCEL_OIDC_TOKEN = INVALID_OIDC_TOKEN; delete process.env.AI_GATEWAY_API_KEY; }, expectSuccess: false, }, { name: 'invalid oidc, invalid api key', setupEnv: () => { process.env.VERCEL_OIDC_TOKEN = INVALID_OIDC_TOKEN; process.env.AI_GATEWAY_API_KEY = INVALID_API_KEY; }, expectSuccess: false, }, ]; async function testAuthenticationScenario(scenario: (typeof testScenarios)[0]) { scenario.setupEnv(); console.log(`Testing: ${scenario.name}`); const abortController = new AbortController(); const timeout = setTimeout(() => { abortController.abort(new Error('timeout')); }, 10000); try { const result = await testStream(abortController.signal); console.log(` Result: SUCCESS`); return { success: true, detectedAuthMethod: result.detectedAuthMethod }; } catch (error) { console.log(` Result: FAILURE`); return { success: false, error }; } finally { clearTimeout(timeout); } } async function testStream(abortSignal?: AbortSignal) { return new Promise<{ detectedAuthMethod: string }>((resolve, reject) => { const result = streamText({ model: gateway('openai/gpt-4'), prompt: 'Respond with "OK"', onError: reject, abortSignal, }); (async () => { try { let text = ''; for await (const chunk of result.textStream) { text += chunk; } const hasApiKey = !!process.env.AI_GATEWAY_API_KEY; const detectedAuthMethod = hasApiKey ? 'api-key' : 'oidc'; resolve({ detectedAuthMethod }); } catch (error) { reject(error); } })(); }); } async function runAllScenarios() { console.log('AI Gateway Authentication Tests\n'); const results = []; for (const scenario of testScenarios) { const result = await testAuthenticationScenario(scenario); const match = result.success === scenario.expectSuccess; results.push({ scenario: scenario.name, expected: scenario.expectSuccess, actual: result.success, match, detectedAuthMethod: result.detectedAuthMethod, }); } console.log('\nSUMMARY:'); const passed = results.filter(r => r.match).length; console.log(`${passed}/${results.length} tests passed`); const failed = results.filter(r => !r.match); if (failed.length > 0) { console.log('\nFAILED:'); failed.forEach(r => { console.log( ` ${r.scenario}: expected ${r.expected ? 'SUCCESS' : 'FAILURE'}, got ${r.actual ? 'SUCCESS' : 'FAILURE'}`, ); }); } } async function main() { const scenarioArg = process.argv[2]; if (!scenarioArg || scenarioArg === 'all') { await runAllScenarios(); } else { const scenario = testScenarios.find(s => s.name === scenarioArg); if (!scenario) { console.log('Available scenarios:'); testScenarios.forEach(s => console.log(` ${s.name}`)); return; } await testAuthenticationScenario(scenario); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/gateway-pdf.ts --- import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = streamText({ model: 'google/gemini-2.0-flash', messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', }, ], }, ], }); 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); --- File: /ai/examples/ai-core/src/stream-text/gateway-provider-options-order.ts --- import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: 'anthropic/claude-4-sonnet', prompt: 'Invent a new holiday and describe its traditions.', providerOptions: { gateway: { order: ['bedrock', 'anthropic'], }, }, }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log(); console.log('Provider metadata:', await result.providerMetadata); console.log('Token usage:', await result.usage); console.log('Finish reason:', await result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/gateway.ts --- import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: 'openai/gpt-4.1', 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); --- File: /ai/examples/ai-core/src/stream-text/google-caching.ts --- import 'dotenv/config'; import { google } from '@ai-sdk/google'; import { streamText } from 'ai'; import fs from 'node:fs'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); async function main() { const result1 = streamText({ model: google('gemini-2.5-flash'), prompt: errorMessage, }); await result1.consumeStream(); const providerMetadata1 = await result1.providerMetadata; console.log(providerMetadata1?.google); // e.g. // { // groundingMetadata: null, // safetyRatings: null, // usageMetadata: { // thoughtsTokenCount: 1336, // promptTokenCount: 2152, // candidatesTokenCount: 992, // totalTokenCount: 4480 // } // } const result2 = streamText({ model: google('gemini-2.5-flash'), prompt: errorMessage, }); await result2.consumeStream(); const providerMetadata2 = await result2.providerMetadata; console.log(providerMetadata2?.google); // e.g. // { // groundingMetadata: null, // safetyRatings: null, // usageMetadata: { // cachedContentTokenCount: 2027, // thoughtsTokenCount: 908, // promptTokenCount: 2152, // candidatesTokenCount: 667, // totalTokenCount: 3727 // } // } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-chatbot-image-output.ts --- import { google } from '@ai-sdk/google'; import { ModelMessage, streamText } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { presentImages } from '../lib/present-image'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { messages.push({ role: 'user', content: await terminal.question('You: ') }); const result = streamText({ model: google('gemini-2.0-flash-exp'), providerOptions: { google: { responseModalities: ['TEXT', 'IMAGE'] }, }, messages, }); process.stdout.write('\nAssistant: '); for await (const delta of result.fullStream) { switch (delta.type) { case 'text-delta': { process.stdout.write(delta.text); break; } case 'file': { if (delta.file.mediaType.startsWith('image/')) { console.log(delta.file); await presentImages([delta.file]); } } } } process.stdout.write('\n\n'); messages.push(...(await result.response).messages); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-chatbot.ts --- import { google } from '@ai-sdk/google'; import { stepCountIs, ModelMessage, streamText, tool } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { z } from 'zod/v4'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { messages.push({ role: 'user', content: await terminal.question('You: ') }); const result = streamText({ model: google('gemini-2.0-pro-exp-02-05'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), messages, }); process.stdout.write('\nAssistant: '); for await (const delta of result.textStream) { process.stdout.write(delta); } process.stdout.write('\n\n'); messages.push(...(await result.response).messages); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-fullstream.ts --- import { google } from '@ai-sdk/google'; import { streamText } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = streamText({ model: google('gemini-1.5-pro-latest'), tools: { weather: weatherTool, cityAttractions: { inputSchema: z.object({ city: z.string() }), }, }, prompt: 'What is the weather in San Francisco?', }); for await (const part of result.fullStream) { switch (part.type) { case 'text-delta': { console.log('Text:', part.text); break; } case 'tool-call': { if (part.dynamic) { continue; } switch (part.toolName) { case 'cityAttractions': { console.log('TOOL CALL cityAttractions'); console.log(`city: ${part.input.city}`); // string break; } case 'weather': { console.log('TOOL CALL weather'); console.log(`location: ${part.input.location}`); // string break; } } break; } case 'tool-result': { if (part.dynamic) { continue; } switch (part.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // console.log('TOOL RESULT cityAttractions'); // console.log(`city: ${part.input.city}`); // string // console.log(`result: ${part.result}`); // break; // } case 'weather': { console.log('TOOL RESULT weather'); console.log(`location: ${part.input.location}`); // string console.log(`temperature: ${part.output.temperature}`); // number break; } } break; } case 'finish': { console.log('Finish reason:', part.finishReason); console.log('Total Usage:', part.totalUsage); break; } case 'error': console.error('Error:', part.error); break; } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-gemma-system-instructions.ts --- import { google } from '@ai-sdk/google'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: google('gemma-3-12b-it'), system: 'You are a helpful pirate assistant. Always respond like a friendly pirate, using "Arrr" and pirate terminology.', prompt: 'Tell me a short story about finding treasure.', }); 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); --- File: /ai/examples/ai-core/src/stream-text/google-grounding.ts --- import { google } from '@ai-sdk/google'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: google('gemini-2.5-flash'), tools: { google_search: google.tools.googleSearch({}), }, prompt: 'List the top 5 San Francisco news from the past week.', }); for await (const part of result.fullStream) { if (part.type === 'text-delta') { process.stdout.write(part.text); } if (part.type === 'source' && part.sourceType === 'url') { console.log('\x1b[36m%s\x1b[0m', 'Source'); console.log('ID:', part.id); console.log('Title:', part.title); console.log('URL:', part.url); console.log(); } } console.log(); console.log('Token usage:', await result.usage); console.log('Finish reason:', await result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-image-output.ts --- import { google } from '@ai-sdk/google'; import { streamText } from 'ai'; import 'dotenv/config'; import { presentImages } from '../lib/present-image'; async function main() { const result = streamText({ model: google('gemini-2.0-flash-exp'), prompt: 'Generate an image of a comic cat', providerOptions: { google: { responseModalities: ['TEXT', 'IMAGE'] }, }, }); for await (const part of result.fullStream) { switch (part.type) { case 'text-delta': { process.stdout.write(part.text); break; } case 'file': { if (part.file.mediaType.startsWith('image/')) { await presentImages([part.file]); } break; } } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-raw-chunks.ts --- import { google } from '@ai-sdk/google'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: google('gemini-2.0-flash'), prompt: 'Count from 1 to 3 slowly.', includeRawChunks: true, }); let textChunkCount = 0; let rawChunkCount = 0; for await (const chunk of result.fullStream) { if (chunk.type === 'text-delta') { textChunkCount++; console.log('Text chunk', textChunkCount, ':', chunk.text); } else if (chunk.type === 'raw') { rawChunkCount++; console.log( 'Raw chunk', rawChunkCount, ':', JSON.stringify(chunk.rawValue), ); } } console.log(); console.log('Text chunks:', textChunkCount); console.log('Raw chunks:', rawChunkCount); console.log('Final text:', await result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-reasoning-with-tools.ts --- import { google } from '@ai-sdk/google'; import { streamText } from 'ai'; import 'dotenv/config'; import { z } from 'zod'; async function main() { const result = streamText({ model: google('gemini-2.0-flash-thinking-exp'), prompt: 'Calculate the sum of 2+2 using the calculate function.', tools: { calculate: { description: 'Calculate the result of a mathematical expression', parameters: z.object({ a: z.number().describe('First number'), b: z.number().describe('Second number'), operation: z .enum(['add', 'subtract', 'multiply', 'divide']) .describe('Mathematical operation'), }), }, }, toolChoice: 'required', providerOptions: { google: { thinkingConfig: { thinkingBudget: -1, includeThoughts: true, }, }, }, }); for await (const chunk of result.fullStream) { switch (chunk.type) { case 'text-delta': process.stdout.write(chunk.text); break; case 'reasoning-delta': if (chunk.providerMetadata?.google?.thoughtSignature) { console.log( '[Reasoning with signature]:', chunk.providerMetadata.google.thoughtSignature, ); } break; case 'tool-call': console.log('\nTool call:', chunk.toolName, chunk.input); if (chunk.providerMetadata?.google?.thoughtSignature) { console.log( '[Tool signature]:', chunk.providerMetadata.google.thoughtSignature, ); } break; } } console.log(); console.log('Token usage:', await result.usage); console.log('Finish reason:', await result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-reasoning.ts --- import { google, GoogleGenerativeAIProviderOptions } from '@ai-sdk/google'; import { stepCountIs, streamText } from 'ai'; import 'dotenv/config'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = streamText({ model: google('gemini-2.5-flash-preview-05-20'), tools: { weather: weatherTool }, prompt: 'What is the weather in San Francisco?', stopWhen: stepCountIs(2), providerOptions: { google: { thinkingConfig: { thinkingBudget: 1024, }, } satisfies GoogleGenerativeAIProviderOptions, }, onError: console.error, }); for await (const part of result.fullStream) { if (part.type === 'reasoning-delta') { process.stdout.write('\x1b[34m' + part.text + '\x1b[0m'); } else if (part.type === 'text-delta') { process.stdout.write(part.text); } } console.log(); console.log('Warnings:', await result.warnings); console.log(); console.log('Token usage:', await result.usage); console.log('Finish reason:', await result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-url-context.ts --- import { google } from '@ai-sdk/google'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: google('gemini-2.5-flash'), prompt: `Based on the document: https://ai.google.dev/gemini-api/docs/url-context#limitations. Answer this question: How many links we can consume in one request?`, tools: { url_context: google.tools.urlContext({}), }, }); for await (const part of result.fullStream) { if (part.type === 'text-delta') { process.stdout.write(part.text); } if (part.type === 'source' && part.sourceType === 'url') { console.log('\x1b[36m%s\x1b[0m', 'Source'); console.log('ID:', part.id); console.log('Title:', part.title); console.log('URL:', part.url); console.log(); } } console.log(); console.log('Token usage:', await result.usage); console.log('Finish reason:', await result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-vertex-anthropic-cache-control.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { streamText } from 'ai'; import fs from 'node:fs'; const errorMessage = fs.readFileSync('data/error-message.txt', 'utf8'); async function main() { const result = streamText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), messages: [ { role: 'user', content: [ { type: 'text', text: 'You are a JavaScript expert.', }, { type: 'text', text: `Error message: ${errorMessage}`, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, { type: 'text', text: 'Explain the error message.', }, ], }, ], onFinish({ providerMetadata }) { console.log(); console.log('=== onFinish ==='); console.log(providerMetadata?.anthropic); }, }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log('=== providerMetadata Promise ==='); console.log((await result.providerMetadata)?.anthropic); // e.g. { cacheCreationInputTokens: 2118, cacheReadInputTokens: 0 } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-vertex-anthropic-chatbot.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { stepCountIs, ModelMessage, streamText, tool } from 'ai'; import * as readline from 'node:readline/promises'; import { z } from 'zod/v4'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const result = streamText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), messages, }); process.stdout.write('\nAssistant: '); for await (const delta of result.textStream) { process.stdout.write(delta); } process.stdout.write('\n\n'); messages.push(...(await result.response).messages); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-vertex-anthropic-fullstream.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { streamText } from 'ai'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = streamText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), tools: { weather: weatherTool, cityAttractions: { inputSchema: z.object({ city: z.string() }), }, }, prompt: 'What is the weather in San Francisco?', }); for await (const part of result.fullStream) { switch (part.type) { case 'text-delta': { console.log('Text:', part.text); break; } case 'tool-call': { if (part.dynamic) { continue; } switch (part.toolName) { case 'cityAttractions': { console.log('TOOL CALL cityAttractions'); console.log(`city: ${part.input.city}`); // string break; } case 'weather': { console.log('TOOL CALL weather'); console.log(`location: ${part.input.location}`); // string break; } } break; } case 'tool-result': { if (part.dynamic) { continue; } switch (part.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // console.log('TOOL RESULT cityAttractions'); // console.log(`city: ${part.input.city}`); // string // console.log(`result: ${part.result}`); // break; // } case 'weather': { console.log('TOOL RESULT weather'); console.log(`location: ${part.input.location}`); // string console.log(`temperature: ${part.output.temperature}`); // number break; } } break; } case 'error': console.error('Error:', part.error); break; } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-vertex-anthropic-image-url.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { streamText } from 'ai'; import fs from 'node:fs'; async function main() { const result = streamText({ model: vertexAnthropic('claude-3-7-sonnet@20250219'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/comic-cat.png?raw=true', }, ], }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-vertex-anthropic-image.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { streamText } from 'ai'; import fs from 'node:fs'; async function main() { const result = streamText({ model: vertexAnthropic('claude-3-7-sonnet@20250219'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png') }, ], }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-vertex-anthropic-pdf.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { streamText } from 'ai'; import fs from 'node:fs'; async function main() { const result = streamText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: fs.readFileSync('./data/ai.pdf'), mediaType: 'application/pdf', }, ], }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-vertex-anthropic-tool-call.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { streamText, ModelMessage, ToolCallPart, ToolResultPart } from 'ai'; import { weatherTool } from '../tools/weather-tool'; const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; const result = streamText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), maxOutputTokens: 512, tools: { weather: weatherTool, }, toolChoice: 'required', prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); let fullResponse = ''; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const delta of result.fullStream) { switch (delta.type) { case 'text-delta': { fullResponse += delta.text; process.stdout.write(delta.text); break; } case 'tool-call': { toolCalls.push(delta); process.stdout.write( `\nTool call: '${delta.toolName}' ${JSON.stringify(delta.input)}`, ); break; } case 'tool-result': { if (delta.dynamic) { continue; } const transformedDelta: ToolResultPart = { ...delta, output: { type: 'json', value: delta.output }, }; toolResponses.push(transformedDelta); process.stdout.write( `\nTool response: '${delta.toolName}' ${JSON.stringify( delta.output, )}`, ); break; } } } process.stdout.write('\n\n'); messages.push({ role: 'assistant', content: [{ type: 'text', text: fullResponse }, ...toolCalls], }); if (toolResponses.length > 0) { messages.push({ role: 'tool', content: toolResponses }); } toolResponseAvailable = toolCalls.length > 0; console.log('Messages:', messages[0].content); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-vertex-anthropic.ts --- import 'dotenv/config'; import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { streamText } from 'ai'; async function main() { const result = streamText({ model: vertexAnthropic('claude-3-5-sonnet-v2@20241022'), 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); --- File: /ai/examples/ai-core/src/stream-text/google-vertex-code-execution.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { googleTools } from '@ai-sdk/google/internal'; import { ModelMessage, streamText, ToolCallPart, ToolResultPart } from 'ai'; import 'dotenv/config'; import * as process from 'process'; const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; const result = streamText({ model: vertex('gemini-2.5-pro'), tools: { code_execution: googleTools.codeExecution({}) }, maxOutputTokens: 10000, prompt: 'Calculate 20th fibonacci number. Then find the nearest palindrome to it.', }); let fullResponse = ''; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const delta of result.fullStream) { switch (delta.type) { case 'text-delta': { fullResponse += delta.text; process.stdout.write(delta.text); break; } case 'tool-call': { toolCalls.push(delta); process.stdout.write( `\nTool call: '${delta.toolName}' ${JSON.stringify(delta.input)}`, ); break; } case 'tool-result': { const transformedDelta: ToolResultPart = { ...delta, output: { type: 'json', value: delta.output as any }, }; toolResponses.push(transformedDelta); process.stdout.write( `\nTool response: '${delta.toolName}' ${JSON.stringify( delta.output, )}`, ); break; } } } process.stdout.write('\n\n'); messages.push({ role: 'assistant', content: [{ type: 'text', text: fullResponse }, ...toolCalls], }); if (toolResponses.length > 0) { messages.push({ role: 'tool', content: toolResponses }); } toolResponseAvailable = toolCalls.length > 0; console.log('Messages:', messages[0].content); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-vertex-fullstream.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { streamText } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = streamText({ model: vertex('gemini-1.5-pro'), tools: { weather: weatherTool, cityAttractions: { inputSchema: z.object({ city: z.string() }), }, }, prompt: 'What is the weather in San Francisco?', }); for await (const part of result.fullStream) { switch (part.type) { case 'text-delta': { console.log('Text:', part.text); break; } case 'tool-call': { if (part.dynamic) { continue; } switch (part.toolName) { case 'cityAttractions': { console.log('TOOL CALL cityAttractions'); console.log(`city: ${part.input.city}`); // string break; } case 'weather': { console.log('TOOL CALL weather'); console.log(`location: ${part.input.location}`); // string break; } } break; } case 'tool-result': { if (part.dynamic) { continue; } switch (part.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // console.log('TOOL RESULT cityAttractions'); // console.log(`city: ${part.input.city}`); // string // console.log(`result: ${part.result}`); // break; // } case 'weather': { console.log('TOOL RESULT weather'); console.log(`location: ${part.input.location}`); // string console.log(`temperature: ${part.output.temperature}`); // number break; } } break; } case 'finish': { console.log('Finish reason:', part.finishReason); console.log('Total Usage:', part.totalUsage); break; } case 'error': console.error('Error:', part.error); break; } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-vertex-grounding.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: vertex('gemini-1.5-pro'), providerOptions: { google: { useSearchGrounding: true, }, }, prompt: 'List the top 5 San Francisco news from the past week.' + 'You must include the date of each article.', }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log((await result.providerMetadata)?.google); console.log(); console.log('Token usage:', await result.usage); console.log('Finish reason:', await result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/google-vertex-pdf-url.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: vertex('gemini-pro-experimental'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is an embedding model according to this document?', }, { type: 'file', data: 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/ai.pdf?raw=true', mediaType: 'application/pdf', }, ], }, ], }); 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); --- File: /ai/examples/ai-core/src/stream-text/google-vertex-reasoning.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { streamText } from 'ai'; async function main() { const result = streamText({ model: vertex('gemini-2.5-flash-preview-04-17'), prompt: "Describe the most unusual or striking architectural feature you've ever seen in a building or structure.", providerOptions: { google: { thinkingConfig: { thinkingBudget: 2048, includeThoughts: true, }, }, }, }); for await (const part of result.fullStream) { if (part.type === 'reasoning-delta') { process.stdout.write('\x1b[34m' + part.text + '\x1b[0m'); } else if (part.type === 'text-delta') { process.stdout.write(part.text); } } console.log(); console.log('Warnings:', await result.warnings); console.log(); console.log('Token usage:', await result.usage); console.log('Finish reason:', await result.finishReason); } main().catch(console.log); --- File: /ai/examples/ai-core/src/stream-text/google-vertex.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: vertex('gemini-1.5-pro'), system: 'You are a comedian. Only give funny answers.', 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); --- File: /ai/examples/ai-core/src/stream-text/google-youtube-url.ts --- import { google } from '@ai-sdk/google'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: google('gemini-1.5-pro-latest'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Watch this video and provide a detailed analysis of its content, themes, and any notable elements.', }, { type: 'file', data: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', mediaType: 'video/mp4', }, ], }, ], }); 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); --- File: /ai/examples/ai-core/src/stream-text/google.ts --- import { google } from '@ai-sdk/google'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: google('gemini-1.5-pro-latest'), system: 'You are a comedian. Only give funny answers.', 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); --- File: /ai/examples/ai-core/src/stream-text/groq-kimi-k2-tool-call.ts --- import { groq } from '@ai-sdk/groq'; import { streamText, ModelMessage, ToolCallPart, ToolResultPart } from 'ai'; import 'dotenv/config'; import { weatherTool } from '../tools/weather-tool'; const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; const result = streamText({ model: groq('moonshotai/kimi-k2-instruct'), maxOutputTokens: 512, tools: { weather: weatherTool, }, toolChoice: 'auto', prompt: 'What is the weather in San Francisco?', }); let fullResponse = ''; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const delta of result.fullStream) { switch (delta.type) { case 'text-delta': { fullResponse += delta.text; process.stdout.write(delta.text); break; } case 'tool-call': { toolCalls.push(delta); process.stdout.write( `\nTool call: '${delta.toolName}' ${JSON.stringify(delta.input)}`, ); break; } case 'tool-result': { if (delta.dynamic) { continue; } const transformedDelta: ToolResultPart = { ...delta, output: { type: 'json', value: delta.output }, }; toolResponses.push(transformedDelta); process.stdout.write( `\nTool response: '${delta.toolName}' ${JSON.stringify( delta.output, )}`, ); break; } } } process.stdout.write('\n\n'); messages.push({ role: 'assistant', content: [{ type: 'text', text: fullResponse }, ...toolCalls], }); if (toolResponses.length > 0) { messages.push({ role: 'tool', content: toolResponses }); } toolResponseAvailable = toolCalls.length > 0; console.log('Messages:', messages[0].content); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/groq-kimi-k2.ts --- import { groq } from '@ai-sdk/groq'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: groq('moonshotai/kimi-k2-instruct'), 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); --- File: /ai/examples/ai-core/src/stream-text/groq-openai-oss.ts --- import { groq } from '@ai-sdk/groq'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: groq('openai/gpt-oss-120b'), 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); --- File: /ai/examples/ai-core/src/stream-text/groq-raw-chunks.ts --- import { groq } from '@ai-sdk/groq'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: groq('llama-3.3-70b-versatile'), prompt: 'Count from 1 to 3 slowly.', includeRawChunks: true, }); let textChunkCount = 0; let rawChunkCount = 0; for await (const chunk of result.fullStream) { if (chunk.type === 'text-delta') { textChunkCount++; console.log('Text chunk', textChunkCount, ':', chunk.text); } else if (chunk.type === 'raw') { rawChunkCount++; console.log( 'Raw chunk', rawChunkCount, ':', JSON.stringify(chunk.rawValue), ); } } console.log(); console.log('Text chunks:', textChunkCount); console.log('Raw chunks:', rawChunkCount); console.log('Final text:', await result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/groq-reasoning-fullstream.ts --- import { groq } from '@ai-sdk/groq'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: groq('deepseek-r1-distill-llama-70b'), providerOptions: { groq: { reasoningFormat: 'parsed' }, }, prompt: 'How many "r"s are in the word "strawberry"?', }); let enteredReasoning = false; let enteredText = false; for await (const part of result.fullStream) { if (part.type === 'reasoning-delta') { if (!enteredReasoning) { enteredReasoning = true; console.log('\nREASONING:\n'); } process.stdout.write(part.text); } else if (part.type === 'text-delta') { if (!enteredText) { enteredText = true; console.log('\nTEXT:\n'); } process.stdout.write(part.text); } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/groq.ts --- import { groq } from '@ai-sdk/groq'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: groq('gemma2-9b-it'), 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); --- File: /ai/examples/ai-core/src/stream-text/lmstudio.ts --- import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { streamText } from 'ai'; import 'dotenv/config'; const lmstudio = createOpenAICompatible({ name: 'lmstudio', baseURL: 'http://localhost:1234/v1', }); async function main() { const result = streamText({ model: lmstudio('bartowski/gemma-2-9b-it-GGUF'), prompt: 'Invent a new holiday and describe its traditions.', maxRetries: 1, }); 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); --- File: /ai/examples/ai-core/src/stream-text/mistral-chatbot.ts --- import { mistral } from '@ai-sdk/mistral'; import { stepCountIs, ModelMessage, streamText, tool } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { z } from 'zod/v4'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const result = streamText({ model: mistral('mistral-large-latest'), onError(error) { console.error(error); }, system: `You are a helpful, respectful and honest assistant.`, tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), messages, }); process.stdout.write('\nAssistant: '); for await (const delta of result.textStream) { process.stdout.write(delta); } process.stdout.write('\n\n'); messages.push(...(await result.response).messages); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/mistral-fullstream.ts --- import { mistral } from '@ai-sdk/mistral'; import { streamText } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = streamText({ model: mistral('mistral-large-latest'), tools: { weather: weatherTool, cityAttractions: { inputSchema: z.object({ city: z.string() }), }, }, prompt: 'What is the weather in San Francisco?', }); for await (const part of result.fullStream) { switch (part.type) { case 'text-delta': { console.log('Text:', part.text); break; } case 'tool-call': { if (part.dynamic) { continue; } switch (part.toolName) { case 'cityAttractions': { console.log('TOOL CALL cityAttractions'); console.log(`city: ${part.input.city}`); // string break; } case 'weather': { console.log('TOOL CALL weather'); console.log(`location: ${part.input.location}`); // string break; } } break; } case 'tool-result': { if (part.dynamic) { continue; } switch (part.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // console.log('TOOL RESULT cityAttractions'); // console.log(`city: ${part.input.city}`); // string // console.log(`result: ${part.result}`); // break; // } case 'weather': { console.log('TOOL RESULT weather'); console.log(`location: ${part.input.location}`); // string console.log(`temperature: ${part.output.temperature}`); // number break; } } break; } case 'error': console.error('Error:', part.error); break; } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/mistral-raw-chunks.ts --- import { mistral } from '@ai-sdk/mistral'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: mistral('mistral-small-latest'), prompt: 'Count from 1 to 3 slowly.', includeRawChunks: true, }); let textChunkCount = 0; let rawChunkCount = 0; for await (const chunk of result.fullStream) { if (chunk.type === 'text-delta') { textChunkCount++; console.log('Text chunk', textChunkCount, ':', chunk.text); } else if (chunk.type === 'raw') { rawChunkCount++; console.log( 'Raw chunk', rawChunkCount, ':', JSON.stringify(chunk.rawValue), ); } } console.log(); console.log('Text chunks:', textChunkCount); console.log('Raw chunks:', rawChunkCount); console.log('Final text:', await result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/mistral-reasoning-raw.ts --- import { mistral } from '@ai-sdk/mistral'; import { extractReasoningMiddleware, streamText, wrapLanguageModel } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: wrapLanguageModel({ model: mistral('magistral-small-2506'), middleware: extractReasoningMiddleware({ tagName: 'think', }), }), prompt: 'What is 2 + 2?', }); console.log('Mistral reasoning model with extracted reasoning:'); console.log(); let enteredReasoning = false; let enteredText = false; for await (const part of result.fullStream) { if (part.type === 'reasoning-delta') { if (!enteredReasoning) { enteredReasoning = true; console.log('REASONING:'); } process.stdout.write(part.text); } else if (part.type === 'text-delta') { if (!enteredText) { enteredText = true; console.log('\n\nTEXT:'); } process.stdout.write(part.text); } } console.log(); console.log(); console.log('Usage:', await result.usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/mistral.ts --- import { mistral } from '@ai-sdk/mistral'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: mistral('ministral-8b-latest'), maxOutputTokens: 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); --- File: /ai/examples/ai-core/src/stream-text/mock.ts --- import { streamText } from 'ai'; import { convertArrayToReadableStream, MockLanguageModelV2 } from 'ai/test'; import 'dotenv/config'; async function main() { const result = streamText({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '0' }, { type: 'text-delta', id: '0', delta: 'Hello' }, { type: 'text-delta', id: '0', delta: ', ' }, { type: 'text-delta', id: '0', delta: `world!` }, { type: 'text-end', id: '0' }, { type: 'finish', finishReason: 'stop', logprobs: undefined, usage: { inputTokens: 3, outputTokens: 10, totalTokens: 13, }, }, ]), }), }), prompt: 'Hello, test!', }); 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); --- File: /ai/examples/ai-core/src/stream-text/nim.ts --- import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const nim = createOpenAICompatible({ baseURL: 'https://integrate.api.nvidia.com/v1', name: 'nim', headers: { Authorization: `Bearer ${process.env.NIM_API_KEY}`, }, }); const model = nim.chatModel('deepseek-ai/deepseek-r1'); const result = streamText({ model, prompt: 'Tell me the history of the Northern White Rhino.', }); 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); --- File: /ai/examples/ai-core/src/stream-text/openai-abort.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { try { const { textStream } = streamText({ model: openai('gpt-3.5-turbo'), prompt: 'Write a short story about a robot learning to love:\n\n', abortSignal: AbortSignal.timeout(3000), }); for await (const textPart of textStream) { process.stdout.write(textPart); } } catch (error) { if ( error instanceof Error && (error.name === 'AbortError' || error.name === 'TimeoutError') ) { console.log('\n\nAbortError: The run was aborted.'); } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-audio.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = streamText({ model: openai('gpt-4o-audio-preview'), messages: [ { role: 'user', content: [ { type: 'text', text: 'What is the audio saying?' }, { type: 'file', mediaType: 'audio/mpeg', data: fs.readFileSync('./data/galileo.mp3'), }, ], }, ], }); 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); --- File: /ai/examples/ai-core/src/stream-text/openai-cached-prompt-tokens.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; import { setTimeout } from 'node:timers/promises'; import { performance } from 'node:perf_hooks'; const longPrompt = ` Arms and the man I sing, who first made way, Predestined exile, from the Trojan shore To Italy, the blest Lavinian strand. Smitten of storms he was on land and sea By violence of Heaven, to satisfy 5 Stern Juno’s sleepless wrath; and much in war He suffered, seeking at the last to found The city, and bring o’er his fathers’ gods To safe abode in Latium; whence arose The Latin race, old Alba’s reverend lords, 10 And from her hills wide-walled, imperial Rome. O Muse, the causes tell! What sacrilege, Or vengeful sorrow, moved the heavenly Queen To thrust on dangers dark and endless toil A man whose largest honor in men’s eyes 15 Was serving Heaven? Can gods such anger feel? In ages gones an ancient city stood— Carthage, a Tyrian seat, which from afar Made front on Italy and on the mouths Of Tiber’s stream; its wealth and revenues 20 Were vast, and ruthless was its quest of war. ’T is said that Juno, of all lands she loved, Most cherished this,—not Samos’ self so dear. Here were her arms, her chariot; even then A throne of power o’er nations near and far, 25 If Fate opposed not, ’t was her darling hope To ’stablish here; but anxiously she heard That of the Trojan blood there was a breed Then rising, which upon the destined day Should utterly o’erwhelm her Tyrian towers; 30 A people of wide sway and conquest proud Should compass Libya’s doom;—such was the web The Fatal Sisters spun. Such was the fear Of Saturn’s daughter, who remembered well What long and unavailing strife she waged 35 For her loved Greeks at Troy. Nor did she fail To meditate th’ occasions of her rage, And cherish deep within her bosom proud Its griefs and wrongs: the choice by Paris made; Her scorned and slighted beauty; a whole race 40 Rebellious to her godhead; and Jove’s smile That beamed on eagle-ravished Ganymede. With all these thoughts infuriate, her power Pursued with tempests o’er the boundless main The Trojans, though by Grecian victor spared 45 And fierce Achilles; so she thrust them far From Latium; and they drifted, Heaven-impelled, Year after year, o’er many an unknown sea— O labor vast, to found the Roman line! Below th’ horizon the Sicilian isle 50 Just sank from view, as for the open sea With heart of hope they said, and every ship Clove with its brazen beak the salt, white waves. But Juno of her everlasting wound Knew no surcease, but from her heart of pain 55 Thus darkly mused: “Must I, defeated, fail “Of what I will, nor turn the Teucrian King “From Italy away? Can Fate oppose? “Had Pallas power to lay waste in flame “The Argive fleet and sink its mariners, 60 “Revenging but the sacrilege obscene “By Ajax wrought, Oïleus’ desperate son? “She, from the clouds, herself Jove’s lightning threw, “Scattered the ships, and ploughed the sea with storms. “Her foe, from his pierced breast out-breathing fire, 65 “In whirlwind on a deadly rock she flung. “But I, who move among the gods a queen, “Jove’s sister and his spouse, with one weak tribe “Make war so long! Who now on Juno calls? “What suppliant gifts henceforth her altars crown?” 70 So, in her fevered heart complaining still, Unto the storm-cloud land the goddess came, A region with wild whirlwinds in its womb, Æolia named, where royal Æolus In a high-vaulted cavern keeps control 75 O’er warring winds and loud concoùrse of storms. There closely pent in chains and bastions strong, They, scornful, make the vacant mountain roar, Chafing against their bonds. But from a throne Of lofty crag, their king with sceptred hand 80 Allays their fury and their rage confines. Did he not so, our ocean, earth, and sky Were whirled before them through the vast inane. But over-ruling Jove, of this in fear, Hid them in dungeon dark: then o’er them piled 85 Huge mountains, and ordained a lawful king To hold them in firm sway, or know what time, With Jove’s consent, to loose them o’er the world. To him proud Juno thus made lowly plea: “Thou in whose hands the Father of all gods 90 “And Sovereign of mankind confides the power “To calm the waters or with winds upturn, “Great Æolus! a race with me at war “Now sails the Tuscan main towards Italy, “Bringing their Ilium and its vanquished powers. 95 “Uprouse thy gales! Strike that proud navy down! “Hurl far and wide, and strew the waves with dead! “Twice seven nymphs are mine, of rarest mould, “Of whom Deïopea, the most fair, “I give thee in true wedlock for thine own, 100 “To mate thy noble worth; she at thy side “Shall pass long, happy years, and fruitful bring “Her beauteous offspring unto thee their sire.” Then Æolus: “’T is thy sole task, O Queen “To weigh thy wish and will. My fealty 105 “Thy high behest obeys. This humble throne “Is of thy gift. Thy smiles for me obtain “Authority from Jove. Thy grace concedes “My station at your bright Olympian board, “And gives me lordship of the darkening storm.” 110 Replying thus, he smote with spear reversed The hollow mountain’s wall; then rush the winds Through that wide breach in long, embattled line, And sweep tumultuous from land to land: With brooding pinions o’er the waters spread 115 East wind and south, and boisterous Afric gale Upturn the sea; vast billows shoreward roll; The shout of mariners, the creak of cordage, Follow the shock; low-hanging clouds conceal From Trojan eyes all sight of heaven and day; 120 Night o’er the ocean broods; from sky to sky The thunder roll, the ceaseless lightnings glare; And all things mean swift death for mortal man. `; function createCompletion() { return streamText({ model: openai('gpt-4o-mini'), messages: [ { role: 'user', content: `What book is the following text from?: <text>${longPrompt}</text>`, }, ], providerOptions: { openai: { maxCompletionTokens: 100 }, }, onFinish: ({ usage, providerMetadata }) => { console.log(`metadata:`, providerMetadata); }, }); } async function main() { let start = performance.now(); let result = await createCompletion(); let end = performance.now(); console.log(`duration: ${Math.floor(end - start)} ms`); let fullResponse = ''; process.stdout.write('\nAssistant: '); for await (const delta of result.textStream) { fullResponse += delta; process.stdout.write(delta); } process.stdout.write('\n\n'); } main() .then(() => console.log(`done!`)) .catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-chatbot.ts --- import { openai } from '@ai-sdk/openai'; import { stepCountIs, ModelMessage, streamText, tool } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { z } from 'zod/v4'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { messages.push({ role: 'user', content: await terminal.question('You: ') }); const result = streamText({ model: openai('gpt-4o'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), toModelOutput: ({ location, temperature }) => ({ type: 'text', value: `The weather in ${location} is ${temperature} degrees Fahrenheit.`, }), }), }, stopWhen: stepCountIs(5), messages, }); process.stdout.write('\nAssistant: '); for await (const delta of result.textStream) { process.stdout.write(delta); } process.stdout.write('\n\n'); messages.push(...(await result.response).messages); console.log( (await result.steps) .map(step => JSON.stringify(step.request.body)) .join('\n'), ); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-compatible-deepseek.ts --- import 'dotenv/config'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { streamText } from 'ai'; async function main() { const deepseek = createOpenAICompatible({ baseURL: 'https://api.deepseek.com/v1', name: 'deepseek', headers: { Authorization: `Bearer ${process.env.DEEPSEEK_API_KEY}`, }, }); const model = deepseek.chatModel('deepseek-chat'); const result = streamText({ model, prompt: 'List the top 5 San Francisco news from the past week.' + 'You must include the date of each article.', }); 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); --- File: /ai/examples/ai-core/src/stream-text/openai-compatible-litellm-anthropic-cache-control.ts --- import 'dotenv/config'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { streamText } from 'ai'; async function main() { // See ../../../litellm/README.md for instructions on how to run a LiteLLM // proxy locally configured to interface with Anthropic. const litellmAnthropic = createOpenAICompatible({ baseURL: 'http://0.0.0.0:4000', name: 'litellm-anthropic', }); const model = litellmAnthropic.chatModel('claude-3-5-sonnet-20240620'); const result = streamText({ model, messages: [ { role: 'system', // https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#cache-limitations // The cache content must be of a meaningful size (e.g. 1024 tokens, see // above for detail) and will only be cached for a moderate period of // time e.g. 5 minutes. content: "You are an AI assistant tasked with analyzing this story: The ancient clocktower stood sentinel over Millbrook Valley, its weathered copper face gleaming dully in the late afternoon sun. Sarah Chen adjusted her backpack and gazed up at the structure that had fascinated her since childhood. At thirteen stories tall, it had been the highest building in town for over a century, though now it was dwarfed by the glass and steel office buildings that had sprung up around it.\n\nThe door creaked as she pushed it open, sending echoes through the dusty entrance hall. Her footsteps on the marble floor seemed unnaturally loud in the empty space. The restoration project wouldn't officially begin for another week, but as the lead architectural historian, she had permission to start her preliminary survey early.\n\nThe building had been abandoned for twenty years, ever since the great earthquake of 2003 had damaged the clock mechanism. The city had finally approved funding to restore it to working order, but Sarah suspected there was more to the clocktower than anyone realized. Her research had uncovered hints that its architect, Theodore Hammond, had built secret rooms and passages throughout the structure.\n\nShe clicked on her flashlight and began climbing the main staircase. The emergency lights still worked on the lower floors, but she'd need the extra illumination higher up. The air grew mustier as she ascended, thick with decades of undisturbed dust. Her hand traced along the ornate brass railings, feeling the intricate patterns worked into the metal.\n\nOn the seventh floor, something caught her eye - a slight irregularity in the wall paneling that didn't match the blueprints she'd memorized. Sarah ran her fingers along the edge of the wood, pressing gently until she felt a click. A hidden door swung silently open, revealing a narrow passage.\n\nHer heart pounding with excitement, she squeezed through the opening. The passage led to a small octagonal room she estimated to be directly behind the clock face. Gears and mechanisms filled the space, all connected to a central shaft that rose up through the ceiling. But it was the walls that drew her attention - they were covered in elaborate astronomical charts and mathematical formulas.\n\n\"It's not just a clock,\" she whispered to herself. \"It's an orrery - a mechanical model of the solar system!\"\n\nThe complexity of the mechanism was far beyond what should have existed in the 1890s when the tower was built. Some of the mathematical notations seemed to describe orbital mechanics that wouldn't be discovered for decades after Hammond's death. Sarah's mind raced as she documented everything with her camera.\n\nA loud grinding sound from above made her jump. The central shaft began to rotate slowly, setting the gears in motion. She watched in amazement as the astronomical models came to life, planets and moons tracking across their metal orbits. But something was wrong - the movements didn't match any normal celestial patterns she knew.\n\nThe room grew noticeably colder. Sarah's breath frosted in the air as the mechanism picked up speed. The walls seemed to shimmer, becoming translucent. Through them, she could see not the expected view of downtown Millbrook, but a star-filled void that made her dizzy to look at.\n\nShe scrambled back toward the hidden door, but it had vanished. The room was spinning now, or maybe reality itself was spinning around it. Sarah grabbed onto a support beam as her stomach lurched. The stars beyond the walls wheeled and danced in impossible patterns.\n\nJust when she thought she couldn't take anymore, everything stopped. The mechanism ground to a halt. The walls solidified. The temperature returned to normal. Sarah's hands shook as she checked her phone - no signal, but the time display showed she had lost three hours.\n\nThe hidden door was back, and she practically fell through it in her haste to exit. She ran down all thirteen flights of stairs without stopping, bursting out into the street. The sun was setting now, painting the sky in deep purples and reds. Everything looked normal, but she couldn't shake the feeling that something was subtly different.\n\nBack in her office, Sarah pored over the photos she'd taken. The astronomical charts seemed to change slightly each time she looked at them, the mathematical formulas rearranging themselves when viewed from different angles. None of her colleagues believed her story about what had happened in the clocktower, but she knew what she had experienced was real.\n\nOver the next few weeks, she threw herself into research, trying to learn everything she could about Theodore Hammond. His personal papers revealed an obsession with time and dimensional theory far ahead of his era. There were references to experiments with \"temporal architecture\" and \"geometric manipulation of spacetime.\"\n\nThe restoration project continued, but Sarah made sure the hidden room remained undiscovered. Whatever Hammond had built, whatever portal or mechanism he had created, she wasn't sure the world was ready for it. But late at night, she would return to the clocktower and study the mysterious device, trying to understand its secrets.\n\nSometimes, when the stars aligned just right, she could hear the gears beginning to turn again, and feel reality starting to bend around her. And sometimes, in her dreams, she saw Theodore Hammond himself, standing at a drawing board, sketching plans for a machine that could fold space and time like paper - a machine that looked exactly like the one hidden in the heart of his clocktower.\n\nThe mystery of what Hammond had truly built, and why, consumed her thoughts. But with each new piece of evidence she uncovered, Sarah became more certain of one thing - the clocktower was more than just a timepiece. It was a key to understanding the very nature of time itself, and its secrets were only beginning to be revealed.\n", providerOptions: { openaiCompatible: { cache_control: { type: 'ephemeral', }, }, }, }, { role: 'user', content: 'What are the key narrative points made in this story?', }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log(); console.log('Token usage:', result.usage); console.log('Finish reason:', result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-compatible-raw-chunks.ts --- import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const openaiCompatible = createOpenAICompatible({ baseURL: 'https://api.openai.com/v1', name: 'openai-compatible', headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, }, }); const result = streamText({ model: openaiCompatible.completionModel('gpt-3.5-turbo-instruct'), prompt: 'Hello, World!', includeRawChunks: true, }); let textChunkCount = 0; let rawChunkCount = 0; let otherChunkCount = 0; for await (const chunk of result.fullStream) { console.log('Chunk type:', chunk.type, 'Chunk:', JSON.stringify(chunk)); if (chunk.type === 'text-delta') { textChunkCount++; console.log('Text chunk', textChunkCount, ':', chunk.text); } else if (chunk.type === 'raw') { rawChunkCount++; console.log( 'Raw chunk', rawChunkCount, ':', JSON.stringify(chunk.rawValue), ); } else { otherChunkCount++; console.log('Other chunk', otherChunkCount, ':', chunk.type); } } console.log(); console.log('Text chunks:', textChunkCount); console.log('Raw chunks:', rawChunkCount); console.log('Other chunks:', otherChunkCount); console.log('Final text:', await result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-compatible-togetherai-tool-call.ts --- import 'dotenv/config'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { streamText, ModelMessage, ToolCallPart, ToolResultPart } from 'ai'; import { weatherTool } from '../tools/weather-tool'; const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; const togetherai = createOpenAICompatible({ baseURL: 'https://api.together.xyz/v1', name: 'togetherai', headers: { Authorization: `Bearer ${process.env.TOGETHER_AI_API_KEY}`, }, }); const model = togetherai.chatModel( 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo', ); const result = streamText({ model, maxOutputTokens: 512, tools: { weather: weatherTool, }, toolChoice: 'required', prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); let fullResponse = ''; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const delta of result.fullStream) { switch (delta.type) { case 'text-delta': { fullResponse += delta.text; process.stdout.write(delta.text); break; } case 'tool-call': { toolCalls.push(delta); process.stdout.write( `\nTool call: '${delta.toolName}' ${JSON.stringify(delta.input)}`, ); break; } case 'tool-result': { if (delta.dynamic) { continue; } const transformedDelta: ToolResultPart = { ...delta, output: { type: 'json', value: delta.output }, }; toolResponses.push(transformedDelta); process.stdout.write( `\nTool response: '${delta.toolName}' ${JSON.stringify( delta.output, )}`, ); break; } } } process.stdout.write('\n\n'); messages.push({ role: 'assistant', content: [{ type: 'text', text: fullResponse }, ...toolCalls], }); if (toolResponses.length > 0) { messages.push({ role: 'tool', content: toolResponses }); } toolResponseAvailable = toolCalls.length > 0; console.log('Messages:', messages[0].content); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-compatible-togetherai.ts --- import 'dotenv/config'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { streamText } from 'ai'; async function main() { const togetherai = createOpenAICompatible({ baseURL: 'https://api.together.xyz/v1', name: 'togetherai', headers: { Authorization: `Bearer ${process.env.TOGETHER_AI_API_KEY}`, }, }); const model = togetherai.chatModel( 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo', ); const result = streamText({ model, prompt: 'List the top 5 San Francisco news from the past week.' + 'You must include the date of each article.', }); 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); --- File: /ai/examples/ai-core/src/stream-text/openai-completion-chat.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: openai('gpt-3.5-turbo-instruct'), maxOutputTokens: 1024, system: 'You are a helpful chatbot.', messages: [ { role: 'user', content: 'Hello!', }, { role: 'assistant', content: 'Hello! How can I help you today?', }, { role: 'user', content: 'I need help with my computer.', }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-completion.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: openai('gpt-3.5-turbo-instruct'), maxOutputTokens: 1024, temperature: 0.3, 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); --- File: /ai/examples/ai-core/src/stream-text/openai-custom-fetch-inject-error.ts --- import { createOpenAI } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; const openai = createOpenAI({ // example fetch wrapper that injects an error after 1000 characters: fetch: async (url, options) => { const result = await fetch(url, options); // Intercept the response stream const originalBody = result.body; if (originalBody) { const reader = originalBody.getReader(); let characterCount = 0; const stream = new ReadableStream({ async start(controller) { while (true) { const { done, value } = await reader.read(); if (done) break; characterCount += value.length; controller.enqueue(value); if (characterCount > 1000) { controller.error( new Error('Injected error after 1000 characters'), ); break; } } controller.close(); }, }); return new Response(stream, { headers: result.headers, status: result.status, statusText: result.statusText, }); } return result; }, }); async function main() { const result = streamText({ model: openai('gpt-3.5-turbo'), prompt: 'Invent a new holiday and describe its traditions.', }); for await (const part of result.fullStream) { process.stdout.write(JSON.stringify(part)); } console.log(); console.log('Token usage:', await result.usage); console.log('Finish reason:', await result.finishReason); } main().catch(error => { console.error('HERE'); console.error(error); }); --- File: /ai/examples/ai-core/src/stream-text/openai-dynamic-tool-call.ts --- import { openai } from '@ai-sdk/openai'; import 'dotenv/config'; import { weatherTool } from '../tools/weather-tool'; import { stepCountIs, streamText, dynamicTool, ToolSet } from 'ai'; import { z } from 'zod/v4'; function dynamicTools(): ToolSet { return { currentLocation: dynamicTool({ description: 'Get the current location.', inputSchema: z.object({}), execute: async () => { const locations = ['New York', 'London', 'Paris']; return { location: locations[Math.floor(Math.random() * locations.length)], }; }, }), }; } async function main() { const result = streamText({ model: openai('gpt-4o'), stopWhen: stepCountIs(5), tools: { ...dynamicTools(), weather: weatherTool, }, prompt: 'What is the weather in my current location?', }); for await (const chunk of result.fullStream) { switch (chunk.type) { case 'text-delta': { process.stdout.write(chunk.text); break; } case 'tool-call': { if (chunk.dynamic) { console.log('DYNAMIC CALL', JSON.stringify(chunk, null, 2)); continue; } switch (chunk.toolName) { case 'weather': { console.log('STATIC CALL', JSON.stringify(chunk, null, 2)); chunk.input.location; // string break; } } break; } case 'tool-result': { if (chunk.dynamic) { console.log('DYNAMIC RESULT', JSON.stringify(chunk, null, 2)); continue; } switch (chunk.toolName) { case 'weather': { console.log('STATIC RESULT', JSON.stringify(chunk, null, 2)); chunk.input.location; // string chunk.output.location; // string chunk.output.temperature; // number break; } } break; } case 'error': console.error('Error:', chunk.error); break; } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-flex-processing.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { console.log('Testing OpenAI Flex Processing...\n'); const result = streamText({ model: openai('o3-mini'), prompt: 'Explain quantum computing in simple terms.', providerOptions: { openai: { serviceTier: 'flex', // 50% cheaper processing with increased latency }, }, }); console.log('Response (using flex processing for 50% cost savings):'); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log('\n\nUsage:'); const usage = await result.usage; console.log(`Input tokens: ${usage.inputTokens}`); console.log(`Output tokens: ${usage.outputTokens}`); console.log(`Total tokens: ${usage.totalTokens}`); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-fullstream-logprobs.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: openai('gpt-3.5-turbo'), maxOutputTokens: 512, temperature: 0.3, maxRetries: 5, prompt: 'Invent a new holiday and describe its traditions.', providerOptions: { openai: { logprobs: 2, }, }, }); for await (const part of result.fullStream) { switch (part.type) { case 'finish-step': { console.log('Logprobs:', part.providerMetadata?.openai.logprobs); break; } case 'error': console.error('Error:', part.error); break; } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-fullstream-raw.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: openai('gpt-4o-mini'), prompt: 'Invent a new holiday and describe its traditions.', }); for await (const part of result.fullStream) { console.log(JSON.stringify(part)); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-fullstream.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = streamText({ model: openai('gpt-3.5-turbo'), tools: { weather: weatherTool, cityAttractions: { inputSchema: z.object({ city: z.string() }), }, }, prompt: 'What is the weather in San Francisco?', }); for await (const part of result.fullStream) { switch (part.type) { case 'text-delta': { console.log('Text:', part.text); break; } case 'tool-call': { if (part.dynamic) { continue; } switch (part.toolName) { case 'cityAttractions': { console.log('TOOL CALL cityAttractions'); console.log(`city: ${part.input.city}`); // string break; } case 'weather': { console.log('TOOL CALL weather'); console.log(`location: ${part.input.location}`); // string break; } } break; } case 'tool-result': { if (part.dynamic) { continue; } switch (part.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // console.log('TOOL RESULT cityAttractions'); // console.log(`city: ${part.input.city}`); // string // console.log(`result: ${part.result}`); // break; // } case 'weather': { console.log('TOOL RESULT weather'); console.log(`location: ${part.input.location}`); // string console.log(`temperature: ${part.output.temperature}`); // number break; } } break; } case 'finish': { console.log('Finish reason:', part.finishReason); console.log('Total Usage:', part.totalUsage); break; } case 'error': console.error('Error:', part.error); break; } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-global-provider.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; globalThis.AI_SDK_DEFAULT_PROVIDER = openai; async function main() { const result = streamText({ model: 'gpt-4o', 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); --- File: /ai/examples/ai-core/src/stream-text/openai-multi-step.ts --- import { openai } from '@ai-sdk/openai'; import { stepCountIs, streamText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamText({ model: openai('gpt-4o-2024-08-06'), tools: { currentLocation: tool({ description: 'Get the current location.', inputSchema: z.object({}), execute: async () => { const locations = ['New York', 'London', 'Paris']; return { location: locations[Math.floor(Math.random() * locations.length)], }; }, }), weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), prompt: 'What is the weather in my current location?', onStepFinish: step => { console.log(JSON.stringify(step, null, 2)); }, }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-on-chunk-tool-call-streaming.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; async function main() { const result = streamText({ model: openai('gpt-3.5-turbo'), tools: { weather: weatherTool, cityAttractions: { inputSchema: z.object({ city: z.string() }), }, }, onChunk(chunk) { console.log('onChunk', chunk); }, prompt: 'What is the weather in San Francisco?', }); // consume stream: for await (const textPart of result.textStream) { } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-on-chunk.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: openai('gpt-3.5-turbo'), onChunk({ chunk }) { console.log('onChunk', chunk); }, prompt: 'Invent a new holiday and describe its traditions.', }); // consume stream: for await (const textPart of result.textStream) { } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-on-finish-response-messages.ts --- import { openai } from '@ai-sdk/openai'; import { stepCountIs, streamText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamText({ model: openai('gpt-4o'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string() }), execute: async () => ({ temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), onFinish({ response }) { console.log(JSON.stringify(response.messages, null, 2)); }, prompt: 'What is the current weather in San Francisco?', }); // consume the text stream for await (const textPart of result.textStream) { } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-on-finish-steps.ts --- import { openai } from '@ai-sdk/openai'; import { stepCountIs, streamText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamText({ model: openai('gpt-4o'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string() }), execute: async () => ({ temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), onFinish({ steps }) { console.log(JSON.stringify(steps, null, 2)); }, prompt: 'What is the current weather in San Francisco?', }); // consume the text stream for await (const textPart of result.textStream) { } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-on-finish.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', onFinish({ usage, finishReason, text, toolCalls, toolResults, response }) { console.log(); console.log('onFinish'); console.log('Token usage:', usage); console.log('Finish reason:', finishReason); console.log('Text:', text); }, }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-on-step-finish.ts --- import { openai } from '@ai-sdk/openai'; import { stepCountIs, streamText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamText({ model: openai('gpt-4o'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string() }), execute: async () => ({ temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), onStepFinish(step) { console.log(JSON.stringify(step, null, 2)); }, prompt: 'What is the current weather in San Francisco?', }); // consume the text stream for await (const textPart of result.textStream) { } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-output-object.ts --- import { openai } from '@ai-sdk/openai'; import { stepCountIs, Output, streamText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const { experimental_partialOutputStream: partialOutputStream } = streamText({ model: openai('gpt-4o-mini'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), // location below is inferred to be a string: execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, experimental_output: Output.object({ schema: z.object({ elements: z.array( z.object({ location: z.string(), temperature: z.number(), touristAttraction: z.string(), }), ), }), }), stopWhen: stepCountIs(2), prompt: 'What is the weather and the main tourist attraction in San Francisco, London Paris, and Berlin?', }); // [{ location: 'San Francisco', temperature: 81 }, ...] for await (const partialOutput of partialOutputStream) { console.clear(); console.log(partialOutput); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-predicted-output.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; const code = ` /// <summary> /// Represents a user with a first name, last name, and username. /// </summary> public class User { /// <summary> /// Gets or sets the user's first name. /// </summary> public string FirstName { get; set; } /// <summary> /// Gets or sets the user's last name. /// </summary> public string LastName { get; set; } /// <summary> /// Gets or sets the user's username. /// </summary> public string Username { get; set; } } `; async function main() { const result = streamText({ model: openai('gpt-4o'), messages: [ { role: 'user', content: 'Replace the Username property with an Email property. Respond only with code, and with no markdown formatting.', }, { role: 'user', content: code, }, ], providerOptions: { openai: { prediction: { type: 'content', content: code, }, }, }, }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } const usage = await result.usage; const openaiMetadata = (await result.providerMetadata)?.openai; console.log(); console.log('Token usage:', { ...usage, acceptedPredictionTokens: openaiMetadata?.acceptedPredictionTokens, rejectedPredictionTokens: openaiMetadata?.rejectedPredictionTokens, }); console.log('Finish reason:', await result.finishReason); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-prepare-step.ts --- import { openai } from '@ai-sdk/openai'; import { stepCountIs, streamText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; const retrieveInformation = tool({ description: 'Retrieve information from the database', inputSchema: z .object({ query: z.string(), }) .describe('The query to retrieve information from the database'), execute: async ({ query }) => { return { content: [`Retrieved information for query: ${query}`], }; }, }); async function main() { const result = streamText({ model: openai('gpt-4o'), prompt: 'How many "r"s are in the word "strawberry"?', tools: { retrieveInformation }, prepareStep({ stepNumber }) { if (stepNumber === 0) { return { toolChoice: { type: 'tool', toolName: 'retrieveInformation' }, activeTools: ['retrieveInformation'], }; } }, activeTools: [], stopWhen: stepCountIs(5), }); await result.consumeStream(); console.log(JSON.stringify(await result.steps, null, 2)); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-read-ui-message-stream.ts --- import { openai } from '@ai-sdk/openai'; import { readUIMessageStream, stepCountIs, streamText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamText({ model: openai('gpt-4.1-mini'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), toModelOutput: ({ location, temperature }) => ({ type: 'text', value: `The weather in ${location} is ${temperature} degrees Fahrenheit.`, }), }), }, stopWhen: stepCountIs(5), prompt: 'What is the weather in Tokyo?', }); for await (const uiMessage of readUIMessageStream({ stream: result.toUIMessageStream(), })) { console.clear(); console.log(JSON.stringify(uiMessage, null, 2)); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-reader.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: openai('gpt-3.5-turbo'), maxOutputTokens: 512, temperature: 0.3, maxRetries: 5, prompt: 'Invent a new holiday and describe its traditions.', }); const reader = result.textStream.getReader(); while (true) { const { done, value } = await reader.read(); if (done) { break; } process.stdout.write(value); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-reasoning.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: openai('o3-mini'), prompt: 'How many "r"s are in the word "strawberry"?', temperature: 0.5, // should get ignored (warning) maxOutputTokens: 1000, // mapped to max_completion_tokens }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log(); console.log('Usage:', await result.usage); console.log('Warnings:', await result.warnings); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-request-body.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: openai('gpt-4o-mini'), prompt: 'Invent a new holiday and describe its traditions.', }); // consume stream for await (const textPart of result.textStream) { } console.log('REQUEST BODY'); console.log((await result.request).body); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-responses-chatbot.ts --- import { openai } from '@ai-sdk/openai'; import { stepCountIs, ModelMessage, streamText, tool } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { z } from 'zod/v4'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const result = streamText({ model: openai.responses('gpt-4o-mini'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), messages, }); process.stdout.write('\nAssistant: '); for await (const delta of result.textStream) { process.stdout.write(delta); } process.stdout.write('\n\n'); messages.push(...(await result.response).messages); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-responses-raw-chunks.ts --- import 'dotenv/config'; import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; async function main() { const result = streamText({ model: openai.responses('o3-mini'), prompt: 'How many "r"s are in the word "strawberry"?', providerOptions: { openai: { reasoningEffort: 'low', reasoningSummary: 'auto', }, }, includeRawChunks: true, }); let textChunkCount = 0; let reasoningChunkCount = 0; let rawChunkCount = 0; let fullText = ''; let fullReasoning = ''; for await (const chunk of result.fullStream) { if (chunk.type === 'raw') { rawChunkCount++; console.log( 'Raw chunk', rawChunkCount, ':', JSON.stringify(chunk.rawValue), ); } else { console.log('Processed chunk:', chunk.type, JSON.stringify(chunk)); } if (chunk.type === 'text-delta') { textChunkCount++; fullText += chunk.text; } if (chunk.type === 'reasoning-delta') { reasoningChunkCount++; fullReasoning += chunk.text; } } console.log(); console.log('Text chunks:', textChunkCount); console.log('Reasoning chunks:', reasoningChunkCount); console.log('Raw chunks:', rawChunkCount); console.log('Final text:', fullText); console.log('Final reasoning:', fullReasoning); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-responses-reasoning-chatbot.ts --- import { openai, OpenAIResponsesProviderOptions } from '@ai-sdk/openai'; import { stepCountIs, ModelMessage, streamText, tool, APICallError } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { z } from 'zod/v4'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; // what is the weather in the 5th largest coastal city of germany? async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const result = streamText({ model: openai.responses('o3'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), messages, // includeRawChunks: true, onError: ({ error }) => { console.log('onError'); console.error(error); if (APICallError.isInstance(error)) { console.error(JSON.stringify(error.requestBodyValues, null, 2)); } }, // providerOptions: { // openai: { // store: false, // No data retention - makes interaction stateless // reasoningEffort: 'medium', // reasoningSummary: 'auto', // include: ['reasoning.encrypted_content'], // Hence, we need to retrieve the model's encrypted reasoning to be able to pass it to follow-up requests // } satisfies OpenAIResponsesProviderOptions, // }, }); process.stdout.write('\nAssistant: '); for await (const chunk of result.fullStream) { switch (chunk.type) { case 'raw': console.log(JSON.stringify(chunk.rawValue, null, 2)); break; case 'reasoning-start': process.stdout.write('\x1b[34m'); break; case 'reasoning-delta': process.stdout.write(chunk.text); break; case 'reasoning-end': process.stdout.write('\x1b[0m'); process.stdout.write('\n'); break; case 'tool-input-start': process.stdout.write('\x1b[33m'); console.log('Tool call:', chunk.toolName); process.stdout.write('Tool args: '); break; case 'tool-input-delta': process.stdout.write(chunk.delta); break; case 'tool-input-end': console.log(); break; case 'tool-result': console.log('Tool result:', chunk.output); process.stdout.write('\x1b[0m'); break; case 'tool-error': process.stdout.write('\x1b[0m'); process.stderr.write('\x1b[31m'); console.error('Tool error:', chunk.error); process.stderr.write('\x1b[0m'); break; case 'text-start': process.stdout.write('\x1b[32m'); break; case 'text-delta': process.stdout.write(chunk.text); break; case 'text-end': process.stdout.write('\x1b[0m'); console.log(); break; } } process.stdout.write('\n\n'); messages.push(...(await result.response).messages); } } main().catch(error => { console.log('main error'); console.error(error); if (APICallError.isInstance(error)) { console.error(JSON.stringify(error.requestBodyValues, null, 2)); } }); --- File: /ai/examples/ai-core/src/stream-text/openai-responses-reasoning-summary.ts --- import 'dotenv/config'; import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; async function main() { const result = streamText({ // supported: o4-mini, o3, o3-mini and o1 model: openai.responses('o3-mini'), system: 'You are a helpful assistant.', prompt: 'Tell me about the debate over Taqueria La Cumbre and El Farolito and who created the San Francisco Mission-style burrito.', providerOptions: { openai: { // https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries // reasoningSummary: 'auto', // 'detailed' reasoningSummary: 'auto', }, }, }); for await (const part of result.fullStream) { if (part.type === 'reasoning-delta') { process.stdout.write('\x1b[34m' + part.text + '\x1b[0m'); } else if (part.type === 'text-delta') { process.stdout.write(part.text); } } console.log(); console.log('Finish reason:', await result.finishReason); console.log('Usage:', await result.usage); console.log('Provider metadata:', await result.providerMetadata); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-responses-reasoning-tool-call.ts --- import { openai, OpenAIResponsesProviderOptions } from '@ai-sdk/openai'; import { stepCountIs, streamText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const result = streamText({ model: openai.responses('o3-mini'), stopWhen: stepCountIs(10), tools: { generateRandomText: tool({ description: 'Generate a random text of a given length', inputSchema: z.object({ length: z.number().min(1) }), execute: async ({ length }) => { if (Math.random() < 0.5) { throw new Error('Segmentation fault'); } return Array.from({ length }, () => String.fromCharCode(Math.floor(Math.random() * 26) + 97), ).join(''); }, }), countChar: tool({ description: 'Count the number of occurrences of a specific character in the text', inputSchema: z.object({ text: z.string(), char: z.string() }), execute: async ({ text, char }) => { if (Math.random() < 0.5) { throw new Error('Buffer overflow'); } return text.split(char).length - 1; }, }), }, system: `If you encounter a function call error, you should retry 3 times before giving up.`, prompt: `Generate two texts of 1024 characters each. Count the number of "a" in the first text, and the number of "b" in the second text.`, providerOptions: { openai: { store: false, reasoningEffort: 'medium', reasoningSummary: 'auto', include: ['reasoning.encrypted_content'], } satisfies OpenAIResponsesProviderOptions, }, }); for await (const chunk of result.fullStream) { switch (chunk.type) { case 'start': console.log('START'); break; case 'start-step': console.log('STEP START'); console.log( 'Request body:', JSON.stringify(chunk.request.body, null, 2), ); break; case 'reasoning-start': process.stdout.write('\x1b[34m'); break; case 'reasoning-delta': process.stdout.write(chunk.text); break; case 'reasoning-end': process.stdout.write('\x1b[0m'); process.stdout.write('\n'); break; case 'tool-input-start': process.stdout.write('\x1b[33m'); console.log('Tool call:', chunk.toolName); process.stdout.write('Tool args: '); break; case 'tool-input-delta': process.stdout.write(chunk.delta); break; case 'tool-input-end': console.log(); break; case 'tool-result': console.log('Tool result:', chunk.output); process.stdout.write('\x1b[0m'); break; case 'tool-error': process.stdout.write('\x1b[0m'); process.stderr.write('\x1b[31m'); console.error('Tool error:', chunk.error); process.stderr.write('\x1b[0m'); break; case 'text-start': process.stdout.write('\x1b[32m'); break; case 'text-delta': process.stdout.write(chunk.text); break; case 'text-end': process.stdout.write('\x1b[0m'); console.log(); break; case 'finish-step': console.log('Finish reason:', chunk.finishReason); console.log('Usage:', chunk.usage); console.log('STEP FINISH'); break; case 'finish': console.log('Finish reason:', chunk.finishReason); console.log('Total usage:', chunk.totalUsage); console.log('FINISH'); break; case 'error': process.stdout.write('\x1b[0m'); process.stderr.write('\x1b[31m'); console.error('Error:', chunk.error); process.stderr.write('\x1b[0m'); break; } } console.log('MESSAGES START'); const messages = (await result.steps).map(step => step.response.messages); for (const [i, message] of messages.entries()) { console.log(`Step ${i}:`, JSON.stringify(message, null, 2)); } console.log('MESSAGES FINISH'); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-responses-reasoning-zero-data-retention.ts --- import { openai, OpenAIResponsesProviderOptions } from '@ai-sdk/openai'; import { APICallError, streamText, UserModelMessage } from 'ai'; import 'dotenv/config'; async function main() { const result1 = streamText({ model: openai.responses('o3-mini'), prompt: 'Analyze the following encrypted data: U2VjcmV0UGFzc3dvcmQxMjM=. What type of encryption is this and what secret does it contain?', providerOptions: { openai: { store: false, // No data retention - makes interaction stateless reasoningEffort: 'medium', reasoningSummary: 'auto', include: ['reasoning.encrypted_content'], // Hence, we need to retrieve the model's encrypted reasoning to be able to pass it to follow-up requests } satisfies OpenAIResponsesProviderOptions, }, }); await result1.consumeStream(); console.log('=== First request ==='); process.stdout.write('\x1b[34m'); console.log(JSON.stringify(await result1.reasoning, null, 2)); process.stdout.write('\x1b[0m'); console.log(await result1.text); console.log(); console.log( 'Request body:', JSON.stringify((await result1.request).body, null, 2), ); const result2 = streamText({ model: openai.responses('o3-mini'), prompt: [ { role: 'user', content: [ { type: 'text', text: 'Analyze the following encrypted data: U2VjcmV0UGFzc3dvcmQxMjM=. What type of encryption is this and what secret does it contain?', }, ], }, ...(await result1.response).messages, // Need to pass all previous messages to the follow-up request { role: 'user', content: 'Based on your previous analysis, what security recommendations would you make?', } satisfies UserModelMessage, ], providerOptions: { openai: { store: false, // No data retention - makes interaction stateless reasoningEffort: 'medium', reasoningSummary: 'auto', include: ['reasoning.encrypted_content'], // Hence, we need to retrieve the model's encrypted reasoning to be able to pass it to follow-up requests } satisfies OpenAIResponsesProviderOptions, }, onError: ({ error }) => { console.error(error); if (APICallError.isInstance(error)) { console.error(JSON.stringify(error.requestBodyValues, null, 2)); } }, }); await result2.consumeStream(); console.log('=== Second request ==='); process.stdout.write('\x1b[34m'); console.log(JSON.stringify(await result2.reasoning, null, 2)); process.stdout.write('\x1b[0m'); console.log(await result2.text); console.log(); console.log( 'Request body:', JSON.stringify((await result2.request).body, null, 2), ); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-responses-reasoning.ts --- import 'dotenv/config'; import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; async function main() { const result = streamText({ model: openai.responses('o3-mini'), system: 'You are a helpful assistant.', prompt: 'Invent a new holiday and describe its traditions.', }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log(); console.log('Finish reason:', await result.finishReason); console.log('Usage:', await result.usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-responses-tool-call.ts --- import { openai, OpenAIResponsesProviderOptions } from '@ai-sdk/openai'; import 'dotenv/config'; import { weatherTool } from '../tools/weather-tool'; import { stepCountIs, streamText, tool } from 'ai'; import { z } from 'zod/v4'; async function main() { const result = streamText({ model: openai.responses('gpt-4o-mini'), stopWhen: stepCountIs(5), tools: { currentLocation: tool({ description: 'Get the current location.', inputSchema: z.object({}), execute: async () => { const locations = ['New York', 'London', 'Paris']; return { location: locations[Math.floor(Math.random() * locations.length)], }; }, }), weather: weatherTool, }, prompt: 'What is the weather in my current location and in Rome?', providerOptions: { openai: { parallelToolCalls: false, } satisfies OpenAIResponsesProviderOptions, }, }); for await (const chunk of result.fullStream) { switch (chunk.type) { case 'text-delta': { process.stdout.write(chunk.text); break; } case 'tool-call': { console.log( `TOOL CALL ${chunk.toolName} ${JSON.stringify(chunk.input)}`, ); break; } case 'tool-result': { console.log( `TOOL RESULT ${chunk.toolName} ${JSON.stringify(chunk.output)}`, ); break; } case 'finish-step': { console.log(); console.log(); console.log('STEP FINISH'); console.log('Finish reason:', chunk.finishReason); console.log('Usage:', chunk.usage); console.log(); break; } case 'finish': { console.log('FINISH'); console.log('Finish reason:', chunk.finishReason); console.log('Total Usage:', chunk.totalUsage); break; } case 'error': console.error('Error:', chunk.error); break; } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-responses-websearch.ts --- import 'dotenv/config'; import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; async function main() { const result = streamText({ model: openai.responses('gpt-4o-mini'), prompt: 'What happened in San Francisco last week?', tools: { web_search_preview: openai.tools.webSearchPreview({ searchContextSize: 'high', userLocation: { type: 'approximate', city: 'San Francisco', region: 'California', }, }), }, }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log(); console.log('Sources:', await result.sources); console.log('Finish reason:', await result.finishReason); console.log('Usage:', await result.usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-responses.ts --- import 'dotenv/config'; import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; async function main() { const result = streamText({ model: openai.responses('gpt-4o-mini'), maxOutputTokens: 100, system: 'You are a helpful assistant.', prompt: 'Invent a new holiday and describe its traditions.', }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log(); console.log('Finish reason:', await result.finishReason); console.log('Usage:', await result.usage); console.log(); console.log((await result.request).body); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-store-generation.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: openai('gpt-3.5-turbo'), maxOutputTokens: 512, temperature: 0.3, maxRetries: 5, prompt: 'Invent a new holiday and describe its traditions.', providerOptions: { openai: { store: true, metadata: { custom: 'value', }, }, }, }); 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); --- File: /ai/examples/ai-core/src/stream-text/openai-swarm.ts --- import { openai } from '@ai-sdk/openai'; import { stepCountIs, streamText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; main().catch(console.error); async function main() { const agentA = { system: 'You are a helpful agent.', activeTools: ['transferToAgentB'] as 'transferToAgentB'[], }; const agentB = { system: 'Only speak in Haikus.', activeTools: [], }; let activeAgent = agentA; const result = streamText({ model: openai('gpt-4o'), tools: { transferToAgentB: tool({ description: 'Transfer to agent B.', inputSchema: z.object({}), execute: async () => { activeAgent = agentB; return 'Transferred to agent B.'; }, }), }, stopWhen: stepCountIs(5), prepareStep: () => activeAgent, prompt: 'I want to talk to agent B.', }); for await (const chunk of result.textStream) { process.stdout.write(chunk); } } --- File: /ai/examples/ai-core/src/stream-text/openai-tool-abort.ts --- import { openai } from '@ai-sdk/openai'; import { stepCountIs, streamText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { const abortController = new AbortController(); const result = streamText({ model: openai('gpt-4o'), stopWhen: stepCountIs(5), tools: { currentLocation: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }, { abortSignal }) => { console.log('Starting tool call'); // simulate compute for 10 seconds, check abort signal every 50ms for (let i = 0; i < 10000 / 50; i++) { await new Promise(resolve => setTimeout(resolve, 50)); abortSignal?.throwIfAborted(); } console.log('Tool call finished'); return { location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }; }, }), }, prompt: 'What is the weather in New York?', abortSignal: abortController.signal, }); // delay for 3 seconds await new Promise(resolve => setTimeout(resolve, 3000)); abortController.abort(); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-tool-call-raw-json-schema.ts --- import { openai } from '@ai-sdk/openai'; import { jsonSchema, streamText, tool } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: openai('gpt-3.5-turbo'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: jsonSchema<{ location: string }>({ type: 'object', properties: { location: { type: 'string' }, }, required: ['location'], }), // location below is inferred to be a string: execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), cityAttractions: tool({ inputSchema: jsonSchema<{ city: string }>({ type: 'object', properties: { city: { type: 'string' }, }, required: ['city'], }), }), }, prompt: 'What is the weather in San Francisco?', }); for await (const part of result.fullStream) { switch (part.type) { case 'text-delta': { console.log('Text:', part.text); break; } case 'tool-call': { if (part.dynamic) { continue; } switch (part.toolName) { case 'cityAttractions': { console.log('TOOL CALL cityAttractions'); console.log(`city: ${part.input.city}`); // string break; } case 'weather': { console.log('TOOL CALL weather'); console.log(`location: ${part.input.location}`); // string break; } } break; } case 'tool-result': { if (part.dynamic) { continue; } switch (part.toolName) { // NOT AVAILABLE (NO EXECUTE METHOD) // case 'cityAttractions': { // console.log('TOOL RESULT cityAttractions'); // console.log(`city: ${part.input.city}`); // string // console.log(`result: ${part.result}`); // break; // } case 'weather': { console.log('TOOL RESULT weather'); console.log(`location: ${part.input.location}`); // string console.log(`temperature: ${part.output.temperature}`); // number break; } } break; } case 'finish': { console.log('Finish reason:', part.finishReason); console.log('Total Usage:', part.totalUsage); break; } case 'error': console.error('Error:', part.error); break; } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-tool-call.ts --- import { openai } from '@ai-sdk/openai'; import 'dotenv/config'; import { weatherTool } from '../tools/weather-tool'; import { stepCountIs, streamText, tool } from 'ai'; import { z } from 'zod/v4'; async function main() { const result = streamText({ model: openai('gpt-4-turbo'), stopWhen: stepCountIs(5), tools: { currentLocation: tool({ description: 'Get the current location.', inputSchema: z.object({}), execute: async () => { const locations = ['New York', 'London', 'Paris']; return { location: locations[Math.floor(Math.random() * locations.length)], }; }, }), weather: weatherTool, }, prompt: 'What is the weather in my current location?', }); for await (const chunk of result.fullStream) { switch (chunk.type) { case 'text-delta': { process.stdout.write(chunk.text); break; } case 'tool-call': { console.log( `TOOL CALL ${chunk.toolName} ${JSON.stringify(chunk.input)}`, ); break; } case 'tool-result': { console.log( `TOOL RESULT ${chunk.toolName} ${JSON.stringify(chunk.output)}`, ); break; } case 'finish-step': { console.log(); console.log(); console.log('STEP FINISH'); console.log('Finish reason:', chunk.finishReason); console.log('Usage:', chunk.usage); console.log(); break; } case 'finish': { console.log('FINISH'); console.log('Finish reason:', chunk.finishReason); console.log('Total Usage:', chunk.totalUsage); break; } case 'error': console.error('Error:', chunk.error); break; } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai-web-search-tool.ts --- import { openai } from '@ai-sdk/openai'; import { stepCountIs, streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: openai.responses('gpt-4o-mini'), stopWhen: stepCountIs(5), tools: { web_search_preview: openai.tools.webSearchPreview({ searchContextSize: 'high', }), }, toolChoice: { type: 'tool', toolName: 'web_search_preview' }, prompt: 'Look up the company that owns Sonny Angel', }); for await (const chunk of result.fullStream) { switch (chunk.type) { case 'text-delta': { process.stdout.write(chunk.text); break; } case 'source': { if (chunk.sourceType === 'url') { process.stdout.write(`\n\n Source: ${chunk.title} (${chunk.url})`); } else { process.stdout.write(`\n\n Document: ${chunk.title}`); } break; } case 'finish-step': { console.log(); console.log(); console.log('STEP FINISH'); console.log('Finish reason:', chunk.finishReason); console.log('Usage:', chunk.usage); console.log(); break; } case 'finish': { console.log('FINISH'); console.log('Finish reason:', chunk.finishReason); console.log('Total Usage:', chunk.totalUsage); break; } case 'error': console.error('Error:', chunk.error); break; } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/openai.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: openai('gpt-3.5-turbo'), maxOutputTokens: 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); --- File: /ai/examples/ai-core/src/stream-text/perplexity-images.ts --- import { perplexity } from '@ai-sdk/perplexity'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: perplexity('sonar-pro'), prompt: 'Tell me about the earliest cave drawings known and include images.', providerOptions: { perplexity: { return_images: true, }, }, }); 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); console.log( 'Metadata:', JSON.stringify(await result.providerMetadata, null, 2), ); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/perplexity-raw-chunks.ts --- import { perplexity } from '@ai-sdk/perplexity'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: perplexity('sonar-reasoning'), prompt: 'Count from 1 to 3 slowly.', includeRawChunks: true, }); let textChunkCount = 0; let rawChunkCount = 0; for await (const chunk of result.fullStream) { if (chunk.type === 'text-delta') { textChunkCount++; console.log('Text chunk', textChunkCount, ':', chunk.text); } else if (chunk.type === 'raw') { rawChunkCount++; console.log( 'Raw chunk', rawChunkCount, ':', JSON.stringify(chunk.rawValue), ); } } console.log(); console.log('Text chunks:', textChunkCount); console.log('Raw chunks:', rawChunkCount); console.log('Final text:', await result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/perplexity.ts --- import { perplexity } from '@ai-sdk/perplexity'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: perplexity('sonar-pro'), prompt: 'What has happened in San Francisco recently?', providerOptions: { perplexity: { search_recency_filter: 'week', }, }, }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log(); console.log('Sources:', await result.sources); console.log('Finish reason:', await result.finishReason); console.log('Usage:', await result.usage); console.log( 'Metadata:', JSON.stringify(await result.providerMetadata, null, 2), ); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/raw-chunks.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: openai('gpt-4o-mini'), prompt: 'Count from 1 to 3 slowly.', includeRawChunks: true, }); let textChunkCount = 0; let rawChunkCount = 0; for await (const chunk of result.fullStream) { if (chunk.type === 'text-delta') { textChunkCount++; console.log('Text chunk', textChunkCount, ':', chunk.text); } else if (chunk.type === 'raw') { rawChunkCount++; console.log( 'Raw chunk', rawChunkCount, ':', JSON.stringify(chunk.rawValue), ); } } console.log(); console.log('Text chunks:', textChunkCount); console.log('Raw chunks:', rawChunkCount); console.log('Final text:', await result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/smooth-stream-chinese.ts --- import { simulateReadableStream, smoothStream, streamText } from 'ai'; import { MockLanguageModelV2 } from 'ai/test'; async function main() { const result = streamText({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: simulateReadableStream({ chunks: [ { type: 'text-start', id: '0' }, { type: 'text-delta', id: '0', delta: '你好你好你好你好你好' }, { type: 'text-delta', id: '0', delta: '你好你好你好你好你好' }, { type: 'text-delta', id: '0', delta: '你好你好你好你好你好' }, { type: 'text-delta', id: '0', delta: '你好你好你好你好你好' }, { type: 'text-delta', id: '0', delta: '你好你好你好你好你好' }, { type: 'text-end', id: '0' }, { type: 'finish', finishReason: 'stop', logprobs: undefined, usage: { inputTokens: 3, outputTokens: 10, totalTokens: 13, }, }, ], chunkDelayInMs: 400, }), }), }), prompt: 'Say hello in Chinese!', experimental_transform: smoothStream({ chunking: /[\u4E00-\u9FFF]|\S+\s+/, }), }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/smooth-stream-japanese.ts --- import { simulateReadableStream, smoothStream, streamText } from 'ai'; import { MockLanguageModelV2 } from 'ai/test'; async function main() { const result = streamText({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: simulateReadableStream({ chunks: [ { type: 'text-start', id: '0' }, { type: 'text-delta', id: '0', delta: 'こんにちは' }, { type: 'text-delta', id: '0', delta: 'こんにちは' }, { type: 'text-delta', id: '0', delta: 'こんにちは' }, { type: 'text-delta', id: '0', delta: 'こんにちは' }, { type: 'text-end', id: '0' }, { type: 'finish', finishReason: 'stop', logprobs: undefined, usage: { inputTokens: 3, outputTokens: 10, totalTokens: 13, }, }, ], chunkDelayInMs: 400, }), }), }), prompt: 'Say hello in Japanese!', experimental_transform: smoothStream({ chunking: /[\u3040-\u309F\u30A0-\u30FF]|\S+\s+/, }), }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/togetherai-tool-call.ts --- import { togetherai } from '@ai-sdk/togetherai'; import { streamText, ModelMessage, ToolCallPart, ToolResultPart } from 'ai'; import 'dotenv/config'; import { weatherTool } from '../tools/weather-tool'; const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; const result = streamText({ model: togetherai('meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo'), maxOutputTokens: 512, tools: { weather: weatherTool, }, toolChoice: 'required', prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); let fullResponse = ''; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const delta of result.fullStream) { switch (delta.type) { case 'text-delta': { fullResponse += delta.text; process.stdout.write(delta.text); break; } case 'tool-call': { toolCalls.push(delta); process.stdout.write( `\nTool call: '${delta.toolName}' ${JSON.stringify(delta.input)}`, ); break; } case 'tool-result': { if (delta.dynamic) { continue; } const transformedDelta: ToolResultPart = { ...delta, output: { type: 'json', value: delta.output }, }; toolResponses.push(transformedDelta); process.stdout.write( `\nTool response: '${delta.toolName}' ${JSON.stringify( delta.output, )}`, ); break; } } } process.stdout.write('\n\n'); messages.push({ role: 'assistant', content: [{ type: 'text', text: fullResponse }, ...toolCalls], }); if (toolResponses.length > 0) { messages.push({ role: 'tool', content: toolResponses }); } toolResponseAvailable = toolCalls.length > 0; console.log('Messages:', messages[0].content); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/togetherai.ts --- import { togetherai } from '@ai-sdk/togetherai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: togetherai('google/gemma-2-9b-it'), 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); --- File: /ai/examples/ai-core/src/stream-text/vercel-image.ts --- import { vercel } from '@ai-sdk/vercel'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = streamText({ model: vercel('v0-1.0-md'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png') }, ], }, ], onError: error => { console.error(error); }, }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/vercel-tool-call.ts --- import { vercel } from '@ai-sdk/vercel'; import { streamText, ToolCallPart, ToolResultPart, ModelMessage } from 'ai'; import 'dotenv/config'; import { weatherTool } from '../tools/weather-tool'; const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; const result = streamText({ model: vercel('v0-1.0-md'), tools: { weather: weatherTool, }, toolChoice: 'required', prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); let fullResponse = ''; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const chunk of result.fullStream) { switch (chunk.type) { case 'text-delta': { process.stdout.write(chunk.text); break; } case 'tool-call': { console.log( `TOOL CALL ${chunk.toolName} ${JSON.stringify(chunk.input)}`, ); break; } case 'tool-result': { console.log( `TOOL RESULT ${chunk.toolName} ${JSON.stringify(chunk.output)}`, ); break; } case 'finish-step': { console.log(); console.log(); console.log('STEP FINISH'); console.log('Finish reason:', chunk.finishReason); console.log('Usage:', chunk.usage); console.log(); break; } case 'finish': { console.log('FINISH'); console.log('Finish reason:', chunk.finishReason); console.log('Total Usage:', chunk.totalUsage); break; } case 'error': console.error('Error:', chunk.error); break; } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/vercel.ts --- import { vercel } from '@ai-sdk/vercel'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: vercel('v0-1.5-md'), prompt: 'Implement Fibonacci in Lua.', }); 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); --- File: /ai/examples/ai-core/src/stream-text/xai-chatbot.ts --- import { xai } from '@ai-sdk/xai'; import { stepCountIs, ModelMessage, streamText, tool } from 'ai'; import 'dotenv/config'; import * as readline from 'node:readline/promises'; import { z } from 'zod/v4'; const terminal = readline.createInterface({ input: process.stdin, output: process.stdout, }); const messages: ModelMessage[] = []; async function main() { while (true) { const userInput = await terminal.question('You: '); messages.push({ role: 'user', content: userInput }); const result = streamText({ model: xai('grok-3-beta'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z .string() .describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(5), messages, }); process.stdout.write('\nAssistant: '); for await (const delta of result.textStream) { process.stdout.write(delta); } process.stdout.write('\n\n'); messages.push(...(await result.response).messages); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/xai-image.ts --- import { xai } from '@ai-sdk/xai'; import { streamText } from 'ai'; import 'dotenv/config'; import fs from 'node:fs'; async function main() { const result = streamText({ model: xai('grok-2-vision-1212'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Describe the image in detail.' }, { type: 'image', image: fs.readFileSync('./data/comic-cat.png') }, ], }, ], }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/xai-raw-chunks.ts --- import { xai } from '@ai-sdk/xai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: xai('grok-3'), prompt: 'Count from 1 to 3 slowly.', includeRawChunks: true, }); let textChunkCount = 0; let rawChunkCount = 0; for await (const chunk of result.fullStream) { if (chunk.type === 'text-delta') { textChunkCount++; console.log('Text chunk', textChunkCount, ':', chunk.text); } else if (chunk.type === 'raw') { rawChunkCount++; console.log( 'Raw chunk', rawChunkCount, ':', JSON.stringify(chunk.rawValue), ); } } console.log(); console.log('Text chunks:', textChunkCount); console.log('Raw chunks:', rawChunkCount); console.log('Final text:', await result.text); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/xai-search.ts --- import { xai } from '@ai-sdk/xai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: xai('grok-3-latest'), prompt: 'What are the latest posts and activities from @nishimiya? Summarize their recent content and interests.', providerOptions: { xai: { searchParameters: { mode: 'on', returnCitations: true, maxSearchResults: 10, sources: [ { type: 'x', xHandles: ['nishimiya'], }, ], }, }, }, }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log(); console.log('Sources:', await result.sources); console.log('Finish reason:', await result.finishReason); console.log('Usage:', await result.usage); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/xai-tool-call.ts --- import { xai } from '@ai-sdk/xai'; import { streamText, ModelMessage, ToolCallPart, ToolResultPart } from 'ai'; import 'dotenv/config'; import { weatherTool } from '../tools/weather-tool'; const messages: ModelMessage[] = []; async function main() { let toolResponseAvailable = false; const result = streamText({ model: xai('grok-3-beta'), maxOutputTokens: 512, tools: { weather: weatherTool, }, toolChoice: 'required', prompt: 'What is the weather in San Francisco and what attractions should I visit?', }); let fullResponse = ''; const toolCalls: ToolCallPart[] = []; const toolResponses: ToolResultPart[] = []; for await (const delta of result.fullStream) { switch (delta.type) { case 'text-delta': { fullResponse += delta.text; process.stdout.write(delta.text); break; } case 'tool-call': { toolCalls.push(delta); process.stdout.write( `\nTool call: '${delta.toolName}' ${JSON.stringify(delta.input)}`, ); break; } case 'tool-result': { if (delta.dynamic) { continue; } const transformedDelta: ToolResultPart = { ...delta, output: { type: 'json', value: delta.output }, }; toolResponses.push(transformedDelta); process.stdout.write( `\nTool response: '${delta.toolName}' ${JSON.stringify( delta.output, )}`, ); break; } } } process.stdout.write('\n\n'); messages.push({ role: 'assistant', content: [{ type: 'text', text: fullResponse }, ...toolCalls], }); if (toolResponses.length > 0) { messages.push({ role: 'tool', content: toolResponses }); } toolResponseAvailable = toolCalls.length > 0; console.log('Messages:', messages[0].content); } main().catch(console.error); --- File: /ai/examples/ai-core/src/stream-text/xai.ts --- import { xai } from '@ai-sdk/xai'; import { streamText } from 'ai'; import 'dotenv/config'; async function main() { const result = streamText({ model: xai('grok-3-beta'), 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); --- File: /ai/examples/ai-core/src/telemetry/generate-object.ts --- import 'dotenv/config'; import { openai } from '@ai-sdk/openai'; import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; import { NodeSDK } from '@opentelemetry/sdk-node'; import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node'; import { generateObject } from 'ai'; import { z } from 'zod/v4'; const sdk = new NodeSDK({ traceExporter: new ConsoleSpanExporter(), instrumentations: [getNodeAutoInstrumentations()], }); sdk.start(); async function main() { const result = await generateObject({ model: openai('gpt-4o-mini'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', experimental_telemetry: { isEnabled: true, functionId: 'my-awesome-function', metadata: { something: 'custom', someOtherThing: 'other-value', }, }, }); console.log(JSON.stringify(result.object.recipe, null, 2)); await sdk.shutdown(); } main().catch(console.error); --- File: /ai/examples/ai-core/src/telemetry/generate-text-tool-call.ts --- import { openai } from '@ai-sdk/openai'; import { generateText, tool } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; import { weatherTool } from '../tools/weather-tool'; import { NodeSDK } from '@opentelemetry/sdk-node'; import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node'; import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; const sdk = new NodeSDK({ traceExporter: new ConsoleSpanExporter(), instrumentations: [getNodeAutoInstrumentations()], }); sdk.start(); async function main() { const result = await generateText({ model: openai('gpt-3.5-turbo'), maxOutputTokens: 512, tools: { weather: weatherTool, cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What is the weather in San Francisco and what attractions should I visit?', experimental_telemetry: { isEnabled: true, functionId: 'my-awesome-function', metadata: { something: 'custom', someOtherThing: 'other-value', }, }, }); console.log(JSON.stringify(result, null, 2)); await sdk.shutdown(); } main().catch(console.error); --- File: /ai/examples/ai-core/src/telemetry/generate-text.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import 'dotenv/config'; import { NodeSDK } from '@opentelemetry/sdk-node'; import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node'; import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; const sdk = new NodeSDK({ traceExporter: new ConsoleSpanExporter(), instrumentations: [getNodeAutoInstrumentations()], }); sdk.start(); async function main() { const result = await generateText({ model: openai('gpt-4o'), maxOutputTokens: 50, prompt: 'Invent a new holiday and describe its traditions.', experimental_telemetry: { isEnabled: true, functionId: 'my-awesome-function', metadata: { something: 'custom', someOtherThing: 'other-value', }, }, }); console.log(result.text); await sdk.shutdown(); } main().catch(console.error); --- File: /ai/examples/ai-core/src/telemetry/stream-object.ts --- import 'dotenv/config'; import { openai } from '@ai-sdk/openai'; import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; import { NodeSDK } from '@opentelemetry/sdk-node'; import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node'; import { streamObject } from 'ai'; import { z } from 'zod/v4'; const sdk = new NodeSDK({ traceExporter: new ConsoleSpanExporter(), instrumentations: [getNodeAutoInstrumentations()], }); sdk.start(); async function main() { const result = streamObject({ model: openai('gpt-4o-mini'), schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), prompt: 'Generate a lasagna recipe.', experimental_telemetry: { isEnabled: true, functionId: 'my-awesome-function', metadata: { something: 'custom', someOtherThing: 'other-value', }, }, }); for await (const partialObject of result.partialObjectStream) { console.clear(); console.log(partialObject); } await sdk.shutdown(); } main().catch(console.error); --- File: /ai/examples/ai-core/src/telemetry/stream-text.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; import { NodeSDK } from '@opentelemetry/sdk-node'; import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node'; import { streamText } from 'ai'; import 'dotenv/config'; const sdk = new NodeSDK({ traceExporter: new ConsoleSpanExporter(), instrumentations: [getNodeAutoInstrumentations()], }); sdk.start(); async function main() { const result = streamText({ model: anthropic('claude-3-5-sonnet-20240620'), maxOutputTokens: 50, prompt: 'Invent a new holiday and describe its traditions.', experimental_telemetry: { isEnabled: true, functionId: 'my-awesome-function', metadata: { something: 'custom', someOtherThing: 'other-value', }, }, }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } await sdk.shutdown(); } main().catch(console.error); --- File: /ai/examples/ai-core/src/test/response-format.ts --- import { openai } from '@ai-sdk/openai'; import 'dotenv/config'; async function main() { const result = await openai('gpt-4-turbo').doStream({ responseFormat: { type: 'json', schema: { type: 'object', properties: { text: { type: 'string' }, }, required: ['text'], }, }, temperature: 0, prompt: [ { role: 'user', content: [ { type: 'text', text: 'Invent a new holiday and describe its traditions. Output as JSON object.', }, ], }, ], includeRawChunks: false, }); const reader = result.stream.getReader(); while (true) { const { done, value } = await reader.read(); if (done) { break; } if (value.type === 'text-delta') { process.stdout.write(value.delta); } } } main().catch(console.error); --- File: /ai/examples/ai-core/src/tools/weather-tool.ts --- import { tool } from 'ai'; import { z } from 'zod/v4'; export const weatherTool = tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), // location below is inferred to be a string: execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }); --- File: /ai/examples/ai-core/src/transcribe/assemblyai-string.ts --- import { assemblyai } from '@ai-sdk/assemblyai'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: assemblyai.transcription('best'), audio: Buffer.from(await readFile('./data/galileo.mp3')).toString('base64'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/assemblyai-url.ts --- import { assemblyai } from '@ai-sdk/assemblyai'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; async function main() { const result = await transcribe({ model: assemblyai.transcription('best'), audio: new URL( 'https://github.com/vercel/ai/raw/refs/heads/main/examples/ai-core/data/galileo.mp3', ), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/assemblyai.ts --- import { assemblyai } from '@ai-sdk/assemblyai'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: assemblyai.transcription('best'), audio: await readFile('data/galileo.mp3'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/azure-string.ts --- import { azure } from '@ai-sdk/azure'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: azure.transcription('whisper-1'), audio: Buffer.from(await readFile('./data/galileo.mp3')).toString('base64'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/azure-url.ts --- import { azure } from '@ai-sdk/azure'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; async function main() { const result = await transcribe({ model: azure.transcription('whisper-1'), audio: new URL( 'https://github.com/vercel/ai/raw/refs/heads/main/examples/ai-core/data/galileo.mp3', ), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/azure.ts --- import { azure } from '@ai-sdk/azure'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: azure.transcription('whisper-1'), audio: await readFile('data/galileo.mp3'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/deepgram-string.ts --- import { deepgram } from '@ai-sdk/deepgram'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: deepgram.transcription('nova-3'), audio: Buffer.from(await readFile('./data/galileo.mp3')).toString('base64'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/deepgram-url.ts --- import { deepgram } from '@ai-sdk/deepgram'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; async function main() { const result = await transcribe({ model: deepgram.transcription('nova-3'), audio: new URL( 'https://github.com/vercel/ai/raw/refs/heads/main/examples/ai-core/data/galileo.mp3', ), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/deepgram.ts --- import { deepgram } from '@ai-sdk/deepgram'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: deepgram.transcription('nova-3'), audio: await readFile('data/galileo.mp3'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/elevenlabs-string.ts --- import { elevenlabs } from '@ai-sdk/elevenlabs'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: elevenlabs.transcription('scribe_v1'), audio: Buffer.from(await readFile('./data/galileo.mp3')).toString('base64'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/elevenlabs-url.ts --- import { elevenlabs } from '@ai-sdk/elevenlabs'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; async function main() { const result = await transcribe({ model: elevenlabs.transcription('scribe_v1'), audio: new URL( 'https://github.com/vercel/ai/raw/refs/heads/main/examples/ai-core/data/galileo.mp3', ), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/elevenlabs.ts --- import { elevenlabs } from '@ai-sdk/elevenlabs'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: elevenlabs.transcription('scribe_v1'), audio: await readFile('data/galileo.mp3'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/fal-string.ts --- import { fal } from '@ai-sdk/fal'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: fal.transcription('whisper'), audio: Buffer.from(await readFile('./data/galileo.mp3')).toString('base64'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/fal-url.ts --- import { fal } from '@ai-sdk/fal'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; async function main() { const result = await transcribe({ model: fal.transcription('whisper'), audio: new URL( 'https://github.com/vercel/ai/raw/refs/heads/main/examples/ai-core/data/galileo.mp3', ), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/fal.ts --- import { fal } from '@ai-sdk/fal'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: fal.transcription('whisper'), audio: await readFile('data/galileo.mp3'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/gladia-string.ts --- import { gladia } from '@ai-sdk/gladia'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: gladia.transcription(), audio: Buffer.from(await readFile('./data/galileo.mp3')).toString('base64'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/gladia-url.ts --- import { gladia } from '@ai-sdk/gladia'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; async function main() { const result = await transcribe({ model: gladia.transcription(), audio: new URL( 'https://github.com/vercel/ai/raw/refs/heads/main/examples/ai-core/data/galileo.mp3', ), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/gladia.ts --- import { gladia } from '@ai-sdk/gladia'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: gladia.transcription(), audio: await readFile('data/galileo.mp3'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/groq-string.ts --- import { groq } from '@ai-sdk/groq'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: groq.transcription('whisper-large-v3-turbo'), audio: Buffer.from(await readFile('./data/galileo.mp3')).toString('base64'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/groq-url.ts --- import { groq } from '@ai-sdk/groq'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; async function main() { const result = await transcribe({ model: groq.transcription('whisper-large-v3-turbo'), audio: new URL( 'https://github.com/vercel/ai/raw/refs/heads/main/examples/ai-core/data/galileo.mp3', ), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/groq.ts --- import { groq } from '@ai-sdk/groq'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: groq.transcription('whisper-large-v3-turbo'), audio: await readFile('data/galileo.mp3'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/openai-string.ts --- import { openai } from '@ai-sdk/openai'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: openai.transcription('whisper-1'), audio: Buffer.from(await readFile('./data/galileo.mp3')).toString('base64'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/openai-url.ts --- import { openai } from '@ai-sdk/openai'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; async function main() { const result = await transcribe({ model: openai.transcription('whisper-1'), audio: new URL( 'https://github.com/vercel/ai/raw/refs/heads/main/examples/ai-core/data/galileo.mp3', ), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/openai.ts --- import { openai } from '@ai-sdk/openai'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: openai.transcription('whisper-1'), audio: await readFile('data/galileo.mp3'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/revai-string.ts --- import { revai } from '@ai-sdk/revai'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: revai.transcription('machine'), audio: Buffer.from(await readFile('./data/galileo.mp3')).toString('base64'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/revai-url.ts --- import { revai } from '@ai-sdk/revai'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; async function main() { const result = await transcribe({ model: revai.transcription('machine'), audio: new URL( 'https://github.com/vercel/ai/raw/refs/heads/main/examples/ai-core/data/galileo.mp3', ), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); } main().catch(console.error); --- File: /ai/examples/ai-core/src/transcribe/revai.ts --- import { revai } from '@ai-sdk/revai'; import { experimental_transcribe as transcribe } from 'ai'; import 'dotenv/config'; import { readFile } from 'fs/promises'; async function main() { const result = await transcribe({ model: revai.transcription('machine'), audio: await readFile('data/galileo.mp3'), }); console.log('Text:', result.text); console.log('Duration:', result.durationInSeconds); console.log('Language:', result.language); console.log('Segments:', result.segments); console.log('Warnings:', result.warnings); console.log('Responses:', result.responses); console.log('Provider Metadata:', result.providerMetadata); } main().catch(console.error); --- File: /ai/examples/ai-core/src/types/tool-set.ts --- import { openai } from '@ai-sdk/openai'; import { StaticToolCall, StaticToolResult, generateText, tool } from 'ai'; import { z } from 'zod/v4'; const myToolSet = { firstTool: tool({ description: 'Greets the user', inputSchema: z.object({ name: z.string() }), execute: async ({ name }) => `Hello, ${name}!`, }), secondTool: tool({ description: 'Tells the user their age', inputSchema: z.object({ age: z.number() }), execute: async ({ age }) => `You are ${age} years old!`, }), }; type MyToolCall = StaticToolCall<typeof myToolSet>; type MyToolResult = StaticToolResult<typeof myToolSet>; async function generateSomething(prompt: string): Promise<{ text: string; staticToolCalls: Array<MyToolCall>; staticToolResults: Array<MyToolResult>; }> { return generateText({ model: openai('gpt-4o'), tools: myToolSet, prompt, }); } const { text, staticToolCalls, staticToolResults } = await generateSomething('...'); --- File: /ai/examples/ai-core/vitest.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/examples/angular/src/app/chat/chat.component.ts --- import { Component, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, } from '@angular/forms'; import { Chat } from '@ai-sdk/angular'; @Component({ selector: 'app-chat', standalone: true, imports: [CommonModule, ReactiveFormsModule], templateUrl: './chat.component.html', styleUrl: './chat.component.css', }) export class ChatComponent { private fb = inject(FormBuilder); public chat: Chat = new Chat({}); chatForm: FormGroup; constructor() { this.chatForm = this.fb.group({ userInput: ['', Validators.required], }); } sendMessage() { if (this.chatForm.invalid) { return; } const userInput = this.chatForm.value.userInput; this.chatForm.reset(); this.chat.sendMessage( { text: userInput, }, { body: { selectedModel: 'gpt-4.1', }, }, ); } } --- File: /ai/examples/angular/src/app/completion/completion.component.ts --- import { Component } from '@angular/core'; import { Completion } from '@ai-sdk/angular'; import { FormsModule } from '@angular/forms'; @Component({ selector: 'app-completion', standalone: true, imports: [FormsModule], template: ` <div class="container"> <div class="input-section"> <textarea [(ngModel)]="completion.input" name="prompt" placeholder="Enter your prompt..." rows="4" class="prompt-input" ></textarea> <div class="button-group"> <button (click)="completion.complete(completion.input)" [disabled]="completion.loading" class="generate-btn" > {{ completion.loading ? 'Generating...' : 'Generate' }} </button> @if (completion.loading) { <button (click)="completion.stop()" class="stop-btn">Stop</button> } </div> </div> @if (completion.completion) { <div class="result-section"> <h4>Result:</h4> <pre class="result-text">{{ completion.completion }}</pre> </div> } @if (completion.error) { <div class="error">{{ completion.error.message }}</div> } </div> `, styleUrl: './completion.component.css', }) export class CompletionComponent { public completion = new Completion({ api: '/api/completion', streamProtocol: 'text', onFinish: (prompt, completion) => { console.log('Completed:', { prompt, completion }); }, }); } --- File: /ai/examples/angular/src/app/structured-object/structured-object.component.ts --- import { Component } from '@angular/core'; import { StructuredObject } from '@ai-sdk/angular'; import { z } from 'zod'; import { FormsModule } from '@angular/forms'; const schema = z.object({ title: z.string(), summary: z.string(), tags: z.array(z.string()), sentiment: z.enum(['positive', 'negative', 'neutral']), }); @Component({ selector: 'app-structured-object', standalone: true, imports: [FormsModule], template: ` <div> <textarea [(ngModel)]="input" name="content" placeholder="Enter content to analyze..." rows="4" class="prompt-input" > </textarea> <button (click)="analyze()" [disabled]="structuredObject.loading" class="generate-btn" > {{ structuredObject.loading ? 'Analyzing...' : 'Analyze' }} </button> @if (structuredObject.object) { <div class="result-section"> <h4>Analysis:</h4> <div class="result-text"> <div> <strong>Title:</strong> {{ structuredObject.object.title }} </div> <div> <strong>Summary:</strong> {{ structuredObject.object.summary }} </div> <div> <strong>Tags:</strong> {{ structuredObject.object.tags?.join(', ') }} </div> <div> <strong>Sentiment:</strong> {{ structuredObject.object.sentiment }} </div> </div> </div> } @if (structuredObject.error) { <div class="error">{{ structuredObject.error.message }}</div> } </div> `, styleUrl: './structured-object.component.css', }) export class StructuredObjectComponent { input = ''; structuredObject = new StructuredObject({ api: '/api/analyze', schema, onFinish: ({ object, error }) => { if (error) { console.error('Schema validation failed:', error); } else { console.log('Generated object:', object); } }, }); async analyze() { if (!this.input.trim()) return; await this.structuredObject.submit(this.input); } } --- File: /ai/examples/angular/src/app/app.component.ts --- import { Component, signal } from '@angular/core'; import { ChatComponent } from './chat/chat.component'; import { CompletionComponent } from './completion/completion.component'; import { StructuredObjectComponent } from './structured-object/structured-object.component'; type TabType = 'chat' | 'completion' | 'structured-object'; @Component({ selector: 'app-root', standalone: true, imports: [ChatComponent, CompletionComponent, StructuredObjectComponent], template: ` <main class="container"> <nav class="tabs"> <button class="tab" [class.active]="activeTab() === 'chat'" (click)="activeTab.set('chat')" > Chat </button> <button class="tab" [class.active]="activeTab() === 'completion'" (click)="activeTab.set('completion')" > Completion </button> <button class="tab" [class.active]="activeTab() === 'structured-object'" (click)="activeTab.set('structured-object')" > Structured Object </button> </nav> <div class="content"> @switch (activeTab()) { @case ('chat') { <app-chat /> } @case ('completion') { <app-completion /> } @case ('structured-object') { <app-structured-object /> } } </div> </main> `, styles: ` :host { display: block; height: 100vh; } .container { display: flex; flex-direction: column; height: 100%; max-width: 800px; margin: 0 auto; padding: 10rem; box-sizing: border-box; } .tabs { display: flex; border-bottom: 1px solid #e1e5e9; margin-bottom: 2rem; } .tab { background: none; border: none; padding: 12px 24px; font-size: 14px; font-weight: 500; color: #64748b; cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.2s; } .tab:hover { color: #334155; } .tab.active { color: #0066cc; border-bottom-color: #0066cc; } .content { flex: 1; display: flex; flex-direction: column; } `, }) export class AppComponent { activeTab = signal<TabType>('chat'); } --- File: /ai/examples/angular/src/app/app.config.ts --- import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), ], }; --- File: /ai/examples/angular/src/app/app.routes.ts --- import { Routes } from '@angular/router'; export const routes: Routes = []; --- File: /ai/examples/angular/src/main.ts --- import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err)); --- File: /ai/examples/angular/src/server.ts --- import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamObject, streamText } from 'ai'; import 'dotenv/config'; import express, { Request, Response } from 'express'; import z from 'zod'; const app = express(); app.use(express.json({ strict: false })); // Allow primitives (for analyze endpoint) app.post('/api/chat', async (req: Request, res: Response) => { const { messages, selectedModel } = req.body; const result = streamText({ model: openai(selectedModel), messages: convertToModelMessages(messages), }); result.pipeUIMessageStreamToResponse(res); }); app.post('/api/completion', async (req: Request, res: Response) => { const { prompt } = req.body; const result = streamText({ model: openai('gpt-4o'), prompt, }); result.pipeTextStreamToResponse(res); }); app.post('/api/analyze', express.raw(), async (req: Request, res: Response) => { const input = req.body.toString('utf8'); const result = streamObject({ model: openai('gpt-4o'), schema: z.object({ title: z.string(), summary: z.string(), tags: z.array(z.string()), sentiment: z.enum(['positive', 'negative', 'neutral']), }), prompt: `Analyze this content: ${input}`, }); result.pipeTextStreamToResponse(res); }); app.listen(3000, () => { console.log(`Example app listening on port ${3000}`); }); --- File: /ai/examples/express/src/server.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; import express, { Request, Response } from 'express'; const app = express(); app.post('/', async (req: Request, res: Response) => { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.pipeUIMessageStreamToResponse(res); }); app.post('/stream-data', async (req: Request, res: Response) => { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.pipeUIMessageStreamToResponse(res, { onError: error => { // Error messages are masked by default for security reasons. // If you want to expose the error message to the client, you can do so here: return error instanceof Error ? error.message : String(error); }, }); }); app.listen(8080, () => { console.log(`Example app listening on port ${8080}`); }); --- File: /ai/examples/fastify/src/server.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; import Fastify from 'fastify'; const fastify = Fastify({ logger: true }); fastify.post('/', async function (request, reply) { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); // Mark the response as a v1 data stream: reply.header('X-Vercel-AI-Data-Stream', 'v1'); reply.header('Content-Type', 'text/plain; charset=utf-8'); return reply.send(result.toUIMessageStream()); }); fastify.post('/stream-data', async function (request, reply) { // immediately start streaming the response const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); const dataStream = result.toUIMessageStream({ onError: error => { // Error messages are masked by default for security reasons. // If you want to expose the error message to the client, you can do so here: return error instanceof Error ? error.message : String(error); }, }); // Mark the response as a v1 data stream: reply.header('X-Vercel-AI-Data-Stream', 'v1'); reply.header('Content-Type', 'text/plain; charset=utf-8'); return reply.send(dataStream); }); fastify.listen({ port: 8080 }); --- File: /ai/examples/hono/src/hono-streaming.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; import { Hono } from 'hono'; import { serve } from '@hono/node-server'; async function main() { console.log('=== Hono Streaming Example ==='); const app = new Hono(); // Basic UI Message Stream endpoint app.post('/chat', async c => { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); return result.toUIMessageStreamResponse(); }); // Text stream endpoint app.post('/text', async c => { const result = streamText({ model: openai('gpt-4o'), prompt: 'Write a short poem about coding.', }); c.header('Content-Type', 'text/plain; charset=utf-8'); return new Response(result.textStream, { headers: c.res.headers, }); }); app.get('/health', c => c.text('Hono streaming server is running!')); const port = 3001; console.log(`Server starting on http://localhost:${port}`); console.log('Test with: curl -X POST http://localhost:3001/chat'); serve({ fetch: app.fetch, port, }); } main().catch(console.error); --- File: /ai/examples/hono/src/server.ts --- import { openai } from '@ai-sdk/openai'; import { serve } from '@hono/node-server'; import { JsonToSseTransformStream, streamText } from 'ai'; import 'dotenv/config'; import { Hono } from 'hono'; import { stream } from 'hono/streaming'; const app = new Hono(); app.post('/', async c => { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); // Mark the response as a v1 data stream: c.header('X-Vercel-AI-Data-Stream', 'v1'); c.header('Content-Type', 'text/plain; charset=utf-8'); return stream(c, stream => stream.pipe(result.toUIMessageStream())); }); app.post('/stream-data', async c => { // immediately start streaming the response const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); const dataStream = result.toUIMessageStream({ onError: error => { // Error messages are masked by default for security reasons. // If you want to expose the error message to the client, you can do so here: return error instanceof Error ? error.message : String(error); }, }); // Mark the response as a v2 data stream: c.header('content-type', 'text/event-stream'); c.header('cache-control', 'no-cache'); c.header('connection', 'keep-alive'); c.header('x-vercel-ai-data-stream', 'v2'); c.header('x-accel-buffering', 'no'); // disable nginx buffering return stream(c, stream => stream.pipe( dataStream .pipeThrough(new JsonToSseTransformStream()) .pipeThrough(new TextEncoderStream()), ), ); }); serve({ fetch: app.fetch, port: 8080 }); --- File: /ai/examples/mcp/src/http/client.ts --- import { openai } from '@ai-sdk/openai'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { experimental_createMCPClient as createMCPClient, experimental_MCPClient as MCPClient, generateText, stepCountIs, } from 'ai'; import 'dotenv/config'; async function main() { const transport = new StreamableHTTPClientTransport( new URL('http://localhost:3000/mcp'), ); const mcpClient: MCPClient = await createMCPClient({ transport, }); try { const tools = await mcpClient.tools(); const { text: answer } = await generateText({ model: openai('gpt-4o-mini'), tools, stopWhen: stepCountIs(10), onStepFinish: async ({ toolResults }) => { console.log(`STEP RESULTS: ${JSON.stringify(toolResults, null, 2)}`); }, system: 'You are a helpful chatbot', prompt: 'Look up information about user with the ID foo_123', }); console.log(`FINAL ANSWER: ${answer}`); } catch (error) { console.error('Error:', error); } finally { await mcpClient.close(); } } main(); --- File: /ai/examples/mcp/src/http/server.ts --- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import express from 'express'; import { z } from 'zod'; // Stateless Mode: see https://github.com/modelcontextprotocol/typescript-sdk/tree/main/src/examples#stateless-mode for more details const app = express(); app.use(express.json()); app.post('/mcp', async (req, res) => { const server = new McpServer({ name: 'example-http-server', version: '1.0.0', }); server.tool( 'get-user-info', 'Get user info', { userId: z.string(), }, async ({ userId }) => { return { content: [ { type: 'text', text: `Here is information about user ${userId}:`, }, { type: 'text', text: `Name: John Doe`, }, { type: 'text', text: `Email: john.doe@example.com`, }, { type: 'text', text: `Age: 30`, }, ], }; }, ); try { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); await server.connect(transport); await transport.handleRequest(req, res, req.body); res.on('close', () => { transport.close(); server.close(); }); } catch (error) { console.error('Error handling MCP request:', error); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: null, }); } } }); app.get('/mcp', async (_req, res) => { console.log('Received GET MCP request'); res.writeHead(405).end( JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed.', }, id: null, }), ); }); app.delete('/mcp', async (_req, res) => { console.log('Received DELETE MCP request'); res.writeHead(405).end( JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed.', }, id: null, }), ); }); app.listen(3000); process.on('SIGINT', async () => { console.log('Shutting down server...'); process.exit(0); }); --- File: /ai/examples/mcp/src/sse/client.ts --- import { openai } from '@ai-sdk/openai'; import { experimental_createMCPClient, generateText, stepCountIs } from 'ai'; import 'dotenv/config'; async function main() { const mcpClient = await experimental_createMCPClient({ transport: { type: 'sse', url: 'http://localhost:8080/sse', headers: { example: 'header', }, }, }); const tools = await mcpClient.tools(); const { text: answer } = await generateText({ model: openai('gpt-4o-mini'), tools, stopWhen: stepCountIs(10), onStepFinish: async ({ toolResults }) => { console.log(`STEP RESULTS: ${JSON.stringify(toolResults, null, 2)}`); }, system: 'You are a helpful chatbot', prompt: 'List all products, then find availability for Product 1.', }); await mcpClient.close(); console.log(`FINAL ANSWER: ${answer}`); } main().catch(console.error); --- File: /ai/examples/mcp/src/sse/server.ts --- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import 'dotenv/config'; import express from 'express'; import { z } from 'zod'; const mcpServer = new McpServer({ name: 'example-server', version: '1.0.0', }); // Tool with arguments: mcpServer.tool( 'check-product-stock', 'Check if a product is available', { productName: z.string(), }, async ({ productName }) => { return { content: [ { type: 'text', text: `The product ${productName} is available in stock`, }, ], }; }, ); // Tool with zero arguments: mcpServer.tool('list-products', 'List all products', async () => { return { content: [ { type: 'text', text: 'Products: Product 1, Product 2, Product 3' }, ], }; }); let transport: SSEServerTransport; const app = express(); app.get('/sse', async (req, res) => { transport = new SSEServerTransport('/messages', res); await mcpServer.connect(transport); }); app.post('/messages', async (req, res) => { await transport.handlePostMessage(req, res); }); app.listen(8080, () => { console.log(`Example SSE MCP server listening on port ${8080}`); }); --- File: /ai/examples/mcp/src/stdio/client.ts --- import { openai } from '@ai-sdk/openai'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { experimental_createMCPClient, generateText, stepCountIs } from 'ai'; import 'dotenv/config'; import { z } from 'zod/v4'; async function main() { let mcpClient; try { // Or use the AI SDK's stdio transport by importing: // import { Experimental_StdioMCPTransport as StdioClientTransport } from 'ai/mcp-stdio' const stdioTransport = new StdioClientTransport({ command: 'node', args: ['src/stdio/dist/server.js'], env: { FOO: 'bar', }, }); mcpClient = await experimental_createMCPClient({ transport: stdioTransport, }); const { text: answer } = await generateText({ model: openai('gpt-4o-mini'), tools: await mcpClient.tools({ schemas: { 'get-pokemon': { inputSchema: z.object({ name: z.string() }), }, }, }), stopWhen: stepCountIs(10), onStepFinish: async ({ toolResults }) => { console.log(`STEP RESULTS: ${JSON.stringify(toolResults, null, 2)}`); }, system: 'You are an expert in Pokemon', prompt: 'Which Pokemon could best defeat Feebas? Choose one and share details about it.', }); console.log(`FINAL ANSWER: ${answer}`); } finally { await mcpClient?.close(); } } main().catch(console.error); --- File: /ai/examples/mcp/src/stdio/server.ts --- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; const POKE_API_BASE = 'https://pokeapi.co/api/v2'; const server = new McpServer({ name: 'pokemon', version: '1.0.0', }); server.tool( 'get-pokemon', 'Get Pokemon details by name', { name: z.string(), }, async ({ name }) => { const path = `/pokemon/${name.toLowerCase()}`; const pokemon = await makePokeApiRequest<Pokemon>(path); if (!pokemon) { return { content: [ { type: 'text', text: 'Failed to retrieve Pokemon data', }, ], }; } return { content: [ { type: 'text', text: formatPokemonData(pokemon), }, ], }; }, ); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.log('Pokemon MCP Server running on stdio'); } main().catch(error => { console.error('Fatal error in main():', error); process.exit(1); }); interface PokemonAbility { id: string; name: string; } interface Pokemon { id: string; name: string; abilities: { ability: PokemonAbility }[]; } async function makePokeApiRequest<T>(path: string): Promise<T | null> { try { const url = `${POKE_API_BASE}${path}`; const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP Error Status: ${response.status}`); } return (await response.json()) as T; } catch (error) { console.error('[ERROR] Failed to make PokeAPI request:', error); return null; } } function formatPokemonData(pokemon: Pokemon) { return [ `Name: ${pokemon.name}`, `Abilities: ${pokemon.abilities .map(ability => ability.ability.name) .join(', ')}`, ].join('\n'); } --- File: /ai/examples/nest/src/app.controller.ts --- import { openai } from '@ai-sdk/openai'; import { Controller, Post, Res } from '@nestjs/common'; import { streamText } from 'ai'; import { Response } from 'express'; @Controller() export class AppController { @Post('/') async root(@Res() res: Response) { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.pipeUIMessageStreamToResponse(res); } @Post('/stream-data') async streamData(@Res() res: Response) { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.pipeUIMessageStreamToResponse(res, { onError: (error) => { // Error messages are masked by default for security reasons. // If you want to expose the error message to the client, you can do so here: return error instanceof Error ? error.message : String(error); }, }); } } --- File: /ai/examples/nest/src/app.module.ts --- import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; @Module({ imports: [], controllers: [AppController], providers: [], }) export class AppModule {} --- File: /ai/examples/nest/src/main.ts --- import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import 'dotenv/config'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(8080); } bootstrap(); --- File: /ai/examples/nest/.eslintrc.js --- module.exports = { parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', tsconfigRootDir: __dirname, sourceType: 'module', }, plugins: ['@typescript-eslint/eslint-plugin'], extends: [ 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', ], root: true, env: { node: true, jest: true, }, ignorePatterns: ['.eslintrc.js'], rules: { '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', }, }; --- File: /ai/examples/next/app/api/chat/[id]/stream/route.ts --- import { readChat } from '@util/chat-store'; import { UI_MESSAGE_STREAM_HEADERS } from 'ai'; import { after } from 'next/server'; import { createResumableStreamContext } from 'resumable-stream'; export async function GET( request: Request, { params }: { params: Promise<{ id: string }> }, ) { const { id } = await params; const chat = await readChat(id); if (chat.activeStreamId == null) { // no content response when there is no active stream return new Response(null, { status: 204 }); } const streamContext = createResumableStreamContext({ waitUntil: after, }); return new Response( await streamContext.resumeExistingStream(chat.activeStreamId), { headers: UI_MESSAGE_STREAM_HEADERS }, ); } --- File: /ai/examples/next/app/api/chat/route.ts --- import { MyUIMessage } from '@/util/chat-schema'; import { openai } from '@ai-sdk/openai'; import { readChat, saveChat } from '@util/chat-store'; import { convertToModelMessages, generateId, streamText } from 'ai'; import { after } from 'next/server'; import { createResumableStreamContext } from 'resumable-stream'; export async function POST(req: Request) { const { message, id, trigger, messageId, }: { message: MyUIMessage | undefined; id: string; trigger: 'submit-message' | 'regenerate-message'; messageId: string | undefined; } = await req.json(); const chat = await readChat(id); let messages: MyUIMessage[] = chat.messages; if (trigger === 'submit-message') { if (messageId != null) { const messageIndex = messages.findIndex(m => m.id === messageId); if (messageIndex === -1) { throw new Error(`message ${messageId} not found`); } messages = messages.slice(0, messageIndex); messages.push(message!); } else { messages = [...messages, message!]; } } else if (trigger === 'regenerate-message') { const messageIndex = messageId == null ? messages.length - 1 : messages.findIndex(message => message.id === messageId); if (messageIndex === -1) { throw new Error(`message ${messageId} not found`); } // set the messages to the message before the assistant message messages = messages.slice( 0, messages[messageIndex].role === 'assistant' ? messageIndex : messageIndex + 1, ); } // save the user message saveChat({ id, messages, activeStreamId: null }); const result = streamText({ model: openai('gpt-4o-mini'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ originalMessages: messages, generateMessageId: generateId, messageMetadata: ({ part }) => { if (part.type === 'start') { return { createdAt: Date.now() }; } }, onFinish: ({ messages }) => { saveChat({ id, messages, activeStreamId: null }); }, async consumeSseStream({ stream }) { const streamId = generateId(); // send the sse stream into a resumable stream sink as well: const streamContext = createResumableStreamContext({ waitUntil: after }); await streamContext.createNewResumableStream(streamId, () => stream); // update the chat with the streamId saveChat({ id, activeStreamId: streamId }); }, }); } --- File: /ai/examples/next/app/chat/[chatId]/chat-input.tsx --- import { useState } from 'react'; export default function ChatInput({ status, onSubmit, inputRef, }: { status: string; onSubmit: (text: string) => void; inputRef: React.RefObject<HTMLInputElement>; }) { const [text, setText] = useState(''); return ( <form onSubmit={e => { e.preventDefault(); if (text.trim() === '') return; onSubmit(text); setText(''); }} > <input ref={inputRef} className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" placeholder="Say something..." disabled={status !== 'ready'} value={text} onChange={e => setText(e.target.value)} /> </form> ); } --- File: /ai/examples/next/app/chat/[chatId]/chat.tsx --- 'use client'; import { invalidateRouterCache } from '@/app/actions'; import { MyUIMessage } from '@/util/chat-schema'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useEffect, useRef } from 'react'; import ChatInput from './chat-input'; import Message from './message'; export default function ChatComponent({ chatData, isNewChat = false, resume = false, }: { chatData: { id: string; messages: MyUIMessage[] }; isNewChat?: boolean; resume?: boolean; }) { const inputRef = useRef<HTMLInputElement>(null); const { status, sendMessage, messages, regenerate } = useChat({ id: chatData.id, messages: chatData.messages, resume, transport: new DefaultChatTransport({ prepareSendMessagesRequest: ({ id, messages, trigger, messageId }) => { switch (trigger) { case 'regenerate-message': // omit messages data transfer, only send the messageId: return { body: { trigger: 'regenerate-message', id, messageId, }, }; case 'submit-message': // only send the last message to the server to limit the request size: return { body: { trigger: 'submit-message', id, message: messages[messages.length - 1], messageId, }, }; } }, }), onFinish() { // for new chats, the router cache needs to be invalidated so // navigation to the previous page triggers SSR correctly if (isNewChat) { invalidateRouterCache(); } // focus the input field again after the response is finished requestAnimationFrame(() => { inputRef.current?.focus(); }); }, }); // activate the input field useEffect(() => { inputRef.current?.focus(); }, []); return ( <div className="flex flex-col py-24 mx-auto w-full max-w-md stretch"> {messages.map(message => ( <Message key={message.id} message={message} regenerate={regenerate} sendMessage={sendMessage} status={status} /> ))} <ChatInput status={status} onSubmit={text => { sendMessage({ text, metadata: { createdAt: Date.now() } }); if (isNewChat) { window.history.pushState(null, '', `/chat/${chatData.id}`); } }} inputRef={inputRef} /> </div> ); } --- File: /ai/examples/next/app/chat/[chatId]/message.tsx --- import { MyUIMessage } from '@/util/chat-schema'; import { ChatStatus } from 'ai'; export default function Message({ message, status, regenerate, sendMessage, }: { status: ChatStatus; message: MyUIMessage; regenerate: ({ messageId }: { messageId: string }) => void; sendMessage: ({ text, messageId, }: { text: string; messageId?: string; }) => void; }) { const date = message.metadata?.createdAt ? new Date(message.metadata.createdAt).toLocaleString() : ''; const isUser = message.role === 'user'; return ( <div className={`whitespace-pre-wrap my-2 p-3 rounded-lg shadow ${isUser ? 'bg-blue-100 text-right ml-10' : 'bg-gray-100 text-left mr-10'}`} > <div className="mb-1 text-xs text-gray-500">{date}</div> <div className="font-semibold">{isUser ? 'User:' : 'AI:'}</div> <div> {message.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> {message.role === 'user' && ( <> <button onClick={() => regenerate({ messageId: message.id })} className="px-3 py-1 mt-2 text-sm transition-colors bg-gray-200 rounded-md hover:bg-gray-300" disabled={status !== 'ready'} > Regenerate </button> <button onClick={() => sendMessage({ text: 'Hello', messageId: message.id }) } className="px-3 py-1 mt-2 text-sm transition-colors bg-gray-200 rounded-md hover:bg-gray-300" disabled={status !== 'ready'} > Replace with Hello </button> </> )} </div> ); } --- File: /ai/examples/next/app/chat/[chatId]/page.tsx --- import { readAllChats, readChat } from '@util/chat-store'; import Link from 'next/link'; import Chat from './chat'; export default async function Page(props: { params: Promise<{ chatId: string }>; }) { const { chatId } = await props.params; // get the chat ID from the URL const chatData = await readChat(chatId); // load the chat const chats = await readAllChats(); // load all chats // filter to 5 most recent chats const recentChats = chats .sort((a, b) => b.createdAt - a.createdAt) .slice(0, 5); return ( <div> <ul> {recentChats.map(chat => ( <li key={chat.id}> <Link href={`/chat/${chat.id}`}>{chat.id}</Link> </li> ))} </ul> <Chat chatData={chatData} resume={chatData.activeStreamId !== null} />; </div> ); } --- File: /ai/examples/next/app/actions.ts --- 'use server'; import { revalidatePath } from 'next/cache'; export async function invalidateRouterCache() { /* * note: this path does not exist, but it will * trigger a client-side reload. */ revalidatePath('/just-trigger-client-reload'); await Promise.resolve(); } --- File: /ai/examples/next/app/layout.tsx --- import './globals.css'; export const metadata = { title: 'AI SDK - Next.js OpenAI Examples', description: 'Examples of using the AI SDK with Next.js and OpenAI.', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body>{children}</body> </html> ); } --- File: /ai/examples/next/app/page.tsx --- import { generateId } from 'ai'; import Chat from './chat/[chatId]/chat'; export default async function ChatPage() { return <Chat chatData={{ id: generateId(), messages: [] }} isNewChat />; } --- File: /ai/examples/next/util/chat-schema.ts --- import { UIDataTypes, UIMessage } from 'ai'; import { z } from 'zod/v4'; export const myMessageMetadataSchema = z.object({ createdAt: z.number(), }); export type MyMessageMetadata = z.infer<typeof myMessageMetadataSchema>; export type MyUIMessage = UIMessage<MyMessageMetadata, UIDataTypes>; export type ChatData = { id: string; messages: MyUIMessage[]; createdAt: number; activeStreamId: string | null; }; --- File: /ai/examples/next/util/chat-store.ts --- import { generateId } from 'ai'; import { existsSync, mkdirSync } from 'fs'; import { readdir, readFile, writeFile } from 'fs/promises'; import path from 'path'; import { ChatData, MyUIMessage } from './chat-schema'; // example implementation for demo purposes // in a real app, you would save the chat to a database // and use the id from the database entry export async function createChat(): Promise<string> { const id = generateId(); getChatFile(id); return id; } export async function saveChat({ id, activeStreamId, messages, }: { id: string; activeStreamId?: string | null; messages?: MyUIMessage[]; }): Promise<void> { const chat = await readChat(id); if (messages !== undefined) { chat.messages = messages; } if (activeStreamId !== undefined) { chat.activeStreamId = activeStreamId; } writeChat(chat); } export async function appendMessageToChat({ id, message, }: { id: string; message: MyUIMessage; }): Promise<void> { const chat = await readChat(id); chat.messages.push(message); writeChat(chat); } async function writeChat(chat: ChatData) { await writeFile(await getChatFile(chat.id), JSON.stringify(chat, null, 2)); } // TODO return null if the chat does not exist export async function readChat(id: string): Promise<ChatData> { return JSON.parse(await readFile(await getChatFile(id), 'utf8')); } export async function readAllChats(): Promise<ChatData[]> { const chatDir = path.join(process.cwd(), '.chats'); const files = await readdir(chatDir, { withFileTypes: true }); return Promise.all( files .filter(file => file.isFile()) .map(async file => readChat(file.name.replace('.json', ''))), ); } async function getChatFile(id: string): Promise<string> { const chatDir = path.join(process.cwd(), '.chats'); if (!existsSync(chatDir)) mkdirSync(chatDir, { recursive: true }); const chatFile = path.join(chatDir, `${id}.json`); if (!existsSync(chatFile)) { await writeFile( chatFile, JSON.stringify({ id, messages: [], createdAt: Date.now() }, null, 2), ); } return chatFile; } --- File: /ai/examples/next/next.config.js --- /** @type {import('next').NextConfig} */ const nextConfig = {}; module.exports = nextConfig; --- File: /ai/examples/next/postcss.config.js --- module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; --- File: /ai/examples/next/tailwind.config.js --- /** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', }, }, }, plugins: [], }; --- File: /ai/examples/next-fastapi/app/(examples)/01-chat-text/layout.tsx --- import { Metadata } from 'next'; export const metadata: Metadata = { title: 'useChat', }; export default function Layout({ children }: { children: React.ReactNode }) { return <>{children}</>; } --- File: /ai/examples/next-fastapi/app/(examples)/01-chat-text/page.tsx --- 'use client'; import { Card } from '@/app/components'; import { useChat } from '@ai-sdk/react'; import { TextStreamChatTransport } from 'ai'; import { useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage, status } = useChat({ transport: new TextStreamChatTransport({ api: '/api/chat?protocol=text', }), }); return ( <div className="flex flex-col gap-2"> <div className="flex flex-col gap-2 p-4"> {messages.map(message => ( <div key={message.id} className="flex flex-row gap-2"> <div className="flex-shrink-0 w-24 text-zinc-500">{`${message.role}: `}</div> <div className="flex flex-col gap-2"> {message.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> </div> ))} </div> {messages.length === 0 && <Card type="chat-text" />} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} className="fixed bottom-0 flex flex-col w-full border-t" > <input value={input} placeholder="Why is the sky blue?" onChange={e => setInput(e.target.value)} className="w-full p-4 bg-transparent outline-none" disabled={status !== 'ready'} /> </form> </div> ); } --- File: /ai/examples/next-fastapi/app/(examples)/02-chat-data/layout.tsx --- import { Metadata } from 'next'; export const metadata: Metadata = { title: 'useChat', }; export default function Layout({ children }: { children: React.ReactNode }) { return <>{children}</>; } --- File: /ai/examples/next-fastapi/app/(examples)/02-chat-data/page.tsx --- 'use client'; import { Card } from '@/app/components'; import { useChat } from '@ai-sdk/react'; import { getToolName, isToolUIPart } from 'ai'; import { GeistMono } from 'geist/font/mono'; import { useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage, status } = useChat(); return ( <div className="flex flex-col gap-2"> <div className="flex flex-col gap-2 p-4"> {messages.map(message => ( <div key={message.id} className="flex flex-row gap-2"> <div className="flex-shrink-0 w-24 text-zinc-500">{`${message.role}: `}</div> <div className="flex flex-col gap-2"> {message.parts.map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } else if (isToolUIPart(part)) { return ( <div key={index} className={`${GeistMono.className} text-sm text-zinc-500 bg-zinc-100 p-3 rounded-lg`} > {`${getToolName(part)}(${JSON.stringify( part.input, null, 2, )})`} </div> ); } })} </div> </div> ))} </div> {messages.length === 0 && <Card type="chat-data" />} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} className="fixed bottom-0 flex flex-col w-full border-t" > <input value={input} placeholder="What's the weather in San Francisco?" onChange={e => setInput(e.target.value)} className="w-full p-4 bg-transparent outline-none" disabled={status !== 'ready'} /> </form> </div> ); } --- File: /ai/examples/next-fastapi/app/(examples)/03-chat-attachments/layout.tsx --- import { Metadata } from 'next'; export const metadata: Metadata = { title: 'useChat with attachments', }; export default function Layout({ children }: { children: React.ReactNode }) { return <>{children}</>; } --- File: /ai/examples/next-fastapi/app/(examples)/03-chat-attachments/page.tsx --- 'use client'; import { Card } from '@/app/components'; /* eslint-disable @next/next/no-img-element */ import { useChat } from '@ai-sdk/react'; import { useRef, useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage, status } = useChat(); const [files, setFiles] = useState<FileList | undefined>(undefined); const fileInputRef = useRef<HTMLInputElement>(null); return ( <div className="flex flex-col gap-2"> <div className="flex flex-col gap-2 p-4"> {messages.map(message => ( <div key={message.id} className="flex flex-row gap-2"> <div className="flex-shrink-0 w-24 text-zinc-500">{`${message.role}: `}</div> <div className="flex flex-col gap-2"> {message.parts.map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } if ( part.type === 'file' && part.mediaType?.startsWith('image/') ) { return ( <div key={index}> <img className="rounded-md w-60" src={part.url} /> </div> ); } })} </div> </div> ))} </div> {messages.length === 0 && <Card type="chat-attachments" />} <form onSubmit={event => { sendMessage({ text: input, files }); setInput(''); setFiles(undefined); if (fileInputRef.current) { fileInputRef.current.value = ''; } }} className="fixed bottom-0 flex flex-col w-full gap-3 p-4 border-t h-28" > <div className="fixed flex flex-row items-end gap-2 right-8 bottom-32"> {files ? Array.from(files).map(attachment => { const { type } = attachment; if (type.startsWith('image/')) { return ( <div key={attachment.name}> <img className="w-24 rounded-md" src={URL.createObjectURL(attachment)} alt={attachment.name} /> <span className="text-sm text-zinc-500"> {attachment.name} </span> </div> ); } else if (type.startsWith('text/')) { return ( <div key={attachment.name} className="flex flex-col flex-shrink-0 w-24 gap-1 text-sm text-zinc-500" > <div className="w-16 h-20 rounded-md bg-zinc-100" /> {attachment.name} </div> ); } }) : ''} </div> <input type="file" onChange={event => { if (event.target.files) { setFiles(event.target.files); } }} multiple ref={fileInputRef} /> <input value={input} placeholder="What's the weather in San Francisco?" onChange={e => setInput(e.target.value)} className="w-full bg-transparent outline-none" disabled={status !== 'ready'} /> </form> </div> ); } --- File: /ai/examples/next-fastapi/app/components.tsx --- import { GeistMono } from 'geist/font/mono'; import Link from 'next/link'; import { ReactNode } from 'react'; const Code = ({ children }: { children: ReactNode }) => { return ( <code className={`${GeistMono.className} text-xs bg-zinc-100 p-1 rounded-md border`} > {children} </code> ); }; export const Card = ({ type }: { type: string }) => { return type === 'chat-text' ? ( <div className="self-center w-full fixed bottom-20 px-8 py-6"> <div className="p-4 border rounded-lg flex flex-col gap-2 w-full"> <div className="text font-semibold text-zinc-800"> Stream Chat Completions </div> <div className="text-zinc-500 text-sm leading-6 flex flex-col gap-4"> <p> The <Code>useChat</Code> hook can be integrated with a Python FastAPI backend to stream chat completions in real-time. The most basic setup involves streaming plain text chunks by setting the{' '} <Code>streamProtocol</Code> to <Code>text</Code>. </p> <p> To make your responses streamable, you will have to use the{' '} <Code>StreamingResponse</Code> class provided by FastAPI. </p> </div> </div> </div> ) : type === 'chat-data' ? ( <div className="self-center w-full fixed bottom-20 px-8 py-6"> <div className="p-4 border rounded-lg flex flex-col gap-2 w-full"> <div className="text font-semibold text-zinc-800"> Stream Chat Completions with Tools </div> <div className="text-zinc-500 text-sm leading-6 flex flex-col gap-4"> <p> The <Code>useChat</Code> hook can be integrated with a Python FastAPI backend to stream chat completions in real-time. However, the most basic setup that involves streaming plain text chunks by setting the <Code>streamProtocol</Code> to <Code>text</Code> is limited. </p> <p> As a result, setting the streamProtocol to <Code>data</Code> allows you to stream chunks that include information about tool calls and results. </p> <p> To make your responses streamable, you will have to use the{' '} <Code>StreamingResponse</Code> class provided by FastAPI. You will also have to ensure that your chunks follow the{' '} <Link target="_blank" className="text-blue-500 hover:underline" href="https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol#data-stream-protocol" > data stream protocol </Link>{' '} and that the response has <Code>x-vercel-ai-data-stream</Code>{' '} header set to <Code>v1</Code>. </p> </div> </div> </div> ) : type === 'chat-attachments' ? ( <div className="self-center w-full fixed top-14 px-8 py-6"> <div className="p-4 border rounded-lg flex flex-col gap-2 w-full"> <div className="text font-semibold text-zinc-800"> Stream Chat Completions with Attachments </div> <div className="text-zinc-500 text-sm leading-6 flex flex-col gap-4"> <p> The <Code>useChat</Code> hook can be integrated with a Python FastAPI backend to stream chat completions in real-time. To make your responses streamable, you will have to use the{' '} <Code>StreamingResponse</Code> class provided by FastAPI. </p> <p> Furthermore, you can send files along with your messages by setting{' '} <Code>experimental_attachments</Code> to <Code>true</Code> in{' '} <Code>handleSubmit</Code>. This will allow you to use process these attachments in your FastAPI backend. </p> </div> </div> </div> ) : null; }; --- File: /ai/examples/next-fastapi/app/icons.tsx --- export const LogoPython = () => ( <svg height={20} strokeLinejoin="round" viewBox="0 0 16 16" width={20} style={{ color: 'currentcolor' }} > <path d="M7.90474 0.00013087C7.24499 0.00316291 6.61494 0.0588153 6.06057 0.15584C4.42745 0.441207 4.13094 1.0385 4.13094 2.14002V3.59479H7.9902V4.07971H4.13094H2.68259C1.56099 4.07971 0.578874 4.7465 0.271682 6.01496C-0.0826597 7.4689 -0.0983767 8.37619 0.271682 9.89434C0.546012 11.0244 1.20115 11.8296 2.32276 11.8296H3.64966V10.0856C3.64966 8.82574 4.75179 7.71441 6.06057 7.71441H9.91533C10.9884 7.71441 11.845 6.84056 11.845 5.77472V2.14002C11.845 1.10556 10.9626 0.328487 9.91533 0.15584C9.25237 0.046687 8.56448 -0.00290121 7.90474 0.00013087ZM5.81768 1.17017C6.21631 1.17017 6.54185 1.49742 6.54185 1.89978C6.54185 2.30072 6.21631 2.62494 5.81768 2.62494C5.41761 2.62494 5.09351 2.30072 5.09351 1.89978C5.09351 1.49742 5.41761 1.17017 5.81768 1.17017Z" fill="url(#paint0_linear_872_3163)" ></path> <path d="M12.3262 4.07971V5.77472C12.3262 7.08883 11.1997 8.19488 9.91525 8.19488H6.06049C5.0046 8.19488 4.13086 9.0887 4.13086 10.1346V13.7693C4.13086 14.8037 5.04033 15.4122 6.06049 15.709C7.28211 16.0642 8.45359 16.1285 9.91525 15.709C10.8868 15.4307 11.8449 14.8708 11.8449 13.7693V12.3145H7.99012V11.8296H11.8449H13.7745C14.8961 11.8296 15.3141 11.0558 15.7041 9.89434C16.1071 8.69865 16.0899 7.5488 15.7041 6.01495C15.4269 4.91058 14.8975 4.07971 13.7745 4.07971H12.3262ZM10.1581 13.2843C10.5582 13.2843 10.8823 13.6086 10.8823 14.0095C10.8823 14.4119 10.5582 14.7391 10.1581 14.7391C9.7595 14.7391 9.43397 14.4119 9.43397 14.0095C9.43397 13.6086 9.7595 13.2843 10.1581 13.2843Z" fill="url(#paint1_linear_872_3163)" ></path> <defs> <linearGradient id="paint0_linear_872_3163" x1="-4.80577e-08" y1="-4.12903e-08" x2="8.81665" y2="7.59598" gradientUnits="userSpaceOnUse" > <stop stopColor="#5A9FD4"></stop> <stop offset="1" stopColor="#306998"></stop> </linearGradient> <linearGradient id="paint1_linear_872_3163" x1="10.0654" y1="13.8872" x2="6.91907" y2="9.42956" gradientUnits="userSpaceOnUse" > <stop stopColor="#FFD43B"></stop> <stop offset="1" stopColor="#FFE873"></stop> </linearGradient> </defs> </svg> ); export const LogoNext = () => ( <svg height={20} strokeLinejoin="round" viewBox="0 0 16 16" width={20} style={{ color: 'currentcolor' }} > <g clipPath="url(#clip0_53_108)"> <circle cx="8" cy="8" r="7.375" fill="black" stroke="var(--ds-gray-1000)" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" ></circle> <path d="M10.63 11V5" stroke="url(#paint0_linear_53_1080o22379mo)" strokeWidth="1.25" strokeMiterlimit="1.41421" ></path> <path fillRule="evenodd" clipRule="evenodd" d="M5.995 5.00087V5H4.745V11H5.995V6.96798L12.3615 14.7076C12.712 14.4793 13.0434 14.2242 13.353 13.9453L5.99527 5.00065L5.995 5.00087Z" fill="url(#paint1_linear_53_1080o22379mo)" ></path> </g> <defs> <linearGradient id="paint0_linear_53_1080o22379mo" x1="11.13" y1="5" x2="11.13" y2="11" gradientUnits="userSpaceOnUse" > <stop stopColor="white"></stop> <stop offset="0.609375" stopColor="white" stopOpacity="0.57"></stop> <stop offset="0.796875" stopColor="white" stopOpacity="0"></stop> <stop offset="1" stopColor="white" stopOpacity="0"></stop> </linearGradient> <linearGradient id="paint1_linear_53_1080o22379mo" x1="9.9375" y1="9.0625" x2="13.5574" y2="13.3992" gradientUnits="userSpaceOnUse" > <stop stopColor="white"></stop> <stop offset="1" stopColor="white" stopOpacity="0"></stop> </linearGradient> <clipPath id="clip0_53_108"> <rect width="16" height="16" fill="red"></rect> </clipPath> </defs> </svg> ); --- File: /ai/examples/next-fastapi/app/layout.tsx --- import './globals.css'; import { LogoNext, LogoPython } from './icons'; import Link from 'next/link'; import { GeistSans } from 'geist/font/sans'; import { Metadata } from 'next'; export const metadata: Metadata = { title: 'AI SDK and FastAPI Examples', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body className={GeistSans.className}> <Link href="/"> <div className="border-b p-4 flex flex-row gap-2"> <LogoNext /> <div className="text-sm text-zinc-500">+</div> <LogoPython /> </div> </Link> {children} </body> </html> ); } --- File: /ai/examples/next-fastapi/app/page.tsx --- import Link from 'next/link'; const examples = [ { title: 'useChat', link: '/01-chat-text', }, { title: 'useChat with tools', link: '/02-chat-data', }, { title: 'useChat with attachments', link: '/03-chat-attachments', }, ]; export default function Home() { return ( <main className="flex flex-col gap-2 p-4"> {examples.map((example, index) => ( <Link key={example.link} className="flex flex-row" href={example.link}> <div className="w-8 text-zinc-400">{index + 1}.</div> <div className="hover:underline">{example.title}</div> </Link> ))} </main> ); } --- File: /ai/examples/next-fastapi/next.config.js --- /** @type {import('next').NextConfig} */ const nextConfig = { rewrites: async () => { return [ { source: '/api/:path*', destination: process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:8000/api/:path*' : '/api/', }, { source: '/docs', destination: process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:8000/docs' : '/api/docs', }, { source: '/openapi.json', destination: process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:8000/openapi.json' : '/api/openapi.json', }, ]; }, }; module.exports = nextConfig; --- File: /ai/examples/next-fastapi/postcss.config.js --- module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; --- File: /ai/examples/next-fastapi/tailwind.config.js --- /** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', }, }, }, plugins: [], }; --- File: /ai/examples/next-google-vertex/app/api/generate-edge/route.ts --- export const runtime = 'edge'; import { generateText } from 'ai'; import { vertex } from '@ai-sdk/google-vertex/edge'; export async function GET() { const model = vertex('gemini-1.5-flash'); const { text } = await generateText({ model, prompt: 'tell me a story', }); return Response.json({ message: text }); } --- File: /ai/examples/next-google-vertex/app/api/generate-node/route.ts --- import { vertex } from '@ai-sdk/google-vertex'; import { generateText } from 'ai'; export async function GET() { const model = vertex('gemini-1.5-flash'); const { text } = await generateText({ model, prompt: 'tell me a story', }); return Response.json({ message: text }); } --- File: /ai/examples/next-google-vertex/app/edge/page.tsx --- 'use client'; import { useState } from 'react'; export default function Home() { const [result, setResult] = useState<string>(''); const [loading, setLoading] = useState(false); const generateText = async () => { setLoading(true); try { const response = await fetch('/api/generate-edge'); const data = await response.json(); setResult(data.message); } catch (error) { console.error('Error:', error); setResult('Failed to generate text'); } finally { setLoading(false); } }; return ( <div className="min-h-screen p-8"> <main className="flex flex-col gap-4 items-center"> <h1 className="text-xl font-medium text-gray-700"> Demo text generation with Google Vertex using Edge-compatible authentication </h1> <button onClick={generateText} disabled={loading} className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-400" > {loading ? 'Generating...' : 'Generate Text'} </button> {result && ( <div className="mt-4 p-4 border rounded max-w-2xl">{result}</div> )} </main> </div> ); } --- File: /ai/examples/next-google-vertex/app/node/page.tsx --- 'use client'; import { useState } from 'react'; export default function Home() { const [result, setResult] = useState<string>(''); const [loading, setLoading] = useState(false); const generateText = async () => { setLoading(true); try { const response = await fetch('/api/generate-node'); const data = await response.json(); setResult(data.message); } catch (error) { console.error('Error:', error); setResult('Failed to generate text'); } finally { setLoading(false); } }; return ( <div className="min-h-screen p-8"> <main className="flex flex-col gap-4 items-center"> <h1 className="text-xl font-medium text-gray-700"> Demo text generation with Google Vertex using google-auth-library authentication </h1> <button onClick={generateText} disabled={loading} className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-400" > {loading ? 'Generating...' : 'Generate Text'} </button> {result && ( <div className="mt-4 p-4 border rounded max-w-2xl">{result}</div> )} </main> </div> ); } --- File: /ai/examples/next-google-vertex/app/layout.tsx --- import type { Metadata } from 'next'; import { GeistSans, GeistMono } from 'geist/font'; import './globals.css'; export const metadata: Metadata = { title: 'Create Next App', description: 'Generated by create next app', }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <html lang="en"> <body className={`${GeistSans.variable} ${GeistMono.variable} antialiased`} > {children} </body> </html> ); } --- File: /ai/examples/next-google-vertex/app/page.tsx --- import Link from 'next/link'; export default function Home() { return ( <main className="flex min-h-screen flex-col items-center justify-center p-24"> <h1 className="text-4xl font-bold mb-8">Google Vertex AI Demo</h1> <div className="flex gap-4"> <Link href="/edge" className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors" > Edge Runtime Demo </Link> <Link href="/node" className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors" > Node Runtime Demo </Link> </div> </main> ); } --- File: /ai/examples/next-google-vertex/next.config.js --- /** @type {import('next').NextConfig} */ const nextConfig = {}; module.exports = nextConfig; --- File: /ai/examples/next-google-vertex/tailwind.config.ts --- import type { Config } from 'tailwindcss'; export default { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { colors: { background: 'var(--background)', foreground: 'var(--foreground)', }, }, }, plugins: [], } satisfies Config; --- File: /ai/examples/next-langchain/app/api/chat/route.ts --- import { toUIMessageStream } from '@ai-sdk/langchain'; import { AIMessage, HumanMessage } from '@langchain/core/messages'; import { ChatOpenAI } from '@langchain/openai'; import { createUIMessageStreamResponse, UIMessage } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages, }: { messages: UIMessage[]; } = await req.json(); const model = new ChatOpenAI({ model: 'gpt-3.5-turbo-0125', temperature: 0, }); const stream = await model.stream( messages.map(message => message.role == 'user' ? new HumanMessage( message.parts .map(part => (part.type === 'text' ? part.text : '')) .join(''), ) : new AIMessage( message.parts .map(part => (part.type === 'text' ? part.text : '')) .join(''), ), ), ); return createUIMessageStreamResponse({ stream: toUIMessageStream(stream), }); } --- File: /ai/examples/next-langchain/app/api/completion/route.ts --- import { toUIMessageStream } from '@ai-sdk/langchain'; import { ChatOpenAI } from '@langchain/openai'; import { createUIMessageStreamResponse } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { prompt } = await req.json(); const model = new ChatOpenAI({ model: 'gpt-3.5-turbo-0125', temperature: 0, }); const stream = await model.stream(prompt); return createUIMessageStreamResponse({ stream: toUIMessageStream(stream), }); } --- File: /ai/examples/next-langchain/app/api/completion-string-output-parser/route.ts --- import { toUIMessageStream } from '@ai-sdk/langchain'; import { StringOutputParser } from '@langchain/core/output_parsers'; import { ChatOpenAI } from '@langchain/openai'; import { createUIMessageStreamResponse } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { prompt } = await req.json(); const model = new ChatOpenAI({ model: 'gpt-3.5-turbo-0125', temperature: 0, }); const parser = new StringOutputParser(); const stream = await model.pipe(parser).stream(prompt); return createUIMessageStreamResponse({ stream: toUIMessageStream(stream), }); } --- File: /ai/examples/next-langchain/app/completion/page.tsx --- 'use client'; import { useCompletion } from '@ai-sdk/react'; export default function Chat() { const { completion, input, handleInputChange, handleSubmit, error } = useCompletion({ api: '/api/completion-string-output-parser', }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <h4 className="pb-4 text-xl font-bold text-gray-900 md:text-xl"> useCompletion Example </h4> {error && ( <div className="fixed top-0 left-0 w-full p-4 text-center text-white bg-red-500"> {error.message} </div> )} {completion} <form onSubmit={handleSubmit}> <input className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" value={input} placeholder="Say something..." onChange={handleInputChange} /> </form> </div> ); } --- File: /ai/examples/next-langchain/app/completion-string-output-parser/page.tsx --- 'use client'; import { useCompletion } from '@ai-sdk/react'; export default function Chat() { const { completion, input, handleInputChange, handleSubmit, error } = useCompletion(); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <h4 className="pb-4 text-xl font-bold text-gray-900 md:text-xl"> useCompletion Example </h4> {error && ( <div className="fixed top-0 left-0 w-full p-4 text-center text-white bg-red-500"> {error.message} </div> )} {completion} <form onSubmit={handleSubmit}> <input className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" value={input} placeholder="Say something..." onChange={handleInputChange} /> </form> </div> ); } --- File: /ai/examples/next-langchain/app/layout.tsx --- import './globals.css'; export const metadata = { title: 'Create Next App', description: 'Generated by create next app', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body>{children}</body> </html> ); } --- File: /ai/examples/next-langchain/app/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat(); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.length > 0 ? messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> )) : null} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.target.value)} /> </form> </div> ); } --- File: /ai/examples/next-langchain/next.config.js --- /** @type {import('next').NextConfig} */ const nextConfig = {}; module.exports = nextConfig; --- File: /ai/examples/next-langchain/postcss.config.js --- module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; --- File: /ai/examples/next-langchain/tailwind.config.js --- /** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', }, }, }, plugins: [], }; --- File: /ai/examples/next-openai/app/api/bedrock/route.ts --- import { bedrock } from '@ai-sdk/amazon-bedrock'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; export async function POST(req: Request) { try { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), prompt: convertToModelMessages(messages), maxOutputTokens: 500, temperature: 0.7, }); return result.toUIMessageStreamResponse(); } catch (error) { console.error('Bedrock API Error:', error); return new Response( JSON.stringify({ error: 'Bedrock API failed', details: error instanceof Error ? error.message : 'Unknown error', }), { status: 500, headers: { 'Content-Type': 'application/json' }, }, ); } } --- File: /ai/examples/next-openai/app/api/chat/route.ts --- import { openai } from '@ai-sdk/openai'; import { consumeStream, convertToModelMessages, streamText, UIMessage, } from 'ai'; export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const prompt = convertToModelMessages(messages); const result = streamText({ model: openai('gpt-4o'), prompt, abortSignal: req.signal, }); return result.toUIMessageStreamResponse({ onFinish: async ({ isAborted }) => { if (isAborted) { console.log('Aborted'); } }, consumeSseStream: consumeStream, // needed for correct abort handling }); } --- File: /ai/examples/next-openai/app/api/chat-cohere/route.ts --- import { cohere } from '@ai-sdk/cohere'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const prompt = convertToModelMessages(messages); const result = streamText({ model: cohere('command-r-plus'), prompt, }); return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai/app/api/chat-google/route.ts --- import { google } from '@ai-sdk/google'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const prompt = convertToModelMessages(messages); const result = streamText({ model: google('gemini-2.0-flash'), prompt, }); return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai/app/api/chat-groq/route.ts --- import { groq } from '@ai-sdk/groq'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const prompt = convertToModelMessages(messages); const result = streamText({ model: groq('llama-3.3-70b-versatile'), prompt, }); return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai/app/api/chat-mistral/route.ts --- import { mistral } from '@ai-sdk/mistral'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const prompt = convertToModelMessages(messages); const result = streamText({ model: mistral('mistral-small-latest'), prompt, }); return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai/app/api/chat-openai-file-search/route.ts --- import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, InferUITool, streamText, UIDataTypes, UIMessage, } from 'ai'; export const maxDuration = 30; export type OpenAIFileSearchMessage = UIMessage< never, UIDataTypes, { file_search: InferUITool<ReturnType<typeof openai.tools.fileSearch>>; } >; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai.responses('gpt-4o-mini'), tools: { file_search: openai.tools.fileSearch({ maxNumResults: 10, ranking: { ranker: 'auto', }, // vectorStoreIds: ['vs_123'], // optional: specify vector store IDs // filters: { key: 'category', type: 'eq', value: 'technical' }, // optional: filter results }), }, messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ sendSources: true, }); } --- File: /ai/examples/next-openai/app/api/chat-openai-responses/route.ts --- import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const prompt = convertToModelMessages(messages); const result = streamText({ model: openai.responses('o3-mini'), prompt, providerOptions: { openai: { reasoningEffort: 'low', reasoningSummary: 'auto', }, }, }); return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai/app/api/chat-openai-web-search/route.ts --- import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, InferUITool, streamText, UIDataTypes, UIMessage, } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export type OpenAIWebSearchMessage = UIMessage< never, UIDataTypes, { web_search_preview: InferUITool< ReturnType<typeof openai.tools.webSearchPreview> >; } >; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai.responses('gpt-4o-mini'), tools: { web_search_preview: openai.tools.webSearchPreview({ searchContextSize: 'high', userLocation: { type: 'approximate', city: 'San Francisco', region: 'California', country: 'US', }, }), }, messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ sendSources: true, }); } --- File: /ai/examples/next-openai/app/api/chat-perplexity/route.ts --- import { perplexity } from '@ai-sdk/perplexity'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const prompt = convertToModelMessages(messages); const result = streamText({ model: perplexity('sonar-reasoning'), prompt, }); return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai/app/api/chat-xai/route.ts --- import { xai } from '@ai-sdk/xai'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const prompt = convertToModelMessages(messages); const result = streamText({ model: xai('grok-beta'), prompt, }); return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai/app/api/completion/route.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { // Extract the `prompt` from the body of the request const { prompt } = await req.json(); // Ask OpenAI for a streaming completion given the prompt const result = streamText({ model: openai('gpt-3.5-turbo-instruct'), prompt, }); // Respond with the stream return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai/app/api/dynamic-tools/route.ts --- import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, dynamicTool, InferUITools, stepCountIs, streamText, tool, ToolSet, UIDataTypes, UIMessage, } from 'ai'; import { z } from 'zod/v4'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; const getWeatherInformationTool = tool({ description: 'show the weather in a given city to the user', inputSchema: z.object({ city: z.string() }), execute: async ({ city }: { city: string }, { messages }) => { // count the number of assistant messages. throw error if 2 or less const assistantMessageCount = messages.filter( message => message.role === 'assistant', ).length; if (assistantMessageCount <= 2) { throw new Error('could not get weather information'); } // Add artificial delay of 5 seconds await new Promise(resolve => setTimeout(resolve, 5000)); const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy']; return weatherOptions[Math.floor(Math.random() * weatherOptions.length)]; }, }); const staticTools = { // server-side tool with execute function: getWeatherInformation: getWeatherInformationTool, } as const; export type ToolsMessage = UIMessage< never, UIDataTypes, InferUITools<typeof staticTools> >; function dynamicTools(): ToolSet { return { currentLocation: dynamicTool({ description: 'Get the current location.', inputSchema: z.object({}), execute: async () => { const locations = ['New York', 'London', 'Paris']; return { location: locations[Math.floor(Math.random() * locations.length)], }; }, }), }; } export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), // multi-steps for server-side tools tools: { ...staticTools, ...dynamicTools(), }, }); return result.toUIMessageStreamResponse({ // originalMessages: messages, //add if you want to have correct ids onFinish: options => { console.log('onFinish', options); }, }); } --- File: /ai/examples/next-openai/app/api/files/route.ts --- import { handleUpload, type HandleUploadBody } from '@vercel/blob/client'; import { NextResponse } from 'next/server'; /* * This route is used to upload files to Vercel's Blob Storage. * Example from https://vercel.com/docs/storage/vercel-blob/client-upload#create-a-client-upload-route */ export async function POST(request: Request): Promise<NextResponse> { const body = (await request.json()) as HandleUploadBody; try { const jsonResponse = await handleUpload({ body, request, onBeforeGenerateToken: async ( pathname, /* clientPayload */ ) => { // Generate a client token for the browser to upload the file // ⚠️ Authenticate and authorize users before generating the token. // Otherwise, you're allowing anonymous uploads. return { allowedContentTypes: [ 'image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'text/plain', ], tokenPayload: JSON.stringify({ // optional, sent to your server on upload completion // you could pass a user id from auth, or a value from clientPayload }), }; }, onUploadCompleted: async ({ blob, tokenPayload }) => { // Get notified of client upload completion // ⚠️ This will not work on `localhost` websites, // Use ngrok or similar to get the full upload flow console.log('file upload completed', blob, tokenPayload); try { // Run any logic after the file upload completed // const { userId } = JSON.parse(tokenPayload); // await db.update({ avatar: blob.url, userId }); } catch (error) { throw new Error('Could not complete operation'); } }, }); return NextResponse.json(jsonResponse); } catch (error) { return NextResponse.json( { error: (error as Error).message }, { status: 400 }, // The webhook will retry 5 times waiting for a 200 ); } } --- File: /ai/examples/next-openai/app/api/generate-image/route.ts --- import { openai } from '@ai-sdk/openai'; import { experimental_generateImage as generateImage } from 'ai'; // Allow responses up to 60 seconds export const maxDuration = 60; export async function POST(req: Request) { const { prompt } = await req.json(); const { image } = await generateImage({ model: openai.imageModel('dall-e-3'), prompt, size: '1024x1024', providerOptions: { openai: { style: 'vivid', quality: 'hd' }, }, }); return Response.json(image.base64); } --- File: /ai/examples/next-openai/app/api/mcp-zapier/route.ts --- import { openai } from '@ai-sdk/openai'; import { experimental_createMCPClient, stepCountIs, streamText } from 'ai'; export const maxDuration = 30; export async function POST(req: Request) { const { messages } = await req.json(); const mcpClient = await experimental_createMCPClient({ transport: { type: 'sse', url: 'https://actions.zapier.com/mcp/[YOUR_KEY]/sse', }, }); try { const zapierTools = await mcpClient.tools(); const result = streamText({ model: openai('gpt-4o'), messages, tools: zapierTools, onFinish: async () => { await mcpClient.close(); }, stopWhen: stepCountIs(10), }); return result.toUIMessageStreamResponse(); } catch (error) { return new Response('Internal Server Error', { status: 500 }); } } --- File: /ai/examples/next-openai/app/api/test-invalid-tool-call/route.ts --- import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, InferUITools, stepCountIs, streamText, tool, UIDataTypes, UIMessage, } from 'ai'; import { convertArrayToReadableStream, MockLanguageModelV2 } from 'ai/test'; import { z } from 'zod/v4'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; const getWeatherInformationTool = tool({ description: 'show the weather in a given city to the user', inputSchema: z.object({ city: z.string() }), execute: async ({ city }: { city: string }) => { // Add artificial delay of 5 seconds await new Promise(resolve => setTimeout(resolve, 5000)); const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy']; return weatherOptions[Math.floor(Math.random() * weatherOptions.length)]; }, }); const tools = { // server-side tool with execute function: getWeatherInformation: getWeatherInformationTool, } as const; export type UseChatToolsMessage = UIMessage< never, UIDataTypes, InferUITools<typeof tools> >; export async function POST(req: Request) { const { messages } = await req.json(); console.log('messages', JSON.stringify(messages, null, 2)); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), // multi-steps for server-side tools tools, prepareStep: async ({ stepNumber }) => { // inject invalid tool call in first step: if (stepNumber === 0) { return { model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'stream-start', warnings: [] }, { type: 'tool-input-start', id: 'call-1', toolName: 'getWeatherInformation', providerExecuted: true, }, { type: 'tool-input-delta', id: 'call-1', delta: `{ "cities": "San Francisco" }`, }, { type: 'tool-input-end', id: 'call-1', }, { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'getWeatherInformation', // wrong tool call arguments (city vs cities): input: `{ "cities": "San Francisco" }`, }, { type: 'finish', finishReason: 'stop', usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30, }, }, ]), }), }), }; } }, }); return result.toUIMessageStreamResponse({ // originalMessages: messages, //add if you want to have correct ids onFinish: options => { console.log('onFinish', options); }, }); } --- File: /ai/examples/next-openai/app/api/use-chat-cache/route.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; // simple cache implementation, use Vercel KV or a similar service for production const cache = new Map<string, string>(); export async function POST(req: Request) { const { messages } = await req.json(); // come up with a key based on the request: const key = JSON.stringify(messages); // Check if we have a cached response const cached = cache.get(key); if (cached != null) { return new Response(`data: ${cached}\n\n`, { status: 200, headers: { 'Content-Type': 'text/plain' }, }); } // Call the language model: const result = streamText({ model: openai('gpt-4o'), messages, async onFinish({ text }) { // Cache the response text: cache.set(key, text); }, }); // Respond with the stream return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai/app/api/use-chat-custom-sources/route.ts --- import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse, streamText, UIMessage, } from 'ai'; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const stream = createUIMessageStream({ execute: ({ writer }) => { writer.write({ type: 'start' }); // write a custom url source to the stream: writer.write({ type: 'source-url', sourceId: 'source-1', url: 'https://example.com', title: 'Example Source', }); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), }); writer.merge(result.toUIMessageStream({ sendStart: false })); }, originalMessages: messages, onFinish: options => { console.log('onFinish', JSON.stringify(options, null, 2)); }, }); return createUIMessageStreamResponse({ stream }); } --- File: /ai/examples/next-openai/app/api/use-chat-data-ui-parts/route.ts --- import { openai } from '@ai-sdk/openai'; import { delay } from '@ai-sdk/provider-utils'; import { convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse, stepCountIs, streamText, } from 'ai'; import { z } from 'zod/v4'; export async function POST(req: Request) { const { messages } = await req.json(); const stream = createUIMessageStream({ execute: ({ writer }) => { const result = streamText({ model: openai('gpt-4o'), stopWhen: stepCountIs(2), tools: { weather: { description: 'Get the weather in a city', inputSchema: z.object({ city: z.string(), }), execute: async ({ city }, { toolCallId }) => { // update display writer.write({ type: 'data-weather', id: toolCallId, data: { city, status: 'loading' }, }); await delay(2000); // fake delay const weather = 'sunny'; // update display writer.write({ type: 'data-weather', id: toolCallId, data: { city, weather, status: 'success' }, }); // for LLM roundtrip return { city, weather }; }, }, }, messages: convertToModelMessages(messages), }); writer.merge(result.toUIMessageStream()); }, }); return createUIMessageStreamResponse({ stream }); } --- File: /ai/examples/next-openai/app/api/use-chat-human-in-the-loop/route.ts --- import { openai } from '@ai-sdk/openai'; import { createUIMessageStreamResponse, streamText, createUIMessageStream, convertToModelMessages, stepCountIs, } from 'ai'; import { processToolCalls } from './utils'; import { tools } from './tools'; import { HumanInTheLoopUIMessage } from './types'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: HumanInTheLoopUIMessage[] } = await req.json(); const stream = createUIMessageStream({ execute: async ({ writer }) => { // Utility function to handle tools that require human confirmation // Checks for confirmation in last message and then runs associated tool const processedMessages = await processToolCalls( { messages, writer, tools, }, { // type-safe object for tools without an execute function getWeatherInformation: async ({ city }) => { const conditions = ['sunny', 'cloudy', 'rainy', 'snowy']; return `The weather in ${city} is ${ conditions[Math.floor(Math.random() * conditions.length)] }.`; }, }, ); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(processedMessages), tools, stopWhen: stepCountIs(5), }); writer.merge( result.toUIMessageStream({ originalMessages: processedMessages }), ); }, }); return createUIMessageStreamResponse({ stream }); } --- File: /ai/examples/next-openai/app/api/use-chat-human-in-the-loop/tools.ts --- import { tool, ToolSet } from 'ai'; import { z } from 'zod'; const getWeatherInformation = tool({ description: 'show the weather in a given city to the user', inputSchema: z.object({ city: z.string() }), outputSchema: z.string(), // must define outputSchema // no execute function, we want human in the loop }); const getLocalTime = tool({ description: 'get the local time for a specified location', inputSchema: z.object({ location: z.string() }), // including execute function -> no confirmation required execute: async ({ location }) => { console.log(`Getting local time for ${location}`); return '10am'; }, }); export const tools = { getWeatherInformation, getLocalTime, } satisfies ToolSet; --- File: /ai/examples/next-openai/app/api/use-chat-human-in-the-loop/types.ts --- import { InferUITools, UIDataTypes, UIMessage } from 'ai'; import { tools } from './tools'; export type MyTools = InferUITools<typeof tools>; // Define custom message type with data part schemas export type HumanInTheLoopUIMessage = UIMessage< never, // metadata type UIDataTypes, MyTools >; --- File: /ai/examples/next-openai/app/api/use-chat-human-in-the-loop/utils.ts --- import { convertToModelMessages, Tool, ToolCallOptions, ToolSet, UIMessageStreamWriter, getToolName, isToolUIPart, } from 'ai'; import { HumanInTheLoopUIMessage } from './types'; // Approval string to be shared across frontend and backend export const APPROVAL = { YES: 'Yes, confirmed.', NO: 'No, denied.', } as const; function isValidToolName<K extends PropertyKey, T extends object>( key: K, obj: T, ): key is K & keyof T { return key in obj; } /** * Processes tool invocations where human input is required, executing tools when authorized. * * @param options - The function options * @param options.tools - Map of tool names to Tool instances that may expose execute functions * @param options.writer - UIMessageStream writer for sending results back to the client * @param options.messages - Array of messages to process * @param executionFunctions - Map of tool names to execute functions * @returns Promise resolving to the processed messages */ export async function processToolCalls< Tools extends ToolSet, ExecutableTools extends { [Tool in keyof Tools as Tools[Tool] extends { execute: Function } ? never : Tool]: Tools[Tool]; }, >( { writer, messages, }: { tools: Tools; // used for type inference writer: UIMessageStreamWriter; messages: HumanInTheLoopUIMessage[]; // IMPORTANT: replace with your message type }, executeFunctions: { [K in keyof Tools & keyof ExecutableTools]?: ( args: ExecutableTools[K] extends Tool<infer P> ? P : never, context: ToolCallOptions, ) => Promise<any>; }, ): Promise<HumanInTheLoopUIMessage[]> { const lastMessage = messages[messages.length - 1]; const parts = lastMessage.parts; if (!parts) return messages; const processedParts = await Promise.all( parts.map(async part => { // Only process tool invocations parts if (!isToolUIPart(part)) return part; const toolName = getToolName(part); // Only continue if we have an execute function for the tool (meaning it requires confirmation) and it's in a 'result' state if (!(toolName in executeFunctions) || part.state !== 'output-available') return part; let result; if (part.output === APPROVAL.YES) { // Get the tool and check if the tool has an execute function. if ( !isValidToolName(toolName, executeFunctions) || part.state !== 'output-available' ) { return part; } const toolInstance = executeFunctions[toolName] as Tool['execute']; if (toolInstance) { result = await toolInstance(part.input, { messages: convertToModelMessages(messages), toolCallId: part.toolCallId, }); } else { result = 'Error: No execute function found on tool'; } } else if (part.output === APPROVAL.NO) { result = 'Error: User denied access to tool execution'; } else { // For any unhandled responses, return the original part. return part; } // Forward updated tool result to the client. writer.write({ type: 'tool-output-available', toolCallId: part.toolCallId, output: result, }); // Return updated toolInvocation with the actual result. return { ...part, output: result, }; }), ); // Finally return the processed messages return [...messages.slice(0, -1), { ...lastMessage, parts: processedParts }]; } export function getToolsRequiringConfirmation< T extends ToolSet, // E extends { // [K in keyof T as T[K] extends { execute: Function } ? never : K]: T[K]; // }, >(tools: T): string[] { return (Object.keys(tools) as (keyof T)[]).filter(key => { const maybeTool = tools[key]; return typeof maybeTool.execute !== 'function'; }) as string[]; } --- File: /ai/examples/next-openai/app/api/use-chat-image-output/route.ts --- import { google } from '@ai-sdk/google'; import { streamText, convertToModelMessages } from 'ai'; export const maxDuration = 30; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: google('gemini-2.0-flash-exp'), providerOptions: { google: { responseModalities: ['TEXT', 'IMAGE'] }, }, messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai/app/api/use-chat-message-metadata/example-metadata-schema.ts --- import { z } from 'zod/v4'; export const exampleMetadataSchema = z.object({ createdAt: z.number().optional(), duration: z.number().optional(), model: z.string().optional(), totalTokens: z.number().optional(), finishReason: z.string().optional(), }); export type ExampleMetadata = z.infer<typeof exampleMetadataSchema>; --- File: /ai/examples/next-openai/app/api/use-chat-message-metadata/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 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 { createdAt: Date.now(), 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 }; } // when the message is finished, send additional information: if (part.type === 'finish') { return { totalTokens: part.totalUsage.totalTokens, finishReason: part.finishReason, }; } }, }); } --- File: /ai/examples/next-openai/app/api/use-chat-persistence/route.ts --- import { openai } from '@ai-sdk/openai'; import { saveChat } from '@util/chat-store'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; export async function POST(req: Request) { const { messages, chatId }: { messages: UIMessage[]; chatId: string } = await req.json(); const result = streamText({ model: openai('gpt-4o-mini'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ originalMessages: messages, onFinish: ({ messages }) => { saveChat({ chatId, messages }); }, }); } --- File: /ai/examples/next-openai/app/api/use-chat-persistence-metadata/route.ts --- import { openai } from '@ai-sdk/openai'; import { saveChat } from '@util/chat-store'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; export async function POST(req: Request) { const { messages, chatId }: { messages: UIMessage[]; chatId: string } = await req.json(); const result = streamText({ model: openai('gpt-4o-mini'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ originalMessages: messages, messageMetadata: ({ part }) => { if (part.type === 'start') { return { createdAt: Date.now() }; } }, onFinish: ({ messages }) => { saveChat({ chatId, messages }); }, }); } --- File: /ai/examples/next-openai/app/api/use-chat-persistence-single-message/route.ts --- import { openai } from '@ai-sdk/openai'; import { loadChat, saveChat } from '@util/chat-store'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; export async function POST(req: Request) { const { message, chatId }: { message: UIMessage; chatId: string } = await req.json(); const previousMessages = await loadChat(chatId); const messages = [...previousMessages, message]; const result = streamText({ model: openai('gpt-4o-mini'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ originalMessages: messages, onFinish: ({ messages }) => { saveChat({ chatId, messages }); }, }); } --- File: /ai/examples/next-openai/app/api/use-chat-reasoning/route.ts --- import { fireworks } from '@ai-sdk/fireworks'; import { convertToModelMessages, extractReasoningMiddleware, streamText, wrapLanguageModel, } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages } = await req.json(); console.log(JSON.stringify(messages, null, 2)); const result = streamText({ model: wrapLanguageModel({ model: fireworks('accounts/fireworks/models/deepseek-r1'), middleware: extractReasoningMiddleware({ tagName: 'think' }), }), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ sendReasoning: true, }); } --- File: /ai/examples/next-openai/app/api/use-chat-reasoning-tools/route.ts --- import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, stepCountIs, streamText, tool, UIDataTypes, UIMessage, } from 'ai'; import { z } from 'zod/v4'; export type ReasoningToolsMessage = UIMessage< never, // could define metadata here UIDataTypes, // could define data parts here { getWeatherInformation: { input: { city: string }; output: string; }; askForConfirmation: { input: { message: string }; output: string; }; getLocation: { input: {}; output: string; }; } >; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages } = await req.json(); console.log(JSON.stringify(messages, null, 2)); const result = streamText({ model: openai('o3'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), // multi-steps for server-side tools tools: { // server-side tool with execute function: getWeatherInformation: tool({ description: 'show the weather in a given city to the user', inputSchema: z.object({ city: z.string() }), execute: async ({ city }: { city: string }) => { // Add artificial delay of 2 seconds await new Promise(resolve => setTimeout(resolve, 2000)); const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy']; return weatherOptions[ Math.floor(Math.random() * weatherOptions.length) ]; }, }), // client-side tool that starts user interaction: askForConfirmation: tool({ description: 'Ask the user for confirmation.', inputSchema: z.object({ message: z.string().describe('The message to ask for confirmation.'), }), }), // client-side tool that is automatically executed on the client: getLocation: tool({ description: 'Get the user location. Always ask for confirmation before using this tool.', inputSchema: z.object({}), }), }, }); return result.toUIMessageStreamResponse({ sendReasoning: true, }); } --- File: /ai/examples/next-openai/app/api/use-chat-resilient-persistence/route.ts --- import { openai } from '@ai-sdk/openai'; import { saveChat } from '@util/chat-store'; import { convertToModelMessages, streamText, UIMessage } from 'ai'; export async function POST(req: Request) { const { messages, chatId }: { messages: UIMessage[]; chatId: string } = await req.json(); const result = streamText({ model: openai('gpt-4o-mini'), messages: convertToModelMessages(messages), }); // consume the stream to ensure it runs to completion and triggers onFinish // even when the client response is aborted (e.g. when the browser tab is closed). // no await result.consumeStream({ onError: error => { console.log('Error during background stream consumption: ', error); // optional error callback }, }); return result.toUIMessageStreamResponse({ originalMessages: messages, onFinish: ({ messages }) => { saveChat({ chatId, messages }); }, }); } --- File: /ai/examples/next-openai/app/api/use-chat-resume/[id]/stream/route.ts --- import { loadStreams } from '@/util/chat-store'; import { createUIMessageStream, JsonToSseTransformStream } from 'ai'; import { after } from 'next/server'; import { createResumableStreamContext } from 'resumable-stream'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function GET( request: Request, { params }: { params: Promise<{ id: string }> }, ) { const { id } = await params; if (!id) { return new Response('id is required', { status: 400 }); } const streamIds = await loadStreams(id); if (!streamIds.length) { return new Response(null, { status: 204 }); } const recentStreamId = streamIds.at(-1); if (!recentStreamId) { return new Response(null, { status: 204 }); } const streamContext = createResumableStreamContext({ waitUntil: after, }); const resumedStream = await streamContext.resumeExistingStream(recentStreamId); if (!resumedStream) { return new Response(null, { status: 204 }); } return new Response(resumedStream); } --- File: /ai/examples/next-openai/app/api/use-chat-resume/route.ts --- import { appendMessageToChat, appendStreamId, saveChat, } from '@/util/chat-store'; import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, createUIMessageStream, generateId, JsonToSseTransformStream, streamText, UIMessage, } from 'ai'; import { after } from 'next/server'; import { createResumableStreamContext } from 'resumable-stream'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { chatId, messages }: { chatId: string; messages: UIMessage[] } = await req.json(); const streamId = generateId(); const recentUserMessage = messages .filter(message => message.role === 'user') .at(-1); if (!recentUserMessage) { throw new Error('No recent user message found'); } await appendMessageToChat({ chatId, message: recentUserMessage }); await appendStreamId({ chatId, streamId }); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ originalMessages: messages, onFinish: ({ messages }) => { saveChat({ chatId, messages }); }, async consumeSseStream({ stream }) { // send the sse stream into a resumable stream sink as well: const streamContext = createResumableStreamContext({ waitUntil: after }); await streamContext.createNewResumableStream(streamId, () => stream); }, }); } --- File: /ai/examples/next-openai/app/api/use-chat-sources/route.ts --- import { anthropic } from '@ai-sdk/anthropic'; import { convertToModelMessages, InferUITool, streamText, UIDataTypes, UIMessage, } from 'ai'; export type SourcesChatMessage = UIMessage< never, UIDataTypes, { web_search: InferUITool< ReturnType<typeof anthropic.tools.webSearch_20250305> >; } >; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: anthropic('claude-3-5-sonnet-latest'), tools: { web_search: anthropic.tools.webSearch_20250305(), }, messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ sendSources: true, }); } --- File: /ai/examples/next-openai/app/api/use-chat-streaming-tool-calls/route.ts --- import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, UIDataTypes, UIMessage } from 'ai'; import { z } from 'zod/v4'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export type StreamingToolCallsMessage = UIMessage< never, UIDataTypes, { showWeatherInformation: { input: { city: string; weather: string; temperature: number; typicalWeather: string; }; output: string; }; } >; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), system: 'You are a helpful assistant that answers questions about the weather in a given city.' + 'You use the showWeatherInformation tool to show the weather information to the user instead of talking about it.', tools: { // server-side tool with execute function: getWeatherInformation: { description: 'show the weather in a given city to the user', inputSchema: z.object({ city: z.string() }), execute: async ({}: { city: string }) => { const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy']; return { weather: weatherOptions[Math.floor(Math.random() * weatherOptions.length)], temperature: Math.floor(Math.random() * 50 - 10), }; }, }, // client-side tool that displays whether information to the user: showWeatherInformation: { description: 'Show the weather information to the user. Always use this tool to tell weather information to the user.', inputSchema: z.object({ city: z.string(), weather: z.string(), temperature: z.number(), typicalWeather: z .string() .describe( '2-3 sentences about the typical weather in the city during spring.', ), }), }, }, }); return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai/app/api/use-chat-throttle/route.ts --- import { createUIMessageStreamResponse, simulateReadableStream } from 'ai'; export async function POST(req: Request) { return createUIMessageStreamResponse({ stream: simulateReadableStream({ initialDelayInMs: 0, // Delay before the first chunk chunkDelayInMs: 0, // Delay between chunks chunks: [ { type: 'start', }, { type: 'start-step', }, ...Array(5000).fill({ type: 'text', value: 'T\n' }), { type: 'finish-step', }, { type: 'finish', }, ], }), }); } --- File: /ai/examples/next-openai/app/api/use-chat-tools/route.ts --- import { openai } from '@ai-sdk/openai'; import { convertToModelMessages, InferUITools, stepCountIs, streamText, tool, UIDataTypes, UIMessage, } from 'ai'; import { z } from 'zod/v4'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; const getWeatherInformationTool = tool({ description: 'show the weather in a given city to the user', inputSchema: z.object({ city: z.string() }), execute: async ({ city }: { city: string }, { messages }) => { // count the number of assistant messages. throw error if 2 or less const assistantMessageCount = messages.filter( message => message.role === 'assistant', ).length; if (assistantMessageCount <= 2) { throw new Error('could not get weather information'); } // Add artificial delay of 5 seconds await new Promise(resolve => setTimeout(resolve, 5000)); const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy']; return weatherOptions[Math.floor(Math.random() * weatherOptions.length)]; }, onInputStart: () => { console.log('onInputStart'); }, onInputDelta: ({ inputTextDelta }) => { console.log('onInputDelta', inputTextDelta); }, onInputAvailable: ({ input }) => { console.log('onInputAvailable', input); }, }); const askForConfirmationTool = tool({ description: 'Ask the user for confirmation.', inputSchema: z.object({ message: z.string().describe('The message to ask for confirmation.'), }), outputSchema: z.string(), }); const getLocationTool = tool({ description: 'Get the user location. Always ask for confirmation before using this tool.', inputSchema: z.object({}), outputSchema: z.string(), }); const tools = { // server-side tool with execute function: getWeatherInformation: getWeatherInformationTool, // client-side tool that starts user interaction: askForConfirmation: askForConfirmationTool, // client-side tool that is automatically executed on the client: getLocation: getLocationTool, } as const; export type UseChatToolsMessage = UIMessage< never, UIDataTypes, InferUITools<typeof tools> >; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), // multi-steps for server-side tools tools, }); return result.toUIMessageStreamResponse({ // originalMessages: messages, //add if you want to have correct ids onFinish: options => { console.log('onFinish', options); }, }); } --- File: /ai/examples/next-openai/app/api/use-completion-server-side-multi-step/route.ts --- import { openai } from '@ai-sdk/openai'; import { stepCountIs, streamText, tool } from 'ai'; import { z } from 'zod/v4'; // Allow streaming responses up to 60 seconds export const maxDuration = 60; export async function POST(req: Request) { // Extract the `prompt` from the body of the request const { prompt } = await req.json(); const result = streamText({ model: openai('gpt-4-turbo'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: stepCountIs(4), prompt, }); // Respond with the stream return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai/app/api/use-completion-throttle/route.ts --- import { createUIMessageStreamResponse, simulateReadableStream } from 'ai'; export async function POST(req: Request) { return createUIMessageStreamResponse({ stream: simulateReadableStream({ initialDelayInMs: 0, // Delay before the first chunk chunkDelayInMs: 0, // Delay between chunks chunks: [ { type: 'start', }, { type: 'start-step', }, ...Array(5000).fill({ type: 'text', value: 'T\n' }), { type: 'finish-step', }, { type: 'finish', }, ], }), }); } --- File: /ai/examples/next-openai/app/api/use-object/route.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import { notificationSchema } from './schema'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const context = await req.json(); const result = streamObject({ model: openai('gpt-4o'), prompt: `Generate 3 notifications for a messages app in this context: ${context}`, schema: notificationSchema, }); return result.toTextStreamResponse(); } --- File: /ai/examples/next-openai/app/api/use-object/schema.ts --- import { DeepPartial } from 'ai'; import { z } from 'zod/v4'; // define a schema for the notifications export const notificationSchema = z.object({ notifications: z.array( z.object({ name: z.string().describe('Name of a fictional person.'), message: z.string().describe('Message. Do not use emojis or links.'), minutesAgo: z.number(), }), ), }); // define a type for the partial notifications during generation export type PartialNotification = DeepPartial<typeof notificationSchema>; --- File: /ai/examples/next-openai/app/api/use-object-expense-tracker/route.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import { expenseSchema } from './schema'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { expense }: { expense: string } = await req.json(); const result = streamObject({ model: openai('gpt-4o'), system: 'You categorize expenses into one of the following categories: ' + 'TRAVEL, MEALS, ENTERTAINMENT, OFFICE SUPPLIES, OTHER.' + // provide date (including day of week) for reference: 'The current date is: ' + new Date() .toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: '2-digit', weekday: 'short', }) .replace(/(\w+), (\w+) (\d+), (\d+)/, '$4-$2-$3 ($1)') + '. When no date is supplied, use the current date.', prompt: `Please categorize the following expense: "${expense}"`, schema: expenseSchema, onFinish({ object }) { // save object to database }, }); return result.toTextStreamResponse(); } --- File: /ai/examples/next-openai/app/api/use-object-expense-tracker/schema.ts --- import { DeepPartial } from 'ai'; import { z } from 'zod/v4'; export const expenseSchema = z.object({ expense: z.object({ category: z .string() .describe( 'Category of the expense. Allowed categories: TRAVEL, MEALS, ENTERTAINMENT, OFFICE SUPPLIES, OTHER.', ), amount: z.number().describe('Amount of the expense in USD.'), date: z .string() .describe('Date of the expense. Format yyyy-mmm-dd, e.g. 1952-Feb-19.'), details: z.string().describe('Details of the expense.'), }), }); export type Expense = z.infer<typeof expenseSchema>['expense']; export type PartialExpense = DeepPartial<Expense>; --- File: /ai/examples/next-openai/app/bedrock/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import ChatInput from '@/component/chat-input'; export default function Chat() { const { error, status, sendMessage, messages, regenerate, stop } = useChat({ transport: new DefaultChatTransport({ api: '/api/bedrock' }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts.map(part => { if (part.type === 'text') { return part.text; } })} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/completion/page.tsx --- 'use client'; import { useCompletion } from '@ai-sdk/react'; export default function Page() { const { completion, input, handleInputChange, handleSubmit, error, isLoading, stop, } = useCompletion(); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <h4 className="pb-4 text-xl font-bold text-gray-900 md:text-xl"> useCompletion Example </h4> {error && ( <div className="fixed top-0 left-0 w-full p-4 text-center text-white bg-red-500"> {error.message} </div> )} {isLoading && ( <div className="mt-4 text-gray-500"> <div>Loading...</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {completion} <form onSubmit={handleSubmit}> <input className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" value={input} placeholder="Say something..." onChange={handleInputChange} disabled={isLoading} /> </form> </div> ); } --- File: /ai/examples/next-openai/app/completion-rsc/generate-completion.ts --- 'use server'; import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import { createStreamableValue } from '@ai-sdk/rsc'; export async function generateCompletion(prompt: string) { const result = streamText({ model: openai('gpt-4-turbo'), maxOutputTokens: 2000, prompt, }); return createStreamableValue(result.textStream).value; } --- File: /ai/examples/next-openai/app/completion-rsc/page.tsx --- 'use client'; import { readStreamableValue } from '@ai-sdk/rsc'; import { useState } from 'react'; import { generateCompletion } from './generate-completion'; export default function Chat() { const [input, setInput] = useState(''); const [completion, setCompletion] = useState(''); const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { setInput(event.target.value); }; return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <h4 className="pb-4 text-xl font-bold text-gray-900 md:text-xl"> RSC Completion Example </h4> {completion} <form onSubmit={async e => { e.preventDefault(); const streamableCompletion = await generateCompletion(input); for await (const text of readStreamableValue(streamableCompletion)) { setCompletion(text ?? ''); } }} > <input className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" value={input} placeholder="Say something..." onChange={handleInputChange} /> </form> </div> ); } --- File: /ai/examples/next-openai/app/dynamic-tools/page.tsx --- 'use client'; import ChatInput from '@/component/chat-input'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { ToolsMessage } from '../api/dynamic-tools/route'; export default function Chat() { const { messages, sendMessage, status } = useChat<ToolsMessage>({ transport: new DefaultChatTransport({ api: '/api/dynamic-tools' }), }); return ( <div className="flex flex-col py-24 mx-auto w-full max-w-md stretch"> {messages?.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> <strong>{`${message.role}: `}</strong> {message.parts.map((part, index) => { switch (part.type) { case 'text': return <div key={index}>{part.text}</div>; case 'step-start': return index > 0 ? ( <div key={index} className="text-gray-500"> <hr className="my-2 border-gray-300" /> </div> ) : null; case 'dynamic-tool': { switch (part.state) { case 'input-streaming': case 'input-available': case 'output-available': return ( <pre key={index}>{JSON.stringify(part, null, 2)}</pre> ); case 'output-error': return ( <div key={index} className="text-red-500"> Error: {part.errorText} </div> ); } } case 'tool-getWeatherInformation': { switch (part.state) { // example of pre-rendering streaming tool calls: case 'input-streaming': return ( <pre key={index}> {JSON.stringify(part.input, null, 2)} </pre> ); case 'input-available': return ( <div key={index} className="text-gray-500"> Getting weather information for {part.input.city}... </div> ); case 'output-available': return ( <div key={index} className="text-gray-500"> Weather in {part.input.city}: {part.output} </div> ); case 'output-error': return ( <div key={index} className="text-red-500"> Error: {part.errorText} </div> ); } } } })} <br /> </div> ))} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/generate-image/page.tsx --- 'use client'; import { useState } from 'react'; export default function Page() { const [inputValue, setInputValue] = useState(''); const [imageSrc, setImageSrc] = useState<string | null>(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<string | null>(null); const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); setIsLoading(true); setImageSrc(null); setError(null); try { const response = await fetch('/api/generate-image', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: inputValue }), }); if (response.ok) { const image = await response.json(); setImageSrc(`data:image/png;base64,${image}`); return; } setError(await response.text()); } finally { setIsLoading(false); } }; return ( <div className="flex flex-col items-center min-h-screen p-24"> <div className="space-y-2"> <h2 className="text-3xl font-bold tracking-tighter text-center sm:text-4xl md:text-5xl"> Image Generator </h2> <p className="max-w-[600px] text-gray-500 md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed dark:text-gray-400"> Generate images. </p> </div> <div className="w-full max-w-sm pt-6 pb-8 space-y-2"> <form className="flex space-x-2" onSubmit={handleSubmit}> <input className="flex-1 max-w-lg px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" placeholder="Describe the image" type="text" value={inputValue} onChange={e => setInputValue(e.target.value)} disabled={isLoading} /> <button type="submit" disabled={isLoading} className="px-4 py-2 text-white bg-blue-500 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:bg-blue-300 disabled:cursor-not-allowed" > Generate </button> </form> </div> {error && ( <div className="p-4 mb-4 text-red-700 bg-red-100 border border-red-400 rounded-lg"> {error} </div> )} <div className="w-[512px] h-[512px] space-y-2"> {isLoading ? ( <div className="h-[512px] w-[512px] animate-pulse bg-gray-200 rounded-lg" /> ) : ( imageSrc && ( <img alt="Generated Image" className="object-cover overflow-hidden rounded-lg" src={imageSrc} /> ) )} </div> </div> ); } --- File: /ai/examples/next-openai/app/mcp/chat/route.ts --- import { openai } from '@ai-sdk/openai'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { convertToModelMessages, experimental_createMCPClient, stepCountIs, streamText, } from 'ai'; export async function POST(req: Request) { const url = new URL('http://localhost:3000/mcp/server'); const transport = new StreamableHTTPClientTransport(url); const [client, { messages }] = await Promise.all([ experimental_createMCPClient({ transport, }), req.json(), ]); try { const tools = await client.tools(); const result = streamText({ model: openai('gpt-4o-mini'), tools, stopWhen: stepCountIs(5), onStepFinish: async ({ toolResults }) => { console.log(`STEP RESULTS: ${JSON.stringify(toolResults, null, 2)}`); }, system: 'You are a helpful chatbot capable of basic arithmetic problems', messages: convertToModelMessages(messages), onFinish: async () => { await client.close(); }, // Optional, enables immediate clean up of resources but connection will not be retained for retries: // onError: async error => { // await client.close(); // }, }); return result.toUIMessageStreamResponse(); } catch (error) { console.error(error); return Response.json({ error: 'Unexpected error' }, { status: 500 }); } } --- File: /ai/examples/next-openai/app/mcp/server/route.ts --- import { mcpApiHandler } from '@/util/mcp/handler'; import { createServerResponseAdapter } from '@/util/mcp/server-response'; import { NextRequest } from 'next/server'; // This route (/mcp/server) serves the MCP server; it's called by the /mcp/chat route that's used by useChat to connect to the server and fetch tools: const requestHandler = (req: NextRequest) => { return createServerResponseAdapter(req.signal, res => { mcpApiHandler(req, res); }); }; export { requestHandler as DELETE, requestHandler as GET, requestHandler as POST, }; --- File: /ai/examples/next-openai/app/mcp/page.tsx --- 'use client'; import ChatInput from '@/component/chat-input'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; export default function Chat() { const { error, status, sendMessage, messages, regenerate, stop } = useChat({ transport: new DefaultChatTransport({ api: '/mcp/chat' }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/mcp-zapier/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, isToolUIPart } from 'ai'; import { useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/mcp-zapier' }), }); return ( <div className="flex flex-col items-center justify-end h-screen gap-4"> <h1 className="p-4 text-xl">My AI Assistant</h1> <div className="flex flex-col gap-2 p-4 mt-auto"> {messages.map(message => ( <div key={message.id}> <strong>{`${message.role}: `}</strong> {message.parts.map((part, index) => { if (part.type === 'text') { return <span key={index}>{part.text}</span>; } else if (isToolUIPart(part)) { return <pre key={index}>{JSON.stringify(part, null, 2)}</pre>; } })} </div> ))} </div> <div className="flex flex-col items-center gap-2 p-4"> <textarea value={input} onChange={e => setInput(e.target.value)} placeholder="Start chatting" className="h-32 p-2 border-2 border-gray-300 rounded-md w-96" /> <button className="w-full p-2 px-4 text-white bg-blue-500 rounded-md" type="button" onClick={() => sendMessage({ text: input })} > Send </button> </div> </div> ); } --- File: /ai/examples/next-openai/app/stream-object/actions.ts --- 'use server'; import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import { createStreamableValue } from '@ai-sdk/rsc'; import { PartialNotification, notificationSchema } from './schema'; export async function generateNotifications(context: string) { const notificationsStream = createStreamableValue<PartialNotification>(); const result = streamObject({ model: openai('gpt-4-turbo'), prompt: `Generate 3 notifications for a messages app in this context: ${context}`, schema: notificationSchema, }); try { for await (const partialObject of result.partialObjectStream) { notificationsStream.update(partialObject); } } finally { notificationsStream.done(); } return notificationsStream.value; } --- File: /ai/examples/next-openai/app/stream-object/page.tsx --- 'use client'; import { StreamableValue, useStreamableValue } from '@ai-sdk/rsc'; import { useState } from 'react'; import { generateNotifications } from './actions'; import { PartialNotification } from './schema'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; // page component with a button to generate notifications export default function Page() { const [notificationStream, setNotificationStream] = useState<StreamableValue<PartialNotification> | null>(null); return ( <div className="flex flex-col items-center min-h-screen p-4 m-4"> <button className="px-4 py-2 mt-4 text-white bg-blue-500 rounded-md" onClick={async () => { setNotificationStream( await generateNotifications('Messages during finals week.'), ); }} > Generate notifications </button> {notificationStream && ( <NotificationsView notificationStream={notificationStream} /> )} </div> ); } // separate component to display notifications that received the streamable value: function NotificationsView({ notificationStream, }: { notificationStream: StreamableValue<PartialNotification>; }) { const [data, pending, error] = useStreamableValue(notificationStream); return ( <div className="flex flex-col gap-4 mt-4"> {data?.notifications?.map((notification, index) => ( <div className="flex items-start gap-4 p-4 bg-gray-100 rounded-md dark:bg-gray-800" key={index} > <div className="flex-1 space-y-1"> <div className="flex items-center justify-between"> <p className="font-medium">{notification?.name}</p> <p className="text-sm text-gray-500 dark:text-gray-400"> {notification?.minutesAgo} {notification?.minutesAgo != null ? ' minutes ago' : ''} </p> </div> <p className="text-gray-700 dark:text-gray-300"> {notification?.message} </p> </div> </div> ))} </div> ); } --- File: /ai/examples/next-openai/app/stream-object/schema.ts --- import { DeepPartial } from 'ai'; import { z } from 'zod/v4'; // define a schema for the notifications export const notificationSchema = z.object({ notifications: z.array( z.object({ name: z.string().describe('Name of a fictional person.'), message: z.string().describe('Message. Do not use emojis or links.'), minutesAgo: z.number(), }), ), }); // define a type for the partial notifications during generation export type PartialNotification = DeepPartial<typeof notificationSchema>; --- File: /ai/examples/next-openai/app/stream-ui/actions.tsx --- import { openai } from '@ai-sdk/openai'; import { ModelMessage, generateId } from 'ai'; import { createAI, createStreamableValue, getMutableAIState as $getMutableAIState, streamUI, } from '@ai-sdk/rsc'; import { Message, BotMessage } from './message'; import { z } from 'zod'; type AIProviderNoActions = ReturnType<typeof createAI<AIState, UIState>>; // typed wrapper *without* actions defined to avoid circular dependencies const getMutableAIState = $getMutableAIState<AIProviderNoActions>; // mock function to fetch weather data const fetchWeatherData = async (location: string) => { await new Promise(resolve => setTimeout(resolve, 1000)); return { temperature: '72°F' }; }; export async function submitUserMessage(content: string) { 'use server'; const aiState = getMutableAIState(); aiState.update({ ...aiState.get(), messages: [ ...aiState.get().messages, { id: generateId(), role: 'user', content }, ], }); let textStream: undefined | ReturnType<typeof createStreamableValue<string>>; let textNode: React.ReactNode; const result = await streamUI({ model: openai('gpt-4-turbo'), initial: <Message role="assistant">Working on that...</Message>, system: 'You are a weather assistant.', messages: aiState .get() .messages.map(({ role, content }) => ({ role, content }) as ModelMessage), text: ({ content, done, delta }) => { if (!textStream) { textStream = createStreamableValue(''); textNode = <BotMessage textStream={textStream.value} />; } if (done) { textStream.done(); aiState.update({ ...aiState.get(), messages: [ ...aiState.get().messages, { id: generateId(), role: 'assistant', content }, ], }); } else { textStream.append(delta); } return textNode; }, tools: { get_current_weather: { description: 'Get the current weather', inputSchema: z.object({ location: z.string(), }), generate: async function* ({ location }) { yield ( <Message role="assistant">Loading weather for {location}</Message> ); const { temperature } = await fetchWeatherData(location); return ( <Message role="assistant"> <span> The temperature in {location} is{' '} <span className="font-semibold">{temperature}</span> </span> </Message> ); }, }, }, onFinish: event => { // your own logic, e.g. for saving the chat history or recording usage console.log(`[onFinish]: ${JSON.stringify(event, null, 2)}`); }, }); return { id: generateId(), display: result.value, }; } export type ClientMessage = ModelMessage & { id: string; }; export type AIState = { chatId: string; messages: ClientMessage[]; }; export type UIState = { id: string; display: React.ReactNode; }[]; --- File: /ai/examples/next-openai/app/stream-ui/ai.ts --- import { createAI } from '@ai-sdk/rsc'; import { AIState, submitUserMessage, UIState } from './actions'; import { generateId } from 'ai'; export const AI = createAI({ actions: { submitUserMessage }, initialUIState: [] as UIState, initialAIState: { chatId: generateId(), messages: [] } as AIState, }); --- File: /ai/examples/next-openai/app/stream-ui/layout.tsx --- import { AI } from './ai'; export default function Layout({ children }: { children: React.ReactNode }) { return <AI>{children}</AI>; } --- File: /ai/examples/next-openai/app/stream-ui/message.tsx --- 'use client'; import { StreamableValue, useStreamableValue } from '@ai-sdk/rsc'; export function BotMessage({ textStream }: { textStream: StreamableValue }) { const [text] = useStreamableValue(textStream); return <Message role="assistant">{text}</Message>; } export function Message({ role, children, }: { role: string; children: React.ReactNode; }) { return ( <div className="flex flex-col gap-1 border-b p-2"> <div className="flex flex-row justify-between"> <div className="text-sm text-zinc-500">{role}</div> </div> {children} </div> ); } --- File: /ai/examples/next-openai/app/stream-ui/page.tsx --- 'use client'; import { Fragment, useState } from 'react'; import type { AI } from './ai'; import { useActions } from '@ai-sdk/rsc'; import { useAIState, useUIState } from '@ai-sdk/rsc'; import { generateId } from 'ai'; import { Message } from './message'; export default function Home() { const [input, setInput] = useState(''); const [messages, setMessages] = useUIState<typeof AI>(); const { submitUserMessage } = useActions<typeof AI>(); const handleSubmission = async () => { setMessages(currentMessages => [ ...currentMessages, { id: generateId(), display: <Message role="user">{input}</Message>, }, ]); const response = await submitUserMessage(input); setMessages(currentMessages => [...currentMessages, response]); setInput(''); }; return ( <div className="flex flex-col-reverse"> <div className="flex flex-row gap-2 p-2 bg-zinc-100 w-full"> <input className="bg-zinc-100 w-full p-2 outline-none" value={input} onChange={event => setInput(event.target.value)} placeholder="Ask a question" onKeyDown={event => { if (event.key === 'Enter') { handleSubmission(); } }} /> <button className="p-2 bg-zinc-900 text-zinc-100 rounded-md" onClick={handleSubmission} > Send </button> </div> <div className="flex flex-col h-[calc(100dvh-56px)] overflow-y-scroll"> <div> {messages.map(message => ( <Fragment key={message.id}>{message.display}</Fragment> ))} </div> </div> </div> ); } --- File: /ai/examples/next-openai/app/test-cohere/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import ChatInput from '@/component/chat-input'; export default function TestCohere() { const { error, status, sendMessage, messages, regenerate, stop } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat-cohere' }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <h1 className="mb-4 text-xl font-bold"> Cohere Block-Based Streaming Test </h1> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts.map(part => { if (part.type === 'text') { return part.text; } })} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/test-google/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import ChatInput from '@/component/chat-input'; export default function TestGoogle() { const { error, status, sendMessage, messages, regenerate, stop } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat-google' }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <h1 className="mb-4 text-xl font-bold"> Google Block-Based Streaming Test </h1> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts.map(part => { if (part.type === 'text') { return part.text; } })} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/test-groq/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import ChatInput from '@/component/chat-input'; export default function TestGroq() { const { error, status, sendMessage, messages, regenerate, stop } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat-groq' }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <h1 className="mb-4 text-xl font-bold"> Groq Block-Based Streaming Test </h1> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts.map(part => { if (part.type === 'text') { return part.text; } })} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/test-invalid-tool-call/page.tsx --- 'use client'; import ChatInput from '@/component/chat-input'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { UseChatToolsMessage } from '../api/test-invalid-tool-call/route'; export default function Chat() { const { messages, sendMessage, status } = useChat<UseChatToolsMessage>({ transport: new DefaultChatTransport({ api: '/api/test-invalid-tool-call', }), }); return ( <div className="flex flex-col py-24 mx-auto w-full max-w-md stretch"> {messages?.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> <strong>{`${message.role}: `}</strong> {message.parts.map((part, index) => { switch (part.type) { case 'text': return <div key={index}>{part.text}</div>; case 'step-start': return index > 0 ? ( <div key={index} className="text-gray-500"> <hr className="my-2 border-gray-300" /> </div> ) : null; case 'tool-getWeatherInformation': { switch (part.state) { // example of pre-rendering streaming tool calls: case 'input-streaming': return ( <pre key={index}> {JSON.stringify(part.input, null, 2)} </pre> ); case 'input-available': return ( <div key={index} className="text-gray-500"> Getting weather information for {part.input.city}... </div> ); case 'output-available': return ( <div key={index} className="text-gray-500"> Weather in {part.input.city}: {part.output} </div> ); case 'output-error': return ( <div key={index} className="text-red-500"> Error: {part.errorText} </div> ); } } } })} <br /> </div> ))} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/test-mistral/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import ChatInput from '@/component/chat-input'; export default function TestMistral() { const { error, status, sendMessage, messages, regenerate, stop } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat-mistral' }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <h1 className="mb-4 text-xl font-bold"> Mistral Block-Based Streaming Test </h1> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts.map(part => { if (part.type === 'text') { return part.text; } })} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/test-openai-file-search/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import ChatInput from '@/component/chat-input'; import { OpenAIFileSearchMessage } from '@/app/api/chat-openai-file-search/route'; export default function TestOpenAIFileSearch() { const { error, status, sendMessage, messages, regenerate, stop } = useChat<OpenAIFileSearchMessage>({ transport: new DefaultChatTransport({ api: '/api/chat-openai-file-search', }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <h1 className="mb-4 text-xl font-bold"> OpenAI File Search Block-Based Streaming Test </h1> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } if (part.type === 'tool-file_search') { if (part.state === 'input-available') { return ( <pre key={index} className="overflow-auto p-2 text-sm bg-gray-100 rounded" > {JSON.stringify(part.input, null, 2)} </pre> ); } if (part.state === 'output-available') { return ( <pre key={index} className="overflow-auto p-2 text-sm bg-gray-100 rounded" > {JSON.stringify(part.input, null, 2)} {`\n\nDONE - File search completed`} </pre> ); } } if (part.type === 'source-document') { return ( <span key={index}> [ <span className="text-sm font-bold text-green-600"> {part.title || part.filename || 'Document'} </span> ] </span> ); } if (part.type === 'source-url') { return ( <span key={index}> [ <a href={part.url} target="_blank" rel="noopener noreferrer" className="text-sm font-bold text-blue-500 hover:underline" > {part.title ?? new URL(part.url).hostname} </a> ] </span> ); } return null; })} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/test-openai-responses/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import ChatInput from '@/component/chat-input'; export default function TestOpenAIResponses() { const { error, status, sendMessage, messages, regenerate, stop } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat-openai-responses' }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <h1 className="mb-4 text-xl font-bold"> OpenAI Responses Block-Based Streaming Test </h1> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap mb-4"> <div className="font-semibold mb-1"> {m.role === 'user' ? 'User:' : 'AI:'} </div> {m.parts.map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } else if (part.type === 'reasoning') { return ( <div key={index} className="mt-2 p-2 bg-blue-50 border-l-2 border-blue-300 text-blue-800 text-sm" > <strong>Reasoning:</strong> {part.text} </div> ); } })} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/test-openai-web-search/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import ChatInput from '@/component/chat-input'; import { OpenAIWebSearchMessage } from '@/app/api/chat-openai-web-search/route'; export default function TestOpenAIWebSearch() { const { error, status, sendMessage, messages, regenerate, stop } = useChat<OpenAIWebSearchMessage>({ transport: new DefaultChatTransport({ api: '/api/chat-openai-web-search', }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <h1 className="mb-4 text-xl font-bold"> OpenAI Web Search Block-Based Streaming Test </h1> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } if (part.type === 'tool-web_search_preview') { if (part.state === 'input-available') { return ( <pre key={index} className="overflow-auto p-2 text-sm bg-gray-100 rounded" > {JSON.stringify(part.input, null, 2)} </pre> ); } if (part.state === 'output-available') { return ( <pre key={index} className="overflow-auto p-2 text-sm bg-gray-100 rounded" > {JSON.stringify(part.input, null, 2)} {`\n\nDONE - Web search completed`} </pre> ); } } if (part.type === 'source-url') { return ( <span key={index}> [ <a href={part.url} target="_blank" rel="noopener noreferrer" className="text-sm font-bold text-blue-500 hover:underline" > {part.title ?? new URL(part.url).hostname} </a> ] </span> ); } return null; })} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/test-perplexity/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import ChatInput from '@/component/chat-input'; export default function TestPerplexity() { const { error, status, sendMessage, messages, regenerate, stop } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat-perplexity' }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <h1 className="mb-4 text-xl font-bold"> Perplexity Block-Based Streaming Test </h1> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts.map(part => { if (part.type === 'text') { return part.text; } })} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/test-xai/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import ChatInput from '@/component/chat-input'; export default function TestXai() { const { error, status, sendMessage, messages, regenerate, stop } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat-xai' }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <h1 className="mb-4 text-xl font-bold">XAI Block-Based Streaming Test</h1> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts.map(part => { if (part.type === 'text') { return part.text; } })} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-attachments/page.tsx --- 'use client'; /* eslint-disable @next/next/no-img-element */ import { useChat } from '@ai-sdk/react'; import { useRef, useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage, status } = useChat(); const [files, setFiles] = useState<FileList | undefined>(undefined); const fileInputRef = useRef<HTMLInputElement>(null); return ( <div className="flex flex-col gap-2"> <div className="flex flex-col gap-2 p-2"> {messages.map(message => ( <div key={message.id} className="flex flex-row gap-2"> <div className="flex-shrink-0 w-24 text-zinc-500">{`${message.role}: `}</div> <div className="flex flex-col gap-2"> {message.parts.map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } if ( part.type === 'file' && part.mediaType?.startsWith('image/') ) { return ( <div key={index}> <img className="rounded-md w-60" src={part.url} alt={part.filename} /> <span className="text-sm text-zinc-500"> {part.filename} </span> </div> ); } })} </div> </div> ))} </div> <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input, files }); setFiles(undefined); setInput(''); if (fileInputRef.current) { fileInputRef.current.value = ''; } }} className="fixed bottom-0 flex flex-col w-full gap-2 p-2" > <div className="fixed flex flex-row items-end gap-2 right-2 bottom-14"> {files ? Array.from(files).map(attachment => { const { type } = attachment; if (type.startsWith('image/')) { return ( <div key={attachment.name}> <img className="w-24 rounded-md" src={URL.createObjectURL(attachment)} alt={attachment.name} /> <span className="text-sm text-zinc-500"> {attachment.name} </span> </div> ); } else if (type.startsWith('text/')) { return ( <div key={attachment.name} className="flex flex-col flex-shrink-0 w-24 gap-1 text-sm text-zinc-500" > <div className="w-16 h-20 rounded-md bg-zinc-100" /> {attachment.name} </div> ); } }) : ''} </div> <input type="file" onChange={event => { if (event.target.files) { setFiles(event.target.files); } }} multiple ref={fileInputRef} /> <input value={input} placeholder="Send message..." onChange={e => setInput(e.target.value)} className="w-full p-2 bg-zinc-100" disabled={status !== 'ready'} /> </form> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-attachments-append/page.tsx --- 'use client'; /* eslint-disable @next/next/no-img-element */ import { useChat } from '@ai-sdk/react'; import { useRef, useState } from 'react'; export default function Page() { const { messages, sendMessage, status } = useChat(); const [input, setInput] = useState(''); const [files, setFiles] = useState<FileList | undefined>(undefined); const fileInputRef = useRef<HTMLInputElement>(null); return ( <div className="flex flex-col gap-2"> <div className="flex flex-col gap-2 p-2"> {messages.map(message => ( <div key={message.id} className="flex flex-row gap-2"> <div className="flex-shrink-0 w-24 text-zinc-500">{`${message.role}: `}</div> <div className="flex flex-col gap-2"> {message.parts.map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } if ( part.type === 'file' && part.mediaType?.startsWith('image/') ) { return ( <div key={index}> <img className="rounded-md w-60" src={part.url} alt={part.filename} /> </div> ); } })} </div> </div> ))} </div> <form onSubmit={async event => { event.preventDefault(); sendMessage({ text: input, files }); setFiles(undefined); setInput(''); if (fileInputRef.current) { fileInputRef.current.value = ''; } }} className="fixed bottom-0 flex flex-col w-full gap-2 p-2" > <div className="fixed flex flex-row items-end gap-2 right-2 bottom-14"> {files ? Array.from(files).map(attachment => { const { type } = attachment; if (type.startsWith('image/')) { return ( <div key={attachment.name}> <img className="w-24 rounded-md" src={URL.createObjectURL(attachment)} alt={attachment.name} /> <span className="text-sm text-zinc-500"> {attachment.name} </span> </div> ); } }) : ''} </div> <input type="file" onChange={event => { if (event.target.files) { setFiles(event.target.files); } }} multiple ref={fileInputRef} /> <input value={input} placeholder="Send message..." onChange={e => setInput(e.target.value)} className="w-full p-2 bg-zinc-100" disabled={status !== 'ready'} /> </form> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-attachments-url/page.tsx --- 'use client'; /* eslint-disable @next/next/no-img-element */ import { useChat } from '@ai-sdk/react'; import { upload } from '@vercel/blob/client'; import { FileUIPart } from 'ai'; import { useRef, useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage, status } = useChat(); const [files, setFiles] = useState<FileUIPart[]>([]); const [isUploading, setIsUploading] = useState<boolean>(false); const fileInputRef = useRef<HTMLInputElement>(null); return ( <div className="flex flex-col gap-2"> <div className="flex flex-col gap-2 p-2"> {messages.map(message => ( <div key={message.id} className="flex flex-row gap-2"> <div className="flex-shrink-0 w-24 text-zinc-500">{`${message.role}: `}</div> <div className="flex flex-col gap-2"> {message.parts.map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } if ( part.type === 'file' && part.mediaType?.startsWith('image/') ) { return ( <div key={index}> <img className="rounded-md w-60" src={part.url} alt={part.filename} /> </div> ); } })} </div> </div> ))} </div> <form onSubmit={event => { if (isUploading) { alert('Please wait for the files to finish uploading.'); return; } sendMessage({ text: input, files }); setInput(''); setFiles([]); if (fileInputRef.current) { fileInputRef.current.value = ''; } }} className="fixed bottom-0 flex flex-col w-full gap-2 p-2" > <div className="fixed flex flex-row items-end gap-2 right-2 bottom-14"> {Array.from(files) .filter(file => file.mediaType?.startsWith('image/')) .map(file => ( <div key={file.url}> <img className="w-24 rounded-md" src={file.url} alt={file.filename} /> <span className="text-sm text-zinc-500">{file.filename}</span> </div> ))} </div> <input type="file" onChange={async event => { if (event.target.files) { setIsUploading(true); for (const file of Array.from(event.target.files)) { const blob = await upload(file.name, file, { access: 'public', handleUploadUrl: '/api/files', }); setFiles(prevFiles => [ ...prevFiles, { type: 'file' as const, filename: file.name, mediaType: blob.contentType ?? '*/*', url: blob.url, }, ]); } setIsUploading(false); } }} multiple ref={fileInputRef} /> <input value={input} placeholder="Send message..." onChange={e => setInput(e.target.value)} className="w-full p-2 bg-zinc-100" disabled={status !== 'ready'} /> </form> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-custom-sources/page.tsx --- 'use client'; import ChatInput from '@/component/chat-input'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; export default function Chat() { const { error, status, sendMessage, messages, regenerate, stop } = useChat({ transport: new DefaultChatTransport({ api: '/api/use-chat-custom-sources', }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts .filter(part => part.type !== 'source-url') .map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } })} {message.parts .filter(part => part.type === 'source-url') .map(part => ( <span key={`source-${part.sourceId}`}> [ <a href={part.url} target="_blank" className="text-sm font-bold text-blue-500 hover:underline" > {part.title ?? new URL(part.url).hostname} </a> ] </span> ))} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-data-ui-parts/page.tsx --- 'use client'; import ChatInput from '@/component/chat-input'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, UIMessage } from 'ai'; type MyMessage = UIMessage< never, { weather: { city: string; weather: string; status: 'loading' | 'success'; }; } >; export default function Chat() { const { error, status, sendMessage, messages, regenerate, stop } = useChat<MyMessage>({ transport: new DefaultChatTransport({ api: '/api/use-chat-data-ui-parts', }), onData: dataPart => { console.log('dataPart', JSON.stringify(dataPart, null, 2)); }, }); return ( <div className="flex flex-col py-24 mx-auto w-full max-w-md stretch"> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'AI: '}{' '} {message.parts .filter(part => part.type === 'data-weather') .map((part, index) => ( <span key={index} style={{ border: '2px solid red', padding: '2px', borderRadius: '4px', display: 'inline-block', minWidth: '180px', }} > {part.data.status === 'loading' ? ( <> Getting weather for <b>{part.data.city}</b>... </> ) : part.data.status === 'success' ? ( <> Weather in <b>{part.data.city}</b>:{' '} <b>{part.data.weather}</b> </> ) : ( <>Unknown weather state</> )} </span> ))} {message.parts .filter(part => part.type !== 'data-weather') .map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } })} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 rounded-md border border-blue-500" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 rounded-md border border-blue-500" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-human-in-the-loop/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, getToolName, isToolUIPart, lastAssistantMessageIsCompleteWithToolCalls, } from 'ai'; import { tools } from '../api/use-chat-human-in-the-loop/tools'; import { APPROVAL, getToolsRequiringConfirmation, } from '../api/use-chat-human-in-the-loop/utils'; import { useState } from 'react'; import { HumanInTheLoopUIMessage, MyTools, } from '../api/use-chat-human-in-the-loop/types'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage, addToolResult } = useChat<HumanInTheLoopUIMessage>({ transport: new DefaultChatTransport({ api: '/api/use-chat-human-in-the-loop', }), sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, }); const toolsRequiringConfirmation = getToolsRequiringConfirmation(tools); const pendingToolCallConfirmation = messages.some(m => m.parts?.some( part => isToolUIPart(part) && part.state === 'input-available' && toolsRequiringConfirmation.includes(getToolName(part)), ), ); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages?.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> <strong>{`${m.role}: `}</strong> {m.parts?.map((part, i) => { if (part.type === 'text') { return <div key={i}>{part.text}</div>; } if (isToolUIPart<MyTools>(part)) { const toolInvocation = part; const toolName = getToolName(toolInvocation); const toolCallId = toolInvocation.toolCallId; const dynamicInfoStyles = 'font-mono bg-zinc-100 p-1 text-sm'; // render confirmation tool (client-side tool with user interaction) if ( toolsRequiringConfirmation.includes(toolName) && toolInvocation.state === 'input-available' ) { return ( <div key={toolCallId}> Run <span className={dynamicInfoStyles}>{toolName}</span>{' '} with args: <br /> <span className={dynamicInfoStyles}> {JSON.stringify(toolInvocation.input, null, 2)} </span> <div className="flex gap-2 pt-2"> <button className="px-4 py-2 font-bold text-white bg-blue-500 rounded hover:bg-blue-700" onClick={async () => { await addToolResult({ toolCallId, tool: toolName, output: APPROVAL.YES, }); sendMessage(); }} > Yes </button> <button className="px-4 py-2 font-bold text-white bg-red-500 rounded hover:bg-red-700" onClick={async () => { await addToolResult({ toolCallId, tool: toolName, output: APPROVAL.NO, }); sendMessage(); }} > No </button> </div> </div> ); } return ( <div key={toolCallId}> <div className="font-mono text-sm bg-zinc-100 w-fit"> call {toolInvocation.state === 'output-available' ? 'ed' : 'ing'}{' '} {toolName} </div> </div> ); } })} <br /> </div> ))} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input disabled={pendingToolCallConfirmation} className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 rounded shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.target.value)} /> </form> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-image-output/page.tsx --- 'use client'; import ChatInput from '@/component/chat-input'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; export default function Chat() { const { status, sendMessage, messages } = useChat({ transport: new DefaultChatTransport({ api: '/api/use-chat-image-output' }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } else if ( part.type === 'file' && part.mediaType.startsWith('image/') ) { return ( // eslint-disable-next-line @next/next/no-img-element <img key={index} src={part.url} alt="Generated image" /> ); } })} </div> ))} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-message-metadata/page.tsx --- 'use client'; import ChatInput from '@/component/chat-input'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, UIMessage } from 'ai'; import { ExampleMetadata } from '../api/use-chat-message-metadata/example-metadata-schema'; type MyMessage = UIMessage<ExampleMetadata>; export default function Chat() { const { error, status, sendMessage, messages, regenerate, stop } = useChat<MyMessage>({ transport: new DefaultChatTransport({ api: '/api/use-chat-message-metadata', }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'AI: '} {message.metadata?.createdAt && ( <div> Created at:{' '} {new Date(message.metadata.createdAt).toLocaleString()} </div> )} {message.metadata?.duration && ( <div>Duration: {message.metadata.duration}ms</div> )} {message.metadata?.model && ( <div>Model: {message.metadata.model}</div> )} {message.metadata?.totalTokens && ( <div>Total tokens: {message.metadata.totalTokens}</div> )} {message.metadata?.finishReason && ( <div>Finish reason: {message.metadata.finishReason}</div> )} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-persistence/[id]/chat.tsx --- 'use client'; import ChatInput from '@/component/chat-input'; import { UIMessage, useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; export default function Chat({ id, initialMessages, }: { id?: string | undefined; initialMessages?: UIMessage[] } = {}) { const { status, sendMessage, messages } = useChat({ id, // use the provided chatId messages: initialMessages, transport: new DefaultChatTransport({ api: '/api/use-chat-persistence', }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> ))} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-persistence/[id]/page.tsx --- import { loadChat } from '@util/chat-store'; import Chat from './chat'; export default async function Page(props: { params: Promise<{ id: string }> }) { const { id } = await props.params; // get the chat ID from the URL const messages = await loadChat(id); // load the chat messages return <Chat id={id} initialMessages={messages} />; // display the chat } --- File: /ai/examples/next-openai/app/use-chat-persistence/page.tsx --- import { redirect } from 'next/navigation'; import { createChat } from '@util/chat-store'; export default async function ChatPage() { const chatId = await createChat(); redirect(`/use-chat-persistence/${chatId}`); } --- File: /ai/examples/next-openai/app/use-chat-persistence-metadata/[chatId]/chat.tsx --- 'use client'; import ChatInput from '@/component/chat-input'; import { zodSchema } from '@ai-sdk/provider-utils'; import { UIMessage, useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { z } from 'zod'; export default function Chat({ id, initialMessages, }: { id?: string | undefined; initialMessages?: UIMessage<{ createdAt: string }>[]; } = {}) { const { sendMessage, status, messages } = useChat({ id, // use the provided chatId messages: initialMessages, transport: new DefaultChatTransport({ api: '/api/use-chat-persistence-metadata', }), messageMetadataSchema: zodSchema( z.object({ createdAt: z.string().datetime(), }), ), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.metadata?.createdAt && ( <div> Created at: {new Date(m.metadata.createdAt).toLocaleString()} </div> )} {m.parts.map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } })} </div> ))} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-persistence-metadata/[chatId]/page.tsx --- import { loadChat } from '@util/chat-store'; import Chat from './chat'; import { UIMessage } from 'ai'; export default async function Page(props: { params: Promise<{ id: string }> }) { // get the chat ID from the URL: const { id } = await props.params; // load the chat messages: const messages = (await loadChat(id)) as UIMessage<{ createdAt: string }>[]; // display the chat: return <Chat id={id} initialMessages={messages} />; } --- File: /ai/examples/next-openai/app/use-chat-persistence-metadata/page.tsx --- import { redirect } from 'next/navigation'; import { createChat } from '@util/chat-store'; export default async function ChatPage() { const chatId = await createChat(); redirect(`/use-chat-persistence-metadata/${chatId}`); } --- File: /ai/examples/next-openai/app/use-chat-persistence-single-message/[id]/chat.tsx --- 'use client'; import ChatInput from '@/component/chat-input'; import { UIMessage, useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; export default function Chat({ id, initialMessages, }: { id?: string | undefined; initialMessages?: UIMessage[] } = {}) { const { status, sendMessage, messages } = useChat({ id, messages: initialMessages, transport: new DefaultChatTransport({ api: '/api/use-chat-persistence-single-message', // only send the last message to the server: prepareSendMessagesRequest({ messages, id }) { return { body: { message: messages[messages.length - 1], id } }; }, }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> ))} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-persistence-single-message/[id]/page.tsx --- import { loadChat } from '@util/chat-store'; import Chat from './chat'; export default async function Page(props: { params: Promise<{ id: string }> }) { const { id } = await props.params; // get the chat ID from the URL const messages = await loadChat(id); // load the chat messages return <Chat id={id} initialMessages={messages} />; // display the chat } --- File: /ai/examples/next-openai/app/use-chat-persistence-single-message/page.tsx --- import { redirect } from 'next/navigation'; import { createChat } from '@util/chat-store'; export default async function ChatPage() { const id = await createChat(); redirect(`/use-chat-persistence-single-message/${id}`); } --- File: /ai/examples/next-openai/app/use-chat-reasoning/page.tsx --- 'use client'; import ChatInput from '@/component/chat-input'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; export default function Chat() { const { error, status, sendMessage, messages, regenerate, stop } = useChat({ transport: new DefaultChatTransport({ api: '/api/use-chat-reasoning' }), }); return ( <div className="flex flex-col w-full max-w-2xl py-24 mx-auto stretch"> {messages.map(message => ( <div key={message.id} className="flex gap-4 pb-4 mb-6 border-b border-gray-100 last:border-0" > <div className="font-medium min-w-[50px]"> {message.role === 'user' ? 'User:' : 'AI:'} </div> <div className="flex-1"> {message.parts.map((part, index) => { if (part.type === 'text') { return ( <pre key={index} className="max-w-full overflow-x-auto break-words whitespace-pre-wrap" > {part.text} </pre> ); } if (part.type === 'reasoning') { return ( <pre key={index} className="max-w-full mb-4 overflow-x-auto italic text-gray-500 break-words whitespace-pre-wrap" > {part.text} </pre> ); } })} </div> </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-reasoning-tools/page.tsx --- 'use client'; import ChatInput from '@/component/chat-input'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { ReasoningToolsMessage } from '../api/use-chat-reasoning-tools/route'; export default function Chat() { const { messages, sendMessage, addToolResult, status } = useChat<ReasoningToolsMessage>({ transport: new DefaultChatTransport({ api: '/api/use-chat-reasoning-tools', }), // run client-side tools that are automatically executed: async onToolCall({ toolCall }) { // artificial 2 second delay await new Promise(resolve => setTimeout(resolve, 2000)); if (toolCall.toolName === 'getLocation') { const cities = [ 'New York', 'Los Angeles', 'Chicago', 'San Francisco', ]; addToolResult({ tool: 'getLocation', toolCallId: toolCall.toolCallId, output: cities[Math.floor(Math.random() * cities.length)], }); } }, }); console.log(structuredClone(messages)); return ( <div className="flex flex-col py-24 mx-auto w-full max-w-md stretch"> {messages?.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> <strong>{`${message.role}: `}</strong> {message.parts.map((part, index) => { if (part.type === 'text') { return ( <pre key={index} className="overflow-x-auto max-w-full whitespace-pre-wrap break-words" > {part.text} </pre> ); } if (part.type === 'reasoning') { return ( <pre key={index} className="overflow-x-auto mb-4 max-w-full italic text-gray-500 whitespace-pre-wrap break-words" > {part.text} </pre> ); } if (part.type === 'tool-askForConfirmation') { switch (part.state) { case 'input-available': return ( <div key={part.toolCallId} className="text-gray-500"> {part.input.message} <div className="flex gap-2"> <button className="px-4 py-2 font-bold text-white bg-blue-500 rounded hover:bg-blue-700" onClick={() => addToolResult({ tool: 'askForConfirmation', toolCallId: part.toolCallId, output: 'Yes, confirmed.', }) } > Yes </button> <button className="px-4 py-2 font-bold text-white bg-red-500 rounded hover:bg-red-700" onClick={() => addToolResult({ tool: 'askForConfirmation', toolCallId: part.toolCallId, output: 'No, denied', }) } > No </button> </div> </div> ); case 'output-available': return ( <div key={part.toolCallId} className="text-gray-500"> Location access allowed: {part.output} </div> ); } } if (part.type === 'tool-getLocation') { switch (part.state) { case 'input-available': return ( <div key={part.toolCallId} className="text-gray-500"> Getting location... </div> ); case 'output-available': return ( <div key={part.toolCallId} className="text-gray-500"> Location: {part.output} </div> ); } } if (part.type === 'tool-getWeatherInformation') { switch (part.state) { // example of pre-rendering streaming tool calls: case 'input-streaming': return ( <pre key={part.toolCallId}> {JSON.stringify(part, null, 2)} </pre> ); case 'input-available': return ( <div key={part.toolCallId} className="text-gray-500"> Getting weather information for {part.input.city}... </div> ); case 'output-available': return ( <div key={part.toolCallId} className="text-gray-500"> Weather in {part.input.city}: {part.output} </div> ); } } })} </div> ))} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-resilient-persistence/[id]/chat.tsx --- 'use client'; import ChatInput from '@/component/chat-input'; import { UIMessage, useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { createIdGenerator } from 'ai'; export default function Chat({ id, initialMessages, }: { id?: string | undefined; initialMessages?: UIMessage[] } = {}) { const { sendMessage, status, messages, stop } = useChat({ id, messages: initialMessages, transport: new DefaultChatTransport({ api: '/api/use-chat-resilient-persistence', }), generateId: createIdGenerator({ prefix: 'msgc', size: 16 }), // id format for client-side messages }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> ))} <ChatInput status={status} stop={stop} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-resilient-persistence/[id]/page.tsx --- import { loadChat } from '@util/chat-store'; import Chat from './chat'; export default async function Page(props: { params: Promise<{ id: string }> }) { const { id } = await props.params; // get the chat ID from the URL const messages = await loadChat(id); // load the chat messages return <Chat id={id} initialMessages={messages} />; // display the chat } --- File: /ai/examples/next-openai/app/use-chat-resilient-persistence/page.tsx --- import { redirect } from 'next/navigation'; import { createChat } from '@util/chat-store'; export default async function ChatPage() { const chatId = await createChat(); redirect(`/use-chat-resilient-persistence/${chatId}`); } --- File: /ai/examples/next-openai/app/use-chat-resume/[id]/page.tsx --- import { loadChat } from '@/util/chat-store'; import { Chat } from '../chat'; export default async function Page({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; const messages = await loadChat(id); return <Chat id={id} autoResume={true} initialMessages={messages} />; } --- File: /ai/examples/next-openai/app/use-chat-resume/chat.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, type UIMessage } from 'ai'; import Link from 'next/link'; import ChatInput from '@component/chat-input'; export function Chat({ id, autoResume, initialMessages = [], }: { id: string; autoResume: boolean; initialMessages: UIMessage[]; }) { const { error, status, sendMessage, messages, regenerate, stop } = useChat({ id, messages: initialMessages, transport: new DefaultChatTransport({ api: '/api/use-chat-resume' }), onError: error => { console.error('Error streaming text:', error); }, resume: autoResume, }); return ( <div className="flex flex-col w-full max-w-md gap-8 py-24 mx-auto stretch"> <Link href={`/use-chat-resume/${id}`} target="_noblank"> Chat Id: {id} </Link> <div>Status: {status}</div> {messages.map(message => ( <div key={message.id} className="flex flex-row whitespace-pre-wrap"> <div className="min-w-12"> {message.role === 'user' ? 'User: ' : 'AI: '} </div> <div> <div className="text-sm text-zinc-500">{message.id}</div> {message.parts.map((part, partIndex) => { if (part.type === 'text') { return ( <div key={`${message.id}-${partIndex}`}>{part.text}</div> ); } })} </div> </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-resume/page.tsx --- import { Chat } from './chat'; import { createIdGenerator } from 'ai'; const generateId = createIdGenerator({ size: 32 }); export default function Page() { const id = generateId(); return <Chat id={id} autoResume={false} initialMessages={[]} />; } --- File: /ai/examples/next-openai/app/use-chat-sources/page.tsx --- 'use client'; import ChatInput from '@/component/chat-input'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { SourcesChatMessage } from '@/app/api/use-chat-sources/route'; export default function Chat() { const { error, status, sendMessage, messages, regenerate, stop } = useChat<SourcesChatMessage>({ transport: new DefaultChatTransport({ api: '/api/use-chat-sources' }), }); console.log(messages); return ( <div className="flex flex-col py-24 mx-auto w-full max-w-md stretch"> {messages.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } if (part.type === 'tool-web_search') { if ( part.state === 'input-available' || part.state === 'input-streaming' ) { return ( <pre key={index} className="overflow-auto p-2 text-sm bg-gray-100 rounded" > {JSON.stringify(part.input, null, 2)} </pre> ); } if (part.state === 'output-available') { return ( <pre key={index} className="overflow-auto p-2 text-sm bg-gray-100 rounded" > {JSON.stringify(part.input, null, 2)} {`\n\nDONE - ${part.output.length} results`} </pre> ); } } if (part.type === 'source-url') { return ( <span key={index}> [ <a href={part.url} target="_blank" className="text-sm font-bold text-blue-500 hover:underline" > {part.title ?? new URL(part.url).hostname} </a> ] </span> ); } })} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 rounded-md border border-blue-500" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 rounded-md border border-blue-500" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-streaming-tool-calls/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import ChatInput from '@component/chat-input'; import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls, } from 'ai'; import { StreamingToolCallsMessage } from '../api/use-chat-streaming-tool-calls/route'; export default function Chat() { const { messages, status, sendMessage, addToolResult } = useChat<StreamingToolCallsMessage>({ transport: new DefaultChatTransport({ api: '/api/use-chat-streaming-tool-calls', }), sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, // run client-side tools that are automatically executed: async onToolCall({ toolCall }) { if (toolCall.toolName === 'showWeatherInformation') { // display tool. add tool result that informs the llm that the tool was executed. addToolResult({ tool: 'showWeatherInformation', toolCallId: toolCall.toolCallId, output: 'Weather information was shown to the user.', }); } }, }); // used to only render the role when it changes: let lastRole: string | undefined = undefined; return ( <div className="flex flex-col py-24 mx-auto w-full max-w-md stretch"> {messages?.map(m => { const isNewRole = m.role !== lastRole; lastRole = m.role; return ( <div key={m.id} className="whitespace-pre-wrap"> {isNewRole && <strong>{`${m.role}: `}</strong>} {m.parts.map(part => { if (part.type === 'text') { return part.text; } if (part.type === 'tool-showWeatherInformation') { return ( <div key={part.toolCallId} className="p-4 my-2 text-gray-500 rounded border border-gray-300" > <h4 className="mb-2">{part.input?.city ?? ''}</h4> <div className="flex flex-col gap-2"> <div className="flex gap-2"> {part.input?.weather && <b>{part.input.weather}</b>} {part.input?.temperature && ( <b>{part.input.temperature} &deg;C</b> )} </div> {part.input?.typicalWeather && ( <div>{part.input.typicalWeather}</div> )} </div> </div> ); } })} </div> ); })} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-throttle/page.tsx --- 'use client'; import ChatInput from '@/component/chat-input'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useLayoutEffect, useRef } from 'react'; export default function Chat() { const renderCount = useRef(0); useLayoutEffect(() => { console.log(`component rendered #${++renderCount.current}`); }); const { messages, status, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/use-chat-throttle' }), experimental_throttle: 50, }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <h4 className="pb-4 text-xl font-bold text-gray-900 md:text-xl"> useChat throttle example </h4> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> ))} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/use-chat-tools/page.tsx --- 'use client'; import ChatInput from '@/component/chat-input'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls, } from 'ai'; import { UseChatToolsMessage } from '../api/use-chat-tools/route'; export default function Chat() { const { messages, sendMessage, addToolResult, status } = useChat<UseChatToolsMessage>({ transport: new DefaultChatTransport({ api: '/api/use-chat-tools' }), sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, // run client-side tools that are automatically executed: async onToolCall({ toolCall }) { // artificial 2 second delay await new Promise(resolve => setTimeout(resolve, 2000)); if (toolCall.toolName === 'getLocation') { const cities = [ 'New York', 'Los Angeles', 'Chicago', 'San Francisco', ]; addToolResult({ tool: 'getLocation', toolCallId: toolCall.toolCallId, output: cities[Math.floor(Math.random() * cities.length)], }); } }, }); return ( <div className="flex flex-col py-24 mx-auto w-full max-w-md stretch"> {messages?.map(message => ( <div key={message.id} className="whitespace-pre-wrap"> <strong>{`${message.role}: `}</strong> {message.parts.map((part, index) => { switch (part.type) { case 'text': return <div key={index}>{part.text}</div>; case 'step-start': return index > 0 ? ( <div key={index} className="text-gray-500"> <hr className="my-2 border-gray-300" /> </div> ) : null; case 'tool-askForConfirmation': { switch (part.state) { case 'input-available': return ( <div key={index} className="text-gray-500"> {part.input.message} <div className="flex gap-2"> <button className="px-4 py-2 font-bold text-white bg-blue-500 rounded hover:bg-blue-700" onClick={async () => { addToolResult({ tool: 'askForConfirmation', toolCallId: part.toolCallId, output: 'Yes, confirmed.', }); }} > Yes </button> <button className="px-4 py-2 font-bold text-white bg-red-500 rounded hover:bg-red-700" onClick={async () => { addToolResult({ tool: 'askForConfirmation', toolCallId: part.toolCallId, output: 'No, denied', }); }} > No </button> </div> </div> ); case 'output-available': return ( <div key={index} className="text-gray-500"> Location access allowed: {part.output} </div> ); } break; } case 'tool-getLocation': { switch (part.state) { case 'input-available': return ( <div key={index} className="text-gray-500"> Getting location... </div> ); case 'output-available': return ( <div key={index} className="text-gray-500"> Location: {part.output} </div> ); } break; } case 'tool-getWeatherInformation': { switch (part.state) { // example of pre-rendering streaming tool calls: case 'input-streaming': return ( <pre key={index}> {JSON.stringify(part.input, null, 2)} </pre> ); case 'input-available': return ( <div key={index} className="text-gray-500"> Getting weather information for {part.input.city}... </div> ); case 'output-available': return ( <div key={index} className="text-gray-500"> Weather in {part.input.city}: {part.output} </div> ); case 'output-error': return ( <div key={index} className="text-red-500"> Error: {part.errorText} </div> ); } } } })} <br /> </div> ))} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/app/use-completion-server-side-multi-step/page.tsx --- 'use client'; import { useCompletion } from '@ai-sdk/react'; export default function Chat() { const { completion, input, handleInputChange, handleSubmit, error } = useCompletion({ api: '/api/use-completion-server-side-multi-step', }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <h4 className="pb-4 text-xl font-bold text-gray-900 md:text-xl"> useCompletion Example </h4> {error && ( <div className="fixed top-0 left-0 w-full p-4 text-center text-white bg-red-500"> {error.message} </div> )} {completion} <form onSubmit={handleSubmit}> <input className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" value={input} placeholder="What is the weather in Berlin?" onChange={handleInputChange} /> </form> </div> ); } --- File: /ai/examples/next-openai/app/use-completion-throttle/page.tsx --- 'use client'; import { useCompletion } from '@ai-sdk/react'; import { useLayoutEffect, useRef } from 'react'; export default function Chat() { const renderCount = useRef(0); useLayoutEffect(() => { console.log(`component rendered #${++renderCount.current}`); }); const { completion, input, handleInputChange, handleSubmit } = useCompletion({ api: '/api/use-completion-throttle', experimental_throttle: 50, }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> <h4 className="pb-4 text-xl font-bold text-gray-900 md:text-xl"> useCompletion throttle example </h4> {completion} <form onSubmit={handleSubmit}> <input className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" value={input} placeholder="Book topic..." onChange={handleInputChange} /> </form> </div> ); } --- File: /ai/examples/next-openai/app/use-object/page.tsx --- 'use client'; import { experimental_useObject as useObject } from '@ai-sdk/react'; import { notificationSchema } from '../api/use-object/schema'; export default function Page() { const { submit, isLoading, object, stop, error, clear } = useObject({ api: '/api/use-object', schema: notificationSchema, }); return ( <div className="flex flex-col items-center min-h-screen p-4 m-4"> <button className="px-4 py-2 mt-4 text-white bg-blue-500 rounded-md disabled:bg-blue-200" onClick={async () => { submit('Messages during finals week.'); }} disabled={isLoading} > Generate notifications </button> {error && ( <div className="mt-4 text-red-500"> An error occurred. {error.message} </div> )} {isLoading && ( <div className="mt-4 text-gray-500"> <div>Loading...</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => stop()} > STOP </button> </div> )} <div className="mt-4 text-gray-500"> <button type="button" onClick={() => clear()} className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" > Clear </button> </div> <div className="flex flex-col gap-4 mt-4"> {object?.notifications?.map((notification, index) => ( <div className="flex items-start gap-4 p-4 bg-gray-100 rounded-md dark:bg-gray-800" key={index} > <div className="flex-1 space-y-1"> <div className="flex items-center justify-between"> <p className="font-medium dark:text-white"> {notification?.name} </p> <p className="text-sm text-gray-500 dark:text-gray-400"> {notification?.minutesAgo} {notification?.minutesAgo != null ? ' minutes ago' : ''} </p> </div> <p className="text-gray-700 dark:text-gray-300"> {notification?.message} </p> </div> </div> ))} </div> </div> ); } --- File: /ai/examples/next-openai/app/use-object-expense-tracker/page.tsx --- 'use client'; import { experimental_useObject as useObject } from '@ai-sdk/react'; import { Expense, expenseSchema, PartialExpense, } from '../api/use-object-expense-tracker/schema'; import { useState } from 'react'; export default function Page() { const [expenses, setExpenses] = useState<Expense[]>([]); const { submit, isLoading, object } = useObject({ api: '/api/use-object-expense-tracker', schema: expenseSchema, onFinish({ object }) { if (object != null) { setExpenses(prev => [object.expense, ...prev]); } }, }); return ( <div className="flex flex-col items-center min-h-screen p-4 m-4"> <form className="flex items-center w-full max-w-md" onSubmit={e => { e.preventDefault(); const input = e.currentTarget.expense as HTMLInputElement; if (input.value.trim()) { submit({ expense: input.value }); e.currentTarget.reset(); } }} > <input type="text" name="expense" className="flex-grow px-4 py-2 mr-2 border rounded-md" placeholder="Enter expense details" /> <button type="submit" className="px-4 py-2 text-white bg-blue-500 rounded-md disabled:bg-blue-200 whitespace-nowrap" disabled={isLoading} > Log expense </button> </form> {isLoading && object?.expense && <ExpenseView expense={object.expense} />} {expenses.map((expense, index) => ( <ExpenseView key={index} expense={expense} /> ))} </div> ); } const ExpenseView = ({ expense }: { expense: PartialExpense | Expense }) => ( <div className="grid grid-cols-12 gap-4 p-4 mt-4 bg-gray-100 rounded-md dark:bg-gray-800"> <div className="col-span-2"> <p className="text-sm text-gray-500 dark:text-gray-400"> {expense?.date ?? ''} </p> </div> <div className="col-span-2"> <p className="text-sm text-gray-500 dark:text-gray-400"> ${expense?.amount?.toFixed(2) ?? ''} </p> </div> <div className="col-span-3"> <p className="font-medium dark:text-white">{expense?.category ?? ''}</p> </div> <div className="col-span-5"> <p className="text-gray-700 dark:text-gray-300"> {expense?.details ?? ''} </p> </div> </div> ); --- File: /ai/examples/next-openai/app/layout.tsx --- import './globals.css'; export const metadata = { title: 'AI SDK - Next.js OpenAI Examples', description: 'Examples of using the AI SDK with Next.js and OpenAI.', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body>{children}</body> </html> ); } --- File: /ai/examples/next-openai/app/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import ChatInput from '@/component/chat-input'; export default function Chat() { const { error, status, sendMessage, messages, regenerate, stop } = useChat(); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts.map(part => { if (part.type === 'text') { return part.text; } })} </div> ))} {(status === 'submitted' || status === 'streaming') && ( <div className="mt-4 text-gray-500"> {status === 'submitted' && <div>Loading...</div>} <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={stop} > Stop </button> </div> )} {error && ( <div className="mt-4"> <div className="text-red-500">An error occurred.</div> <button type="button" className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md" onClick={() => regenerate()} > Retry </button> </div> )} <ChatInput status={status} onSubmit={text => sendMessage({ text })} /> </div> ); } --- File: /ai/examples/next-openai/component/chat-input.tsx --- import { useState } from 'react'; export default function ChatInput({ status, onSubmit, stop, }: { status: string; onSubmit: (text: string) => void; stop?: () => void; }) { const [text, setText] = useState(''); return ( <form onSubmit={e => { e.preventDefault(); if (text.trim() === '') return; onSubmit(text); setText(''); }} > <input className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" placeholder="Say something..." disabled={status !== 'ready'} value={text} onChange={e => setText(e.target.value)} /> {stop && (status === 'streaming' || status === 'submitted') && ( <button className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" type="submit" onClick={stop} > Stop </button> )} </form> ); } --- File: /ai/examples/next-openai/util/mcp/handler.ts --- import { ServerOptions } from '@modelcontextprotocol/sdk/server/index.js'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { ServerResponse } from 'http'; import { NextRequest } from 'next/server'; import { z } from 'zod'; import { convertNextRequestToIncomingMessage } from './incoming-message'; export const mcpApiHandler = initializeMcpApiHandler({ initializationCallback: server => { server.tool( 'calculateSum', 'Returns the sum of N numbers', { values: z.array(z.number()), }, async ({ values }: { values: number[] }) => ({ content: [ { type: 'text', text: `Sum: ${values.reduce((a: number, b: number) => a + b, 0)}`, }, ], }), ); }, serverOptions: { capabilities: { tools: {}, }, }, }); function initializeMcpApiHandler({ initializationCallback, serverOptions, }: { initializationCallback: (server: McpServer) => void; serverOptions?: ServerOptions; }) { return async function mcpApiHandler(req: NextRequest, res: ServerResponse) { const url = new URL(req.url || '', 'https://example.com'); if (url.pathname === '/mcp/server') { if (req.method === 'GET') { console.log('Received GET MCP request'); res.writeHead(405, { 'Content-Type': 'application/json' }).end( JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed.', }, id: null, }), ); return; } if (req.method === 'DELETE') { console.log('Received DELETE MCP request'); res.writeHead(405, { 'Content-Type': 'application/json' }).end( JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed.', }, id: null, }), ); return; } console.log('New MCP connection', req.url, req.method); if (req.method === 'POST') { /** * In Stateless Mode, we create a new instance of transport and server for each request to ensure complete isolation. A single instance would cause request ID collisions when multiple clients connect concurrently. */ const server = new McpServer( { name: 'MCP Next.js Server', version: '0.1.0', }, serverOptions, ); const statelessTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); initializationCallback(server); await server.connect(statelessTransport); const incomingMessage = await convertNextRequestToIncomingMessage(req); await statelessTransport.handleRequest(incomingMessage, res); } } else { res.statusCode = 404; res.end('Not found'); } }; } --- File: /ai/examples/next-openai/util/mcp/incoming-message.ts --- import { IncomingMessage } from 'http'; import { Socket } from 'net'; import { NextRequest } from 'next/server'; import { Readable } from 'stream'; export async function convertNextRequestToIncomingMessage( request: NextRequest, ): Promise<IncomingMessage> { const method = request.method; const url = request.url; const headers = Object.fromEntries(request.headers); const contentType = request.headers.get('content-type') || ''; const body = contentType.includes('application/json') ? await request.json() : await request.text(); const socket = new Socket(); // Create a readable stream that will be used as the base for IncomingMessage const readable = new Readable(); readable._read = (): void => {}; // Required implementation // Add the body content if provided if (body) { if (typeof body === 'string') { readable.push(body); } else if (Buffer.isBuffer(body)) { readable.push(body); } else { // Ensure proper JSON-RPC format const bodyString = JSON.stringify(body); readable.push(bodyString); } readable.push(null); // Signal the end of the stream } else { readable.push(null); // Always end the stream even if no body } // Create the IncomingMessage instance const req = new IncomingMessage(socket); // Set the properties req.method = method; req.url = url; req.headers = headers; // Copy over the stream methods req.push = readable.push.bind(readable); req.read = readable.read.bind(readable); // @ts-expect-error req.on = readable.on.bind(readable); req.pipe = readable.pipe.bind(readable); return req; } --- File: /ai/examples/next-openai/util/mcp/server-response.ts --- import { EventEmitter } from 'node:events'; import { type ServerResponse } from 'node:http'; type WriteheadArgs = { statusCode: number; headers?: Record<string, string>; }; /** * Anthropic's MCP API requires a ServerResponse object. This function * creates a fake server response object that can be used to pass to the MCP API. */ export function createServerResponseAdapter( signal: AbortSignal, fn: (re: ServerResponse) => Promise<void> | void, ): Promise<Response> { let writeHeadResolver: (v: WriteheadArgs) => void; const writeHeadPromise = new Promise<WriteheadArgs>( async (resolve, _reject) => { writeHeadResolver = resolve; }, ); return new Promise(async (resolve, _reject) => { let controller: ReadableStreamController<Uint8Array> | undefined; let shouldClose = false; let wroteHead = false; const writeHead = ( statusCode: number, headers?: Record<string, string>, ) => { if (typeof headers === 'string') { throw new Error('Status message of writeHead not supported'); } wroteHead = true; writeHeadResolver({ statusCode, headers, }); return fakeServerResponse; }; let bufferedData: Uint8Array[] = []; const write = ( chunk: Buffer | string, encoding?: BufferEncoding, ): boolean => { if (encoding) { throw new Error('Encoding not supported'); } if (chunk instanceof Buffer) { throw new Error('Buffer not supported'); } if (!wroteHead) { writeHead(200); } if (!controller) { bufferedData.push(new TextEncoder().encode(chunk as string)); return true; } controller.enqueue(new TextEncoder().encode(chunk as string)); return true; }; const eventEmitter = new EventEmitter(); const fakeServerResponse = { writeHead, write, end: (data?: Buffer | string) => { if (data) { write(data); } if (!controller) { shouldClose = true; return fakeServerResponse; } try { controller.close(); } catch { /* May be closed on tcp layer */ } return fakeServerResponse; }, on: (event: string, listener: (...args: any[]) => void) => { eventEmitter.on(event, listener); return fakeServerResponse; }, flushHeaders: () => { return fakeServerResponse; }, }; signal.addEventListener('abort', () => { eventEmitter.emit('close'); }); fn(fakeServerResponse as unknown as ServerResponse); const head = await writeHeadPromise; const response = new Response( new ReadableStream({ start(c) { controller = c; for (const chunk of bufferedData) { controller.enqueue(chunk); } if (shouldClose) { controller.close(); } }, }), { status: head.statusCode, headers: head.headers, }, ); resolve(response); }); } --- File: /ai/examples/next-openai/util/chat-store.ts --- import { generateId, UIMessage } from 'ai'; import { existsSync, mkdirSync } from 'fs'; import { readFile, writeFile } from 'fs/promises'; import path from 'path'; // example implementation for demo purposes // in a real app, you would save the chat to a database // and use the id from the database entry export async function createChat(): Promise<string> { const id = generateId(); await writeFile(getChatFile(id), '[]'); return id; } export async function saveChat({ chatId, messages, }: { chatId: string; messages: UIMessage[]; }): Promise<void> { await writeFile(getChatFile(chatId), JSON.stringify(messages, null, 2)); } export async function appendMessageToChat({ chatId, message, }: { chatId: string; message: UIMessage; }): Promise<void> { const file = getChatFile(chatId); const messages = await loadChat(chatId); messages.push(message); await writeFile(file, JSON.stringify(messages, null, 2)); } export async function loadChat(id: string): Promise<UIMessage[]> { return JSON.parse(await readFile(getChatFile(id), 'utf8')); } function getChatFile(id: string): string { const chatDir = path.join(process.cwd(), '.chats'); if (!existsSync(chatDir)) mkdirSync(chatDir, { recursive: true }); const chatFile = path.join(chatDir, `${id}.json`); if (!existsSync(chatFile)) { writeFile(chatFile, '[]'); } return chatFile; } export async function appendStreamId({ chatId, streamId, }: { chatId: string; streamId: string; }) { const file = getStreamsFile(chatId); const streams = await loadStreams(chatId); streams.push(streamId); await writeFile(file, JSON.stringify(streams, null, 2)); } export async function loadStreams(chatId: string): Promise<string[]> { const file = getStreamsFile(chatId); if (!existsSync(file)) return []; return JSON.parse(await readFile(file, 'utf8')); } function getStreamsFile(chatId: string): string { const chatDir = path.join(process.cwd(), '.streams'); if (!existsSync(chatDir)) mkdirSync(chatDir, { recursive: true }); return path.join(chatDir, `${chatId}.json`); } --- File: /ai/examples/next-openai/next.config.js --- /** @type {import('next').NextConfig} */ const nextConfig = {}; module.exports = nextConfig; --- File: /ai/examples/next-openai/postcss.config.js --- module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; --- File: /ai/examples/next-openai/tailwind.config.js --- /** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', }, }, }, plugins: [], }; --- File: /ai/examples/next-openai-kasada-bot-protection/app/149e9513-01fa-4fb0-aad4-566afd725d1b/2d206a39-8ed7-437e-a3be-862e0f06eea3/[[...restpath]]/route.ts --- const KASADA_ENDPOINT = 'FILL_IN.kasadapolyform.io'; async function handler(request: Request) { const url = new URL(request.url); url.protocol = 'https:'; url.host = KASADA_ENDPOINT; url.port = ''; url.searchParams.delete('restpath'); const headers = new Headers(request.headers); headers.set('X-Forwarded-Host', 'FILL_IN'); headers.delete('host'); const r = await fetch(url.toString(), { method: request.method, body: request.body, headers, mode: request.mode, redirect: 'manual', // @ts-expect-error duplex: 'half', }); const responseHeaders = new Headers(r.headers); responseHeaders.set('cdn-cache-control', 'no-cache'); return new Response(r.body, { status: r.status, statusText: r.statusText, headers: responseHeaders, }); } export const GET = handler; export const POST = handler; export const OPTIONS = handler; export const PUT = handler; --- File: /ai/examples/next-openai-kasada-bot-protection/app/api/chat/route.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { // Extract the `messages` from the body of the request const { messages } = await req.json(); // Call the language model const result = streamText({ model: openai('gpt-4o'), messages, }); // Respond with the stream return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai-kasada-bot-protection/app/layout.tsx --- import './globals.css'; import Toaster from './toaster'; import { KasadaClient } from '@/kasada/kasada-client'; export const metadata = { title: 'Create Next App', description: 'Generated by create next app', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <Toaster /> <KasadaClient /> <body>{children}</body> </html> ); } --- File: /ai/examples/next-openai-kasada-bot-protection/app/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; import { toast } from 'sonner'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage, status } = useChat({ onError: err => { toast.error(err.message); }, }); return ( <div className="flex flex-col py-24 mx-auto w-full max-w-md stretch"> {messages.length > 0 ? messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> )) : null} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input className="fixed bottom-0 p-2 mb-8 w-full max-w-md rounded border border-gray-300 shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.target.value)} /> </form> </div> ); } --- File: /ai/examples/next-openai-kasada-bot-protection/app/toaster.tsx --- 'use client'; export { Toaster as default } from 'sonner'; --- File: /ai/examples/next-openai-kasada-bot-protection/kasada/kasada-client.tsx --- import Script from 'next/script'; export function KasadaClient() { return ( <> <script dangerouslySetInnerHTML={{ __html: `document.addEventListener('kpsdk-load', () => {window.KPSDK.configure([ { domain: location.host, path: '/api/chat', method: 'POST' }, ]); });`.replace(/[\n\r\s]/g, ''), }} ></script> <Script async={true} src="/149e9513-01fa-4fb0-aad4-566afd725d1b/2d206a39-8ed7-437e-a3be-862e0f06eea3/p.js" ></Script> </> ); } --- File: /ai/examples/next-openai-kasada-bot-protection/kasada/kasada-server.tsx --- import { NextFetchEvent, NextRequest, NextResponse } from 'next/server'; import { ipAddress } from '@vercel/functions'; // You can get this endpoint name from the application details on the Kasada Portal. const kasadaAPIHostname = 'vercel-endpoint.kasadapolyform.io'; const kasadaAPIVersion = '2023-01-13-preview'; const kasadaAPIURL = `https://${kasadaAPIHostname}/149e9513-01fa-4fb0-aad4-566afd725d1b/2d206a39-8ed7-437e-a3be-862e0f06eea3/api/${kasadaAPIVersion}/classification`; export interface APIRequest { // valid IPv4 orIPv6 address of the original client making the request clientIp: string; // always provide as many of the available header from the client request headers: Array<{ key: string; value: string; }>; method: 'HEAD' | 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; protocol: 'HTTP' | 'HTTPS'; // /some/path path: string; // request querystring including leading '?', e.g. '?foo=bar&bar=foo' querystring: string; // always provide the (redacted) body if available in the client request body?: string; } export interface APIResponse { // unique request id as generated by the API requestId: string; // unique client id; only present when a client ID is available clientId?: string; // API classification classification: 'ALLOWED' | 'BAD-BOT' | 'GOOD-BOT' | 'HUMAN'; // array of Set-Cookie strings, like '<cookie-name>=<cookie-value>; SameSite=None; Secure' responseHeadersToSet: Array<{ key: string; value: string }>; application: { mode: 'MONITOR' | 'PROTECT' | 'PASS_THROUGH'; domain: string; }; error: string; } /** * Function that fetches the Kasada classification and metadata about the request * and returns either this metadata or an error if something went wrong. */ async function getKasadaMetadata(request: NextRequest): Promise<{ metadata?: APIResponse; error?: Error; }> { const url = new URL(request.url); const headers = new Headers(request.headers); headers.delete('x-forwarded-host'); headers.set('Host', 'host'); const headersArray = [...headers.entries()].map(([key, value]) => ({ key, value, })); const kasadaPayload: APIRequest = { clientIp: String(request.headers.get('x-real-ip') || ipAddress(request)), headers: headersArray, method: request.method as APIRequest['method'], protocol: url.protocol.slice(0, -1).toUpperCase() as APIRequest['protocol'], path: url.pathname, querystring: url.search, }; // Set a maximum Kasada response time of 3 seconds const timeout = 3000; const timeoutController = new AbortController(); const timeoutId = setTimeout(() => timeoutController.abort(), timeout); try { // Send request information off to Kasada for classification const response = await fetch(kasadaAPIURL, { method: 'POST', headers: { 'X-Forwarded-Host': url.hostname, 'Content-Type': 'application/json', Authorization: `KasadaApiTokenV1 ${process.env.KASADA_TOKEN ?? ''}`, }, signal: timeoutController.signal, body: JSON.stringify(kasadaPayload), keepalive: true, }); const metadata = (await response.json()) as APIResponse; return { metadata, }; } catch (error) { if (timeoutController.signal.aborted) { return { error: new Error('Fetch request timed out'), }; } // Some other error occurred return { error: error instanceof Error ? error : new Error(String(error)), }; } finally { clearTimeout(timeoutId); } } /** * Function that continues the request to the origin */ async function callOrigin(): Promise<Response> { return NextResponse.next(); } /** * Function that adds the `responseHeadersToSet` headers returned as part of the request metadata * to the response. These headers are necessary for the correct working of the client side SDK. */ function addKasadaHeaders(metadata: APIResponse, response: Response): void { metadata.responseHeadersToSet.forEach(({ key, value }) => { response.headers.set(key, value); }); } /** * Function that adds the required CORS headers to the response on an OPTIONS request */ function addKasadaCORSHeaders(response: Response): void { const kasadaHeaders = [ 'x-kpsdk-ct', 'x-kpsdk-cd', 'x-kpsdk-h', 'x-kpsdk-fc', 'x-kpsdk-v', 'x-kpsdk-r', ].join(', '); response.headers.append('access-control-allow-headers', kasadaHeaders); } export async function kasadaHandler( request: NextRequest, ev: NextFetchEvent, ): Promise<Response> { // If the request is an OPTIONS request we don't send it to Kasada // but we do add the necessary CORS headers. if (request.method === 'OPTIONS') { const response = await callOrigin(); addKasadaCORSHeaders(response); return response; } // Get the classification and associated Kasada metadata about this request const { error, metadata } = await getKasadaMetadata(request); if (error || metadata === undefined || metadata.error) { console.error('Kasada error', error || metadata?.error); return callOrigin(); } if (metadata.classification !== 'ALLOWED') { console.info('Kasada metadata bot', metadata.classification, metadata); } else { console.log('Kasada metadata', metadata.classification, metadata); } // If the request is a Bad Bot and we're in Protect mode, we'll block this request // and add the Kasada headers to the response for the Client-side SDKs if ( metadata.classification === 'BAD-BOT' && metadata.application.mode === 'PROTECT' ) { const blockResponse = new Response(undefined, { status: 429, }); addKasadaHeaders(metadata, blockResponse); return blockResponse; } // No Bad Bot detected (or application is not in Protect mode) // let's send the request to the Origin and add Kasada headers to response const response = await callOrigin(); addKasadaHeaders(metadata, response); return response; } --- File: /ai/examples/next-openai-kasada-bot-protection/middleware.ts --- import type { NextFetchEvent, NextRequest } from 'next/server'; import { kasadaHandler } from './kasada/kasada-server'; export async function middleware(req: NextRequest, ev: NextFetchEvent) { if (req.method === 'POST') { if (process.env.NODE_ENV === 'development') { return undefined; } return kasadaHandler(req, ev); } } export const config = { matcher: ['/api/chat'] }; --- File: /ai/examples/next-openai-kasada-bot-protection/next.config.js --- /** @type {import('next').NextConfig} */ const nextConfig = {}; module.exports = nextConfig; --- File: /ai/examples/next-openai-kasada-bot-protection/postcss.config.js --- module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; --- File: /ai/examples/next-openai-kasada-bot-protection/tailwind.config.js --- /** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', }, }, }, plugins: [], }; --- File: /ai/examples/next-openai-pages/app/api/call-tool/route.ts --- import { convertToModelMessages, streamText, UIMessage } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod/v4'; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4'), system: 'You are a helpful assistant.', messages: convertToModelMessages(messages), tools: { celsiusToFahrenheit: { description: 'Converts celsius to fahrenheit', inputSchema: z.object({ value: z.string().describe('The value in celsius'), }), execute: async ({ value }) => { const celsius = parseFloat(value); const fahrenheit = celsius * (9 / 5) + 32; return `${celsius}°C is ${fahrenheit.toFixed(2)}°F`; }, }, }, }); return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai-pages/app/api/generate-chat/route.ts --- import { ModelMessage, generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; export async function POST(req: Request) { const { messages }: { messages: ModelMessage[] } = await req.json(); const { response } = await generateText({ model: openai('gpt-4'), system: 'You are a helpful assistant.', messages, }); return Response.json({ messages: response.messages }); } --- File: /ai/examples/next-openai-pages/app/api/generate-object/route.ts --- import { generateObject } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod/v4'; export async function POST(req: Request) { const { prompt }: { prompt: string } = await req.json(); const { object } = await generateObject({ model: openai('gpt-4'), system: 'You are a helpful assistant.', prompt, schema: z.object({ notifications: z.array( z.object({ name: z.string().describe('Name of a fictional person.'), message: z.string().describe('Do not use emojis or links.'), minutesAgo: z.number(), }), ), }), }); return Response.json({ object }); } --- File: /ai/examples/next-openai-pages/app/api/generate-text/route.ts --- import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; export async function POST(req: Request) { const { prompt }: { prompt: string } = await req.json(); const { text } = await generateText({ model: openai('gpt-4'), system: 'You are a helpful assistant.', prompt, }); return Response.json({ text }); } --- File: /ai/examples/next-openai-pages/app/api/stream-chat/route.ts --- import { ModelMessage, streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; export async function POST(req: Request) { const { messages }: { messages: ModelMessage[] } = await req.json(); const result = streamText({ model: openai('gpt-4'), system: 'You are a helpful assistant.', messages, }); return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai-pages/app/api/stream-object/route.ts --- import { openai } from '@ai-sdk/openai'; import { streamObject } from 'ai'; import { z } from 'zod/v4'; export const maxDuration = 30; export async function POST(req: Request) { const context = await req.json(); const result = streamObject({ model: openai('gpt-4-turbo'), schema: z.object({ notifications: z.array( z.object({ name: z.string().describe('Name of a fictional person.'), message: z.string().describe('Message. Do not use emojis or links.'), }), ), }), prompt: `Generate 3 notifications for a messages app in this context:` + context, }); return result.toTextStreamResponse(); } --- File: /ai/examples/next-openai-pages/app/api/stream-text/route.ts --- import { streamText } from 'ai'; import { openai } from '@ai-sdk/openai'; export async function POST(req: Request) { const { prompt }: { prompt: string } = await req.json(); const result = streamText({ model: openai('gpt-4'), system: 'You are a helpful assistant.', prompt, }); return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai-pages/pages/api/chat-api-route.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import { NextApiRequest, NextApiResponse } from 'next'; export default async function handler( request: NextApiRequest, response: NextApiResponse, ) { const { messages } = await request.body; const result = streamText({ model: openai('gpt-4-turbo-preview'), messages, }); // write the data stream to the response // Note: this is sent as a single response, not a stream result.pipeUIMessageStreamToResponse(response); } --- File: /ai/examples/next-openai-pages/pages/api/chat-edge.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; export const runtime = 'edge'; export default async function handler(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4-turbo-preview'), messages, }); return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai-pages/pages/basics/generate-object/index.tsx --- import { useState } from 'react'; export default function Page() { const [generation, setGeneration] = useState(''); const [isLoading, setIsLoading] = useState(false); return ( <div className="p-2 flex flex-col gap-2"> <div className="p-2 bg-zinc-100 cursor-pointer" onClick={async () => { setIsLoading(true); await fetch('/api/generate-object', { method: 'POST', body: JSON.stringify({ prompt: 'Messages during finals week.', }), }).then(response => { response.json().then(json => { console.log(json); setGeneration(JSON.stringify(json.object, null, 2)); setIsLoading(false); }); }); }} > Generate </div> {isLoading ? ( 'Loading...' ) : ( <pre className="text-sm w-full whitespace-pre-wrap" data-testid="generation" > {generation} </pre> )} </div> ); } --- File: /ai/examples/next-openai-pages/pages/basics/generate-text/index.tsx --- import { useState } from 'react'; export default function Page() { const [generation, setGeneration] = useState(''); const [isLoading, setIsLoading] = useState(false); return ( <div className="p-2 flex flex-col gap-2"> <div className="p-2 bg-zinc-100 cursor-pointer" onClick={async () => { setIsLoading(true); await fetch('/api/generate-text', { method: 'POST', body: JSON.stringify({ prompt: 'Why is the sky blue?', }), }).then(response => { response.json().then(json => { setGeneration(json.text); setIsLoading(false); }); }); }} > Generate </div> {isLoading ? ( 'Loading...' ) : ( <div data-testid="generation">generation</div> )} </div> ); } --- File: /ai/examples/next-openai-pages/pages/basics/stream-object/index.tsx --- import { experimental_useObject } from '@ai-sdk/react'; import { z } from 'zod'; export default function Page() { const { object, submit } = experimental_useObject({ api: '/api/stream-object', schema: z.object({ content: z.string() }), }); return ( <div className="p-2 flex flex-col gap-2"> <div className="p-2 bg-zinc-100 cursor-pointer" onClick={async () => { submit('Final exams'); }} > Generate </div> <pre className="text-sm w-full whitespace-pre-wrap" data-testid="generation" > {JSON.stringify(object, null, 2)} </pre> </div> ); } --- File: /ai/examples/next-openai-pages/pages/basics/stream-text/index.tsx --- 'use client'; import { useCompletion } from '@ai-sdk/react'; export default function Page() { const { completion, complete } = useCompletion({ api: '/api/stream-text', }); return ( <div className="p-2 flex flex-col gap-2"> <div className="p-2 bg-zinc-100 cursor-pointer" onClick={async () => { await complete('Why is the sky blue?'); }} > Generate </div> <div data-testid="generation">{completion}</div> </div> ); } --- File: /ai/examples/next-openai-pages/pages/chat/generate-chat/index.tsx --- import { ModelMessage } from 'ai'; import { useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const [messages, setMessages] = useState<ModelMessage[]>([]); return ( <div className="flex flex-col gap-2"> <div className="flex flex-col p-2 gap-2"> {messages.map((message, index) => ( <div key={`${message.role}-${index}`} className="flex flex-row gap-2"> <div className="w-24 text-zinc-500">{`${message.role}: `}</div> <div className="w-full"> {typeof message.content === 'string' ? message.content : message.content .filter(part => part.type === 'text') .map((part, partIndex) => ( // @ts-ignore <div key={partIndex}>{part.text}</div> ))} </div> </div> ))} </div> <div className="fixed bottom-0 p-2 w-full"> <input value={input} placeholder="Send message..." onChange={event => { setInput(event.target.value); }} className="bg-zinc-100 w-full p-2" onKeyDown={async event => { if (event.key === 'Enter') { setInput(''); setMessages(currentMessages => [ ...currentMessages, { role: 'user', content: input }, ]); const response = await fetch('/api/generate-chat', { method: 'POST', body: JSON.stringify({ messages: [...messages, { role: 'user', content: input }], }), }); const { messages: newMessages } = await response.json(); setMessages(currentMessages => [ ...currentMessages, ...newMessages, ]); } }} /> </div> </div> ); } --- File: /ai/examples/next-openai-pages/pages/chat/stream-chat/index.tsx --- import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage, status } = useChat({ transport: new DefaultChatTransport({ api: '/api/stream-chat' }), }); return ( <div className="flex flex-col gap-2"> <div className="flex flex-col gap-2 p-2"> {messages.map(message => ( <div key={message.id} className="flex flex-row gap-2"> <div className="w-24 text-zinc-500">{`${message.role}: `}</div> <div className="w-full"> {message.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> </div> ))} </div> <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} className="fixed bottom-0 w-full p-2" > <input value={input} placeholder="Send message..." onChange={e => setInput(e.target.value)} className="w-full p-2 bg-zinc-100" disabled={status !== 'ready'} /> </form> </div> ); } --- File: /ai/examples/next-openai-pages/pages/chat/stream-chat-api-route/index.tsx --- import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage, status } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat-api-route' }), }); return ( <div className="flex flex-col gap-2"> <div className="flex flex-col gap-2 p-2"> {messages.map(message => ( <div key={message.id} className="flex flex-row gap-2"> <div className="w-24 text-zinc-500">{`${message.role}: `}</div> <div className="w-full"> {message.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> </div> ))} </div> <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} className="fixed bottom-0 w-full p-2" > <input value={input} placeholder="Send message..." onChange={e => setInput(e.target.value)} className="w-full p-2 bg-zinc-100" disabled={status !== 'ready'} /> </form> </div> ); } --- File: /ai/examples/next-openai-pages/pages/chat/stream-chat-edge/index.tsx --- import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage, status } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat-edge' }), }); return ( <div className="flex flex-col gap-2"> <div className="flex flex-col gap-2 p-2"> {messages.map(message => ( <div key={message.id} className="flex flex-row gap-2"> <div className="w-24 text-zinc-500">{`${message.role}: `}</div> <div className="w-full"> {message.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> </div> ))} </div> <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} className="fixed bottom-0 w-full p-2" > <input value={input} placeholder="Send message..." onChange={e => setInput(e.target.value)} className="w-full p-2 bg-zinc-100" disabled={status !== 'ready'} /> </form> </div> ); } --- File: /ai/examples/next-openai-pages/pages/tools/call-tool/index.tsx --- import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport, isToolUIPart } from 'ai'; import { useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/call-tool' }), }); return ( <div className="flex flex-col gap-2"> <div className="flex flex-col gap-2 p-2"> {messages.map(message => ( <div key={message.id} className="flex flex-row gap-2"> <strong>{`${message.role}: `}</strong> {message.parts.map((part, index) => { if (part.type === 'text') { return <div key={index}>{part.text}</div>; } else if (isToolUIPart(part)) { return <div key={index}>{JSON.stringify(part.input)}</div>; } })} </div> ))} </div> <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} className="fixed bottom-0 p-2 w-full" > <input value={input} placeholder="Send message..." onChange={e => setInput(e.target.value)} className="p-2 w-full bg-zinc-100" /> </form> </div> ); } --- File: /ai/examples/next-openai-pages/pages/_app.tsx --- import '@/styles/globals.css'; import type { AppProps } from 'next/app'; export default function App({ Component, pageProps }: AppProps) { return <Component {...pageProps} />; } --- File: /ai/examples/next-openai-pages/pages/_document.tsx --- import { Html, Head, Main, NextScript } from 'next/document'; export default function Document() { return ( <Html lang="en"> <Head /> <body> <Main /> <NextScript /> </body> </Html> ); } --- File: /ai/examples/next-openai-pages/pages/index.tsx --- import Link from 'next/link'; const examples = [ { title: 'Generate text', link: '/basics/generate-text', }, { title: 'Stream text', link: '/basics/stream-text', }, { title: 'Generate object', link: '/basics/generate-object', }, { title: 'Stream object', link: '/basics/stream-object', }, { title: 'Generate chat completion', link: '/chat/generate-chat', }, { title: 'Stream chat completion', link: '/chat/stream-chat', }, { title: 'Stream chat completion (API route)', link: '/chat/stream-chat-api-route', }, { title: 'Stream chat completion (edge runtime)', link: '/chat/stream-chat-edge', }, { title: 'Call tools', link: '/tools/call-tool', }, { title: 'Call tools in parallel', link: '/tools/call-tools-in-parallel', }, { title: 'Route components using language model', link: '/generative-user-interface/route-components', }, { title: 'Stream OpenAI Assistant API response', link: '/assistants/stream-assistant-response', }, { title: 'Stream OpenAI Assistant API response with tool calls', link: '/assistants/stream-assistant-response-with-tools', }, { title: 'Stream OpenAI Assistant API response and switch between threads', link: '/assistants/stream-assistant-switch-threads', }, ]; export default function Home() { return ( <main className={`flex flex-col gap-2 p-2`}> {examples.map((example, index) => ( <Link key={example.link} className="flex flex-row" href={example.link}> <div className="w-8 text-zinc-400">{index + 1}.</div> <div className="hover:underline">{example.title}</div> </Link> ))} </main> ); } --- File: /ai/examples/next-openai-pages/next.config.js --- /** @type {import('next').NextConfig} */ const nextConfig = {}; module.exports = nextConfig; --- File: /ai/examples/next-openai-pages/postcss.config.js --- module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; --- File: /ai/examples/next-openai-pages/tailwind.config.js --- /** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', }, }, }, plugins: [], }; --- File: /ai/examples/next-openai-telemetry/app/api/text/route.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; export async function POST(req: Request) { const { prompt } = await req.json(); const { text } = await generateText({ model: openai('gpt-4-turbo'), maxOutputTokens: 100, prompt, experimental_telemetry: { isEnabled: true, functionId: 'example-function-id', metadata: { example: 'value' }, }, }); return new Response(JSON.stringify({ text }), { headers: { 'Content-Type': 'application/json' }, }); } --- File: /ai/examples/next-openai-telemetry/app/layout.tsx --- import './globals.css'; export const metadata = { title: 'AI SDK - Next.js OpenAI Examples', description: 'Examples of using the AI SDK with Next.js and OpenAI.', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body>{children}</body> </html> ); } --- File: /ai/examples/next-openai-telemetry/app/page.tsx --- 'use client'; import { useState } from 'react'; export default function Page() { const [generation, setGeneration] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<Error | null>(null); return ( <div className="flex flex-col items-center justify-center min-h-screen p-4 bg-gray-100"> <button className="px-4 py-2 font-bold text-white bg-blue-500 rounded hover:bg-blue-700" onClick={async () => { try { setIsLoading(true); const response = await fetch('/api/text', { method: 'POST', body: JSON.stringify({ prompt: 'Why is the sky blue?', }), headers: { 'Content-Type': 'application/json', }, }); const json = await response.json(); setGeneration(json.text); } catch (error) { setError(error as Error); } finally { setIsLoading(false); } }} > Generate </button> {error && <div className="text-red-500">{error.message}</div>} <div className="mt-4"> {isLoading ? ( <span className="text-blue-500">Loading...</span> ) : ( <span className="text-gray-800">{generation}</span> )} </div> </div> ); } --- File: /ai/examples/next-openai-telemetry/instrumentation.ts --- import { registerOTel } from '@vercel/otel'; export function register() { registerOTel({ serviceName: 'next-app', }); } --- File: /ai/examples/next-openai-telemetry/next.config.js --- /** @type {import('next').NextConfig} */ const nextConfig = {}; module.exports = nextConfig; --- File: /ai/examples/next-openai-telemetry/postcss.config.js --- module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; --- File: /ai/examples/next-openai-telemetry/tailwind.config.js --- /** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', }, }, }, plugins: [], }; --- File: /ai/examples/next-openai-telemetry-sentry/app/api/text/route.ts --- import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; export async function POST(req: Request) { const { prompt } = await req.json(); const { text } = await generateText({ model: openai('gpt-4-turbo'), maxOutputTokens: 100, prompt, experimental_telemetry: { isEnabled: true, functionId: 'example-function-id', metadata: { example: 'value' }, }, }); return new Response(JSON.stringify({ text }), { headers: { 'Content-Type': 'application/json' }, }); } --- File: /ai/examples/next-openai-telemetry-sentry/app/layout.tsx --- import './globals.css'; export const metadata = { title: 'AI SDK - Next.js OpenAI Examples', description: 'Examples of using the AI SDK with Next.js and OpenAI.', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body>{children}</body> </html> ); } --- File: /ai/examples/next-openai-telemetry-sentry/app/page.tsx --- 'use client'; import { useState } from 'react'; export default function Page() { const [generation, setGeneration] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<Error | null>(null); return ( <div className="flex flex-col items-center justify-center min-h-screen p-4 bg-gray-100"> <button className="px-4 py-2 font-bold text-white bg-blue-500 rounded hover:bg-blue-700" onClick={async () => { try { setIsLoading(true); const response = await fetch('/api/text', { method: 'POST', body: JSON.stringify({ prompt: 'Why is the sky blue?', }), headers: { 'Content-Type': 'application/json', }, }); const json = await response.json(); setGeneration(json.text); } catch (error) { setError(error as Error); } finally { setIsLoading(false); } }} > Generate </button> {error && <div className="text-red-500">{error.message}</div>} <div className="mt-4"> {isLoading ? ( <span className="text-blue-500">Loading...</span> ) : ( <span className="text-gray-800">{generation}</span> )} </div> </div> ); } --- File: /ai/examples/next-openai-telemetry-sentry/instrumentation.ts --- import { registerOTel } from '@vercel/otel'; export async function register() { if (process.env.NEXT_RUNTIME === 'nodejs') { await import('./sentry.server.config'); } if (process.env.NEXT_RUNTIME === 'edge') { await import('./sentry.edge.config'); } registerOTel({ serviceName: 'next-app', }); } --- File: /ai/examples/next-openai-telemetry-sentry/next.config.js --- const { withSentryConfig } = require('@sentry/nextjs'); /** @type {import('next').NextConfig} */ const nextConfig = {}; module.exports = withSentryConfig(nextConfig, { org: process.env.SENTRY_ORG, project: process.env.SENTRY_PROJECT, // An auth token is required for uploading source maps. authToken: process.env.SENTRY_AUTH_TOKEN, silent: false, // Can be used to suppress logs }); --- File: /ai/examples/next-openai-telemetry-sentry/postcss.config.js --- module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; --- File: /ai/examples/next-openai-telemetry-sentry/sentry.client.config.js --- import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, // Replay may only be enabled for the client-side integrations: [Sentry.replayIntegration()], // Set tracesSampleRate to 1.0 to capture 100% // of transactions for tracing. // We recommend adjusting this value in production tracesSampleRate: 1.0, // Capture Replay for 10% of all sessions, // plus for 100% of sessions with an error replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1.0, // ... // Note: if you want to override the automatic release value, do not set a // `release` value here - use the environment variable `SENTRY_RELEASE`, so // that it will also get attached to your source maps }); --- File: /ai/examples/next-openai-telemetry-sentry/sentry.edge.config.js --- import * as Sentry from '@sentry/nextjs'; const { GenericPoolInstrumentation, } = require('@opentelemetry/instrumentation-generic-pool'); Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, // Set tracesSampleRate to 1.0 to capture 100% // of transactions for tracing. // We recommend adjusting this value in production tracesSampleRate: 1.0, // ... // Note: if you want to override the automatic release value, do not set a // `release` value here - use the environment variable `SENTRY_RELEASE`, so // that it will also get attached to your source maps }); Sentry.addOpenTelemetryInstrumentation(new GenericPoolInstrumentation()); --- File: /ai/examples/next-openai-telemetry-sentry/sentry.server.config.js --- import * as Sentry from '@sentry/nextjs'; const { GenericPoolInstrumentation, } = require('@opentelemetry/instrumentation-generic-pool'); Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, // Set tracesSampleRate to 1.0 to capture 100% // of transactions for tracing. // We recommend adjusting this value in production tracesSampleRate: 1.0, // ... // Note: if you want to override the automatic release value, do not set a // `release` value here - use the environment variable `SENTRY_RELEASE`, so // that it will also get attached to your source maps }); Sentry.addOpenTelemetryInstrumentation(new GenericPoolInstrumentation()); --- File: /ai/examples/next-openai-telemetry-sentry/tailwind.config.js --- /** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', }, }, }, plugins: [], }; --- File: /ai/examples/next-openai-upstash-rate-limits/app/api/chat/route.ts --- import { openai } from '@ai-sdk/openai'; import { Ratelimit } from '@upstash/ratelimit'; import { kv } from '@vercel/kv'; import { streamText } from 'ai'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { // eslint-disable-next-line turbo/no-undeclared-env-vars if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) { const ip = req.headers.get('x-forwarded-for'); const ratelimit = new Ratelimit({ redis: kv, // rate limit to 5 requests per 10 seconds limiter: Ratelimit.slidingWindow(5, '10s'), }); const { success, limit, reset, remaining } = await ratelimit.limit( `ratelimit_${ip}`, ); if (!success) { return new Response('You have reached your request limit for the day.', { status: 429, headers: { 'X-RateLimit-Limit': limit.toString(), 'X-RateLimit-Remaining': remaining.toString(), 'X-RateLimit-Reset': reset.toString(), }, }); } } // Extract the `messages` from the body of the request const { messages } = await req.json(); // Call the language model const result = streamText({ model: openai('gpt-4o'), messages, }); // Respond with the stream return result.toUIMessageStreamResponse(); } --- File: /ai/examples/next-openai-upstash-rate-limits/app/layout.tsx --- import './globals.css'; import Toaster from './toaster'; export const metadata = { title: 'Create Next App', description: 'Generated by create next app', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <Toaster /> <body>{children}</body> </html> ); } --- File: /ai/examples/next-openai-upstash-rate-limits/app/page.tsx --- 'use client'; import { useChat } from '@ai-sdk/react'; import { useState } from 'react'; import { toast } from 'sonner'; export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat({ onError: err => toast.error(err.message), }); return ( <div className="flex flex-col py-24 mx-auto w-full max-w-md stretch"> {messages.length > 0 ? messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> )) : null} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input className="fixed bottom-0 p-2 mb-8 w-full max-w-md rounded border border-gray-300 shadow-xl" value={input} placeholder="Say something..." onChange={e => setInput(e.target.value)} /> </form> </div> ); } --- File: /ai/examples/next-openai-upstash-rate-limits/app/toaster.tsx --- 'use client'; export { Toaster as default } from 'sonner'; --- File: /ai/examples/next-openai-upstash-rate-limits/next.config.js --- /** @type {import('next').NextConfig} */ const nextConfig = {}; module.exports = nextConfig; --- File: /ai/examples/next-openai-upstash-rate-limits/postcss.config.js --- module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; --- File: /ai/examples/next-openai-upstash-rate-limits/tailwind.config.js --- /** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', }, }, }, plugins: [], }; --- File: /ai/examples/node-http-server/src/server.ts --- import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import 'dotenv/config'; import { createServer } from 'http'; createServer(async (req, res) => { switch (req.url) { case '/': { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.pipeUIMessageStreamToResponse(res); break; } case '/stream-data': { const result = streamText({ model: openai('gpt-4o'), prompt: 'Invent a new holiday and describe its traditions.', }); result.pipeUIMessageStreamToResponse(res, { onError: error => { // Error messages are masked by default for security reasons. // If you want to expose the error message to the client, you can do so here: return error instanceof Error ? error.message : String(error); }, }); break; } } }).listen(8080); --- File: /ai/examples/nuxt-openai/server/api/chat-with-vision.ts --- import { createOpenAI } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, type UIMessage } from 'ai'; export default defineLazyEventHandler(async () => { const apiKey = useRuntimeConfig().openaiApiKey; if (!apiKey) throw new Error('Missing OpenAI API key'); const openai = createOpenAI({ apiKey }); return defineEventHandler(async (event: any) => { // Extract the `prompt` from the body of the request const { messages, data } = await readBody(event); const initialMessages = convertToModelMessages(messages.slice(0, -1)); const currentMessage = messages[messages.length - 1] as UIMessage; // Ask OpenAI for a streaming chat completion given the prompt const response = streamText({ model: openai('gpt-4o'), maxOutputTokens: 150, messages: [ ...initialMessages, { role: 'user', content: [ { type: 'text', text: currentMessage.parts .map(part => (part.type === 'text' ? part.text : '')) .join(''), }, { type: 'image', image: new URL(data.imageUrl) }, ], }, ], }); // Respond with the stream return response.toUIMessageStreamResponse(); }); }); --- File: /ai/examples/nuxt-openai/server/api/chat.ts --- import { createOpenAI } from '@ai-sdk/openai'; import { convertToModelMessages, streamText } from 'ai'; export default defineLazyEventHandler(async () => { const openai = createOpenAI({ apiKey: useRuntimeConfig().openaiApiKey, }); return defineEventHandler(async (event: any) => { // Extract the `messages` from the body of the request const { messages } = await readBody(event); console.log('messages', messages); // Call the language model const result = streamText({ model: openai('gpt-4-turbo'), messages: convertToModelMessages(messages), async onFinish({ text, toolCalls, toolResults, usage, finishReason }) { // implement your own logic here, e.g. for storing messages // or recording token usage }, }); // Respond with the stream return result.toUIMessageStreamResponse(); }); }); --- File: /ai/examples/nuxt-openai/server/api/completion.ts --- import { createOpenAI } from '@ai-sdk/openai'; import { streamText } from 'ai'; export default defineLazyEventHandler(async () => { const apiKey = useRuntimeConfig().openaiApiKey; if (!apiKey) throw new Error('Missing OpenAI API key'); const openai = createOpenAI({ apiKey }); return defineEventHandler(async (event: any) => { // Extract the `prompt` from the body of the request const { prompt } = await readBody(event); // Ask OpenAI for a streaming chat completion given the prompt const result = streamText({ model: openai('gpt-4o'), prompt, }); // Respond with the stream return result.toUIMessageStreamResponse(); }); }); --- File: /ai/examples/nuxt-openai/server/api/use-chat-request.ts --- import { createOpenAI } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, type UIMessage } from 'ai'; export default defineLazyEventHandler(async () => { const openai = createOpenAI({ apiKey: useRuntimeConfig().openaiApiKey, }); return defineEventHandler(async (event: any) => { // Extract the `messages` from the body of the request const { message } = await readBody(event); // Implement your own logic here to add message history const previousMessages: UIMessage[] = []; const messages = [...previousMessages, message]; // Call the language model const result = streamText({ model: openai('gpt-4o-mini'), messages: convertToModelMessages(messages), async onFinish({ text, toolCalls, toolResults, usage, finishReason }) { // Implement your own logic here, e.g. for storing messages }, }); // Respond with the stream return result.toUIMessageStreamResponse(); }); }); --- File: /ai/examples/nuxt-openai/server/api/use-chat-tools.ts --- import { createOpenAI } from '@ai-sdk/openai'; import { convertToModelMessages, stepCountIs, streamText } from 'ai'; import { z } from 'zod/v4'; export default defineLazyEventHandler(async () => { const openai = createOpenAI({ apiKey: useRuntimeConfig().openaiApiKey, }); return defineEventHandler(async (event: any) => { const { messages } = await readBody(event); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), // multi-steps for server-side tools tools: { // server-side tool with execute function: getWeatherInformation: { description: 'show the weather in a given city to the user', inputSchema: z.object({ city: z.string() }), execute: async ({}: { city: string }) => { // Add artificial delay of 2 seconds await new Promise(resolve => setTimeout(resolve, 2000)); const weatherOptions = [ 'sunny', 'cloudy', 'rainy', 'snowy', 'windy', ]; return weatherOptions[ Math.floor(Math.random() * weatherOptions.length) ]; }, }, // client-side tool that starts user interaction: askForConfirmation: { description: 'Ask the user for confirmation.', inputSchema: z.object({ message: z .string() .describe('The message to ask for confirmation.'), }), }, // client-side tool that is automatically executed on the client: getLocation: { description: 'Get the user location. Always ask for confirmation before using this tool.', inputSchema: z.object({}), }, }, }); return result.toUIMessageStreamResponse(); }); }); --- File: /ai/examples/nuxt-openai/nuxt.config.ts --- // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ devtools: { enabled: true }, modules: ['@nuxtjs/tailwindcss'], nitro: { preset: 'vercel-edge', // you can use 'vercel' or other providers here }, runtimeConfig: { openaiApiKey: '', assistantId: '', }, compatibilityDate: '2024-07-05', telemetry: false, }); --- File: /ai/examples/sveltekit-openai/src/lib/components/ui/button/index.ts --- import Root, { type ButtonProps, type ButtonSize, type ButtonVariant, buttonVariants, } from './button.svelte'; export { Root, type ButtonProps as Props, // Root as Button, buttonVariants, type ButtonProps, type ButtonSize, type ButtonVariant, }; --- File: /ai/examples/sveltekit-openai/src/lib/components/ui/textarea/index.ts --- import Root from './textarea.svelte'; export { Root, // Root as Textarea, }; --- File: /ai/examples/sveltekit-openai/src/lib/utils.ts --- import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } --- File: /ai/examples/sveltekit-openai/src/routes/api/chat/+server.ts --- import { env } from '$env/dynamic/private'; import { createOpenAI } from '@ai-sdk/openai'; import { convertToModelMessages, streamText, stepCountIs } from 'ai'; import { z } from 'zod/v4'; const openai = createOpenAI({ apiKey: env?.OPENAI_API_KEY, }); export const POST = async ({ request }: { request: Request }) => { const { messages } = await request.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), // multi-steps for server-side tools tools: { // server-side tool with execute function: getWeatherInformation: { description: 'show the weather in a given city to the user', inputSchema: z.object({ city: z.string() }), execute: async ({ city: _ }: { city: string }) => { // Add artificial delay of 2 seconds await new Promise(resolve => setTimeout(resolve, 2000)); const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy']; return weatherOptions[ Math.floor(Math.random() * weatherOptions.length) ]; }, }, // client-side tool that starts user interaction: askForConfirmation: { description: 'Ask the user for confirmation.', inputSchema: z.object({ message: z.string().describe('The message to ask for confirmation.'), }), }, // client-side tool that is automatically executed on the client: getLocation: { description: 'Get the user location. Always ask for confirmation before using this tool.', inputSchema: z.object({}), }, }, onError: error => { console.error(error); }, }); return result.toUIMessageStreamResponse(); }; --- File: /ai/examples/sveltekit-openai/src/routes/api/completion/+server.ts --- import { createOpenAI } from '@ai-sdk/openai'; import { streamText } from 'ai'; import { env } from '$env/dynamic/private'; const openai = createOpenAI({ apiKey: env?.OPENAI_API_KEY, }); const system = ` Generate a completion for the given prompt. Your completion should never start with the text of the prompt, but should continue the prompt in a natural way. Your completion should provide a maximum of 100 additional words. Only provide completions you're highly confident are likely to be accurate. Here are some examples: - Prompt: "Hello," Completion: "Hello, world!" - Prompt: "The capital of France is" Completion: "The capital of France is Paris." - Prompt: "We the people" Completion: "We the people of the United States, in order to form a more perfect union, establish justice, insure domestic tranquility, provide for the common defense, promote the general welfare, and secure the blessings of liberty to ourselves and our posterity, do ordain and establish this Constitution for the United States of America." It is VERY IMPORTANT that your completion continues the prompt and does not repeat the prompt. `; export const POST = async ({ request }: { request: Request }) => { const { prompt } = await request.json(); const result = streamText({ model: openai('gpt-4o'), system, prompt, onError: error => { console.error(error); }, }); return result.toUIMessageStreamResponse(); }; --- File: /ai/examples/sveltekit-openai/src/routes/api/structured-object/+server.ts --- import { streamObject } from 'ai'; import { notificationSchema } from '../../structured-object/schema.js'; import { createOpenAI } from '@ai-sdk/openai'; import { env } from '$env/dynamic/private'; const openai = createOpenAI({ apiKey: env?.OPENAI_API_KEY, }); export async function POST({ request }: { request: Request }) { const context = await request.json(); const result = streamObject({ model: openai('gpt-4o'), schema: notificationSchema, prompt: `Generate 3 notifications for a messages app in this context:` + context, onError: error => { console.error(error); }, }); return result.toTextStreamResponse(); } --- File: /ai/examples/sveltekit-openai/src/routes/structured-object/schema.ts --- import { z } from 'zod/v4'; // define a schema for the notifications export const notificationSchema = z.object({ notifications: z.array( z.object({ name: z.string().describe('Name of a fictional person.'), message: z.string().describe('Message. Do not use emojis or links.'), }), ), }); --- File: /ai/examples/sveltekit-openai/src/app.d.ts --- // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces declare global { namespace App { // interface Error {} // interface Locals {} // interface PageData {} // interface PageState {} // interface Platform {} } } export {}; --- File: /ai/examples/sveltekit-openai/eslint.config.js --- import prettier from 'eslint-config-prettier'; import js from '@eslint/js'; import { includeIgnoreFile } from '@eslint/compat'; import svelte from 'eslint-plugin-svelte'; import globals from 'globals'; import { fileURLToPath } from 'node:url'; import ts from 'typescript-eslint'; import svelteConfig from './svelte.config.js'; const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); export default ts.config( includeIgnoreFile(gitignorePath), js.configs.recommended, ...ts.configs.recommended, ...svelte.configs.recommended, prettier, ...svelte.configs.prettier, { rules: { 'no-undef': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { args: 'all', argsIgnorePattern: '^_', caughtErrors: 'all', caughtErrorsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_', varsIgnorePattern: '^_', ignoreRestSiblings: true, }, ], }, languageOptions: { globals: { ...globals.browser, ...globals.node, }, }, }, { files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], ignores: ['eslint.config.js', 'svelte.config.js'], languageOptions: { parserOptions: { projectService: true, extraFileExtensions: ['.svelte'], parser: ts.parser, svelteConfig, }, }, }, ); --- File: /ai/examples/sveltekit-openai/postcss.config.js --- export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; --- File: /ai/examples/sveltekit-openai/svelte.config.js --- import adapter from '@sveltejs/adapter-vercel'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { // Consult https://svelte.dev/docs/kit/integrations // for more information about preprocessors preprocess: vitePreprocess(), kit: { adapter: adapter(), }, }; export default config; --- File: /ai/examples/sveltekit-openai/tailwind.config.ts --- import { fontFamily } from 'tailwindcss/defaultTheme'; import type { Config } from 'tailwindcss'; import tailwindcssAnimate from 'tailwindcss-animate'; const config: Config = { darkMode: ['class'], content: ['./src/**/*.{html,js,svelte,ts}'], safelist: ['dark'], theme: { container: { center: true, padding: '2rem', screens: { '2xl': '1400px', }, }, extend: { colors: { border: 'hsl(var(--border) / <alpha-value>)', input: 'hsl(var(--input) / <alpha-value>)', ring: 'hsl(var(--ring) / <alpha-value>)', background: 'hsl(var(--background) / <alpha-value>)', foreground: 'hsl(var(--foreground) / <alpha-value>)', primary: { DEFAULT: 'hsl(var(--primary) / <alpha-value>)', foreground: 'hsl(var(--primary-foreground) / <alpha-value>)', }, secondary: { DEFAULT: 'hsl(var(--secondary) / <alpha-value>)', foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)', }, destructive: { DEFAULT: 'hsl(var(--destructive) / <alpha-value>)', foreground: 'hsl(var(--destructive-foreground) / <alpha-value>)', }, muted: { DEFAULT: 'hsl(var(--muted) / <alpha-value>)', foreground: 'hsl(var(--muted-foreground) / <alpha-value>)', }, accent: { DEFAULT: 'hsl(var(--accent) / <alpha-value>)', foreground: 'hsl(var(--accent-foreground) / <alpha-value>)', }, popover: { DEFAULT: 'hsl(var(--popover) / <alpha-value>)', foreground: 'hsl(var(--popover-foreground) / <alpha-value>)', }, card: { DEFAULT: 'hsl(var(--card) / <alpha-value>)', foreground: 'hsl(var(--card-foreground) / <alpha-value>)', }, sidebar: { DEFAULT: 'hsl(var(--sidebar-background))', foreground: 'hsl(var(--sidebar-foreground))', primary: 'hsl(var(--sidebar-primary))', 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', accent: 'hsl(var(--sidebar-accent))', 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', border: 'hsl(var(--sidebar-border))', ring: 'hsl(var(--sidebar-ring))', }, }, borderRadius: { xl: 'calc(var(--radius) + 4px)', lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)', }, fontFamily: { sans: [...fontFamily.sans], }, keyframes: { 'accordion-down': { from: { height: '0' }, to: { height: 'var(--bits-accordion-content-height)' }, }, 'accordion-up': { from: { height: 'var(--bits-accordion-content-height)' }, to: { height: '0' }, }, 'caret-blink': { '0%,70%,100%': { opacity: '1' }, '20%,50%': { opacity: '0' }, }, }, animation: { 'accordion-down': 'accordion-down 0.2s ease-out', 'accordion-up': 'accordion-up 0.2s ease-out', 'caret-blink': 'caret-blink 1.25s ease-out infinite', }, }, }, plugins: [tailwindcssAnimate], }; export default config; --- File: /ai/examples/sveltekit-openai/vite.config.ts --- import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [sveltekit()], }); --- File: /ai/packages/ai/internal/index.ts --- // internal re-exports export { convertAsyncIteratorToReadableStream } from '@ai-sdk/provider-utils'; // internal export { convertToLanguageModelPrompt } from '../src/prompt/convert-to-language-model-prompt'; export { prepareToolsAndToolChoice } from '../src/prompt/prepare-tools-and-tool-choice'; export { standardizePrompt } from '../src/prompt/standardize-prompt'; export { prepareCallSettings } from '../src/prompt/prepare-call-settings'; export { prepareRetries } from '../src/util/prepare-retries'; --- File: /ai/packages/ai/mcp-stdio/create-child-process.test.ts --- import { beforeEach, vi } from 'vitest'; import * as getEnvironmentModule from './get-environment'; const DEFAULT_ENV = { PATH: 'path', }; const mockGetEnvironment = vi .fn() .mockImplementation((customEnv?: Record<string, string>) => { return { ...DEFAULT_ENV, ...customEnv, }; }); vi.spyOn(getEnvironmentModule, 'getEnvironment').mockImplementation( mockGetEnvironment, ); // important: import after mocking getEnv const { createChildProcess } = await import('./create-child-process'); describe('createChildProcess', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should spawn a child process', async () => { const childProcess = createChildProcess( { command: process.execPath }, new AbortController().signal, ); expect(childProcess.pid).toBeDefined(); expect(mockGetEnvironment).toHaveBeenCalledWith(undefined); childProcess.kill(); }); it('should spawn a child process with custom env', async () => { const customEnv = { FOO: 'bar' }; const childProcessWithCustomEnv = createChildProcess( { command: process.execPath, env: customEnv }, new AbortController().signal, ); expect(childProcessWithCustomEnv.pid).toBeDefined(); expect(mockGetEnvironment).toHaveBeenCalledWith(customEnv); expect(mockGetEnvironment).toHaveReturnedWith({ ...DEFAULT_ENV, ...customEnv, }); childProcessWithCustomEnv.kill(); }); it('should spawn a child process with args', async () => { const childProcessWithArgs = createChildProcess( { command: process.execPath, args: ['-c', 'echo', 'test'] }, new AbortController().signal, ); expect(childProcessWithArgs.pid).toBeDefined(); expect(childProcessWithArgs.spawnargs).toContain(process.execPath); expect(childProcessWithArgs.spawnargs).toEqual([ process.execPath, '-c', 'echo', 'test', ]); childProcessWithArgs.kill(); }); it('should spawn a child process with cwd', async () => { const childProcessWithCwd = createChildProcess( { command: process.execPath, cwd: '/tmp' }, new AbortController().signal, ); expect(childProcessWithCwd.pid).toBeDefined(); childProcessWithCwd.kill(); }); it('should spawn a child process with stderr', async () => { const childProcessWithStderr = createChildProcess( { command: process.execPath, stderr: 'pipe' }, new AbortController().signal, ); expect(childProcessWithStderr.pid).toBeDefined(); expect(childProcessWithStderr.stderr).toBeDefined(); childProcessWithStderr.kill(); }); }); --- File: /ai/packages/ai/mcp-stdio/create-child-process.ts --- import { ChildProcess, spawn } from 'node:child_process'; import { getEnvironment } from './get-environment'; import { StdioConfig } from './mcp-stdio-transport'; export function createChildProcess( config: StdioConfig, signal: AbortSignal, ): ChildProcess { return spawn(config.command, config.args ?? [], { env: getEnvironment(config.env), stdio: ['pipe', 'pipe', config.stderr ?? 'inherit'], shell: false, signal, windowsHide: globalThis.process.platform === 'win32' && isElectron(), cwd: config.cwd, }); } function isElectron() { return 'type' in globalThis.process; } --- File: /ai/packages/ai/mcp-stdio/get-environment.test.ts --- import { describe, it, expect } from 'vitest'; import { getEnvironment } from './get-environment'; describe('getEnvironment', () => { it('should not mutate the original custom environment object', () => { const customEnv = { CUSTOM_VAR: 'custom_value' }; const result = getEnvironment(customEnv); expect(customEnv).toStrictEqual({ CUSTOM_VAR: 'custom_value' }); expect(result).not.toBe(customEnv); }); }); --- File: /ai/packages/ai/mcp-stdio/get-environment.ts --- /** * Constructs the environment variables for the child process. * * @param customEnv - Custom environment variables to merge with default environment variables. * @returns The environment variables for the child process. */ export function getEnvironment( customEnv?: Record<string, string>, ): Record<string, string> { const DEFAULT_INHERITED_ENV_VARS = globalThis.process.platform === 'win32' ? [ 'APPDATA', 'HOMEDRIVE', 'HOMEPATH', 'LOCALAPPDATA', 'PATH', 'PROCESSOR_ARCHITECTURE', 'SYSTEMDRIVE', 'SYSTEMROOT', 'TEMP', 'USERNAME', 'USERPROFILE', ] : ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER']; const env: Record<string, string> = customEnv ? { ...customEnv } : {}; for (const key of DEFAULT_INHERITED_ENV_VARS) { const value = globalThis.process.env[key]; if (value === undefined) { continue; } if (value.startsWith('()')) { continue; } env[key] = value; } return env; } --- File: /ai/packages/ai/mcp-stdio/index.ts --- export { StdioMCPTransport as Experimental_StdioMCPTransport, type StdioConfig, } from './mcp-stdio-transport'; --- File: /ai/packages/ai/mcp-stdio/mcp-stdio-transport.test.ts --- import type { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { JSONRPCMessage } from '../src/tool/mcp/json-rpc-message'; import { MCPClientError } from '../src/error/mcp-client-error'; import { createChildProcess } from './create-child-process'; import { StdioMCPTransport } from './mcp-stdio-transport'; vi.mock('./create-child-process', { spy: true }); interface MockChildProcess { stdin: EventEmitter & { write?: ReturnType<typeof vi.fn> }; stdout: EventEmitter; stderr: EventEmitter; on: ReturnType<typeof vi.fn>; removeAllListeners: ReturnType<typeof vi.fn>; } describe('StdioMCPTransport', () => { let transport: StdioMCPTransport; let mockChildProcess: MockChildProcess; let mockStdin: EventEmitter & { write?: ReturnType<typeof vi.fn> }; let mockStdout: EventEmitter; beforeEach(() => { vi.clearAllMocks(); mockStdin = new EventEmitter(); mockStdout = new EventEmitter(); mockChildProcess = { stdin: mockStdin, stdout: mockStdout, stderr: new EventEmitter(), on: vi.fn(), removeAllListeners: vi.fn(), }; vi.mocked(createChildProcess).mockReturnValue( mockChildProcess as unknown as ChildProcess, ); transport = new StdioMCPTransport({ command: 'test-command', args: ['--test'], }); }); afterEach(() => { transport.close(); }); describe('start', () => { it('should successfully start the transport', async () => { const stdinOnSpy = vi.spyOn(mockStdin, 'on'); const stdoutOnSpy = vi.spyOn(mockStdout, 'on'); mockChildProcess.on.mockImplementation( (event: string, callback: () => void) => { if (event === 'spawn') { callback(); } }, ); const startPromise = transport.start(); await expect(startPromise).resolves.toBeUndefined(); expect(mockChildProcess.on).toHaveBeenCalledWith( 'error', expect.any(Function), ); expect(mockChildProcess.on).toHaveBeenCalledWith( 'spawn', expect.any(Function), ); expect(mockChildProcess.on).toHaveBeenCalledWith( 'close', expect.any(Function), ); expect(stdinOnSpy).toHaveBeenCalledWith('error', expect.any(Function)); expect(stdoutOnSpy).toHaveBeenCalledWith('error', expect.any(Function)); expect(stdoutOnSpy).toHaveBeenCalledWith('data', expect.any(Function)); }); it('should throw error if already started', async () => { mockChildProcess.on.mockImplementation( (event: string, callback: () => void) => { if (event === 'spawn') { callback(); } }, ); const firstStart = transport.start(); await expect(firstStart).resolves.toBeUndefined(); const secondStart = transport.start(); await expect(secondStart).rejects.toThrow(MCPClientError); }); it('should handle spawn errors', async () => { const error = new Error('Spawn failed'); const onErrorSpy = vi.fn(); transport.onerror = onErrorSpy; // simulate `spawn` failure by emitting error event after returning child process mockChildProcess.on.mockImplementation( (event: string, callback: (err: Error) => void) => { if (event === 'error') { callback(error); } }, ); const startPromise = transport.start(); await expect(startPromise).rejects.toThrow('Spawn failed'); expect(onErrorSpy).toHaveBeenCalledWith(error); }); }); describe('send', () => { beforeEach(async () => { mockChildProcess.on.mockImplementation( (event: string, callback: () => void) => { if (event === 'spawn') { callback(); } }, ); await transport.start(); }); it('should successfully send a message', async () => { const message: JSONRPCMessage = { jsonrpc: '2.0', id: '1', method: 'test', params: {}, }; mockStdin.write = vi.fn().mockReturnValue(true); await transport.send(message); expect(mockStdin.write).toHaveBeenCalledWith( JSON.stringify(message) + '\n', ); }); it('should handle write backpressure', async () => { const message: JSONRPCMessage = { jsonrpc: '2.0', id: '1', method: 'test', params: {}, }; mockStdin.write = vi.fn().mockReturnValue(false); const sendPromise = transport.send(message); mockStdin.emit('drain'); await expect(sendPromise).resolves.toBeUndefined(); }); it('should throw error if transport is not connected', async () => { await transport.close(); const message: JSONRPCMessage = { jsonrpc: '2.0', id: '1', method: 'test', params: {}, }; await expect(transport.send(message)).rejects.toThrow(MCPClientError); }); }); describe('message handling', () => { const onMessageSpy = vi.fn(); beforeEach(async () => { mockChildProcess.on.mockImplementation( (event: string, callback: () => void) => { if (event === 'spawn') { callback(); } }, ); transport.onmessage = onMessageSpy; await transport.start(); }); it('should handle incoming messages correctly', async () => { const message: JSONRPCMessage = { jsonrpc: '2.0', id: '1', method: 'test', params: {}, }; mockStdout.emit('data', Buffer.from(JSON.stringify(message) + '\n')); expect(onMessageSpy).toHaveBeenCalledWith(message); }); it('should handle partial messages correctly', async () => { const message = { jsonrpc: '2.0', id: '1', method: 'test', params: {}, }; const messageStr = JSON.stringify(message); mockStdout.emit('data', Buffer.from(messageStr.slice(0, 10))); mockStdout.emit('data', Buffer.from(messageStr.slice(10) + '\n')); expect(onMessageSpy).toHaveBeenCalledWith(message); }); }); describe('close', () => { const onCloseSpy = vi.fn(); beforeEach(async () => { mockChildProcess.on.mockImplementation( (event: string, callback: (code?: number) => void) => { if (event === 'spawn') { callback(); } else if (event === 'close') { callback(0); } }, ); transport.onclose = onCloseSpy; await transport.start(); }); it('should close the transport successfully', async () => { await transport.close(); expect(mockChildProcess.on).toHaveBeenCalledWith( 'close', expect.any(Function), ); expect(onCloseSpy).toHaveBeenCalled(); }); }); }); --- File: /ai/packages/ai/mcp-stdio/mcp-stdio-transport.ts --- import type { ChildProcess, IOType } from 'node:child_process'; import { Stream } from 'node:stream'; import { JSONRPCMessage, JSONRPCMessageSchema, } from '../src/tool/mcp/json-rpc-message'; import { MCPTransport } from '../src/tool/mcp/mcp-transport'; import { MCPClientError } from '../src/error/mcp-client-error'; import { createChildProcess } from './create-child-process'; export interface StdioConfig { command: string; args?: string[]; env?: Record<string, string>; stderr?: IOType | Stream | number; cwd?: string; } export class StdioMCPTransport implements MCPTransport { private process?: ChildProcess; private abortController: AbortController = new AbortController(); private readBuffer: ReadBuffer = new ReadBuffer(); private serverParams: StdioConfig; onclose?: () => void; onerror?: (error: unknown) => void; onmessage?: (message: JSONRPCMessage) => void; constructor(server: StdioConfig) { this.serverParams = server; } async start(): Promise<void> { if (this.process) { throw new MCPClientError({ message: 'StdioMCPTransport already started.', }); } return new Promise((resolve, reject) => { try { const process = createChildProcess( this.serverParams, this.abortController.signal, ); this.process = process; this.process.on('error', error => { if (error.name === 'AbortError') { this.onclose?.(); return; } reject(error); this.onerror?.(error); }); this.process.on('spawn', () => { resolve(); }); this.process.on('close', _code => { this.process = undefined; this.onclose?.(); }); this.process.stdin?.on('error', error => { this.onerror?.(error); }); this.process.stdout?.on('data', chunk => { this.readBuffer.append(chunk); this.processReadBuffer(); }); this.process.stdout?.on('error', error => { this.onerror?.(error); }); } catch (error) { reject(error); this.onerror?.(error); } }); } private processReadBuffer() { while (true) { try { const message = this.readBuffer.readMessage(); if (message === null) { break; } this.onmessage?.(message); } catch (error) { this.onerror?.(error as Error); } } } async close(): Promise<void> { this.abortController.abort(); this.process = undefined; this.readBuffer.clear(); } send(message: JSONRPCMessage): Promise<void> { return new Promise(resolve => { if (!this.process?.stdin) { throw new MCPClientError({ message: 'StdioClientTransport not connected', }); } const json = serializeMessage(message); if (this.process.stdin.write(json)) { resolve(); } else { this.process.stdin.once('drain', resolve); } }); } } class ReadBuffer { private buffer?: Buffer; append(chunk: Buffer): void { this.buffer = this.buffer ? Buffer.concat([this.buffer, chunk]) : chunk; } readMessage(): JSONRPCMessage | null { if (!this.buffer) return null; const index = this.buffer.indexOf('\n'); if (index === -1) { return null; } const line = this.buffer.toString('utf8', 0, index); this.buffer = this.buffer.subarray(index + 1); return deserializeMessage(line); } clear(): void { this.buffer = undefined; } } function serializeMessage(message: JSONRPCMessage): string { return JSON.stringify(message) + '\n'; } export function deserializeMessage(line: string): JSONRPCMessage { return JSONRPCMessageSchema.parse(JSON.parse(line)); } --- File: /ai/packages/ai/src/agent/agent.ts --- import { IdGenerator } from '@ai-sdk/provider-utils'; import { generateText, GenerateTextOnStepFinishCallback, } from '../generate-text/generate-text'; import { GenerateTextResult } from '../generate-text/generate-text-result'; import { Output } from '../generate-text/output'; import { PrepareStepFunction } from '../generate-text/prepare-step'; import { StopCondition } from '../generate-text/stop-condition'; import { streamText } from '../generate-text/stream-text'; import { StreamTextResult } from '../generate-text/stream-text-result'; import { ToolCallRepairFunction } from '../generate-text/tool-call-repair-function'; import { ToolSet } from '../generate-text/tool-set'; import { CallSettings } from '../prompt/call-settings'; import { Prompt } from '../prompt/prompt'; import { TelemetrySettings } from '../telemetry/telemetry-settings'; import { LanguageModel, ToolChoice } from '../types/language-model'; import { ProviderMetadata } from '../types/provider-metadata'; export type AgentSettings< TOOLS extends ToolSet, OUTPUT = never, OUTPUT_PARTIAL = never, > = CallSettings & { /** * The system prompt to use. */ system?: string; /** The language model to use. */ model: LanguageModel; /** The tools that the model can call. The model needs to support calling tools. */ tools?: TOOLS; /** The tool choice strategy. Default: 'auto'. */ toolChoice?: ToolChoice<NoInfer<TOOLS>>; /** Condition for stopping the generation when there are tool results in the last step. When the condition is an array, any of the conditions can be met to stop the generation. @default stepCountIs(1) */ stopWhen?: | StopCondition<NoInfer<TOOLS>> | Array<StopCondition<NoInfer<TOOLS>>>; /** Optional telemetry configuration (experimental). */ experimental_telemetry?: TelemetrySettings; /** Limits the tools that are available for the model to call without changing the tool call and result types in the result. */ activeTools?: Array<keyof NoInfer<TOOLS>>; /** Optional specification for parsing structured outputs from the LLM response. */ experimental_output?: Output<OUTPUT, OUTPUT_PARTIAL>; /** * @deprecated Use `prepareStep` instead. */ experimental_prepareStep?: PrepareStepFunction<NoInfer<TOOLS>>; /** Optional function that you can use to provide different settings for a step. */ prepareStep?: PrepareStepFunction<NoInfer<TOOLS>>; /** A function that attempts to repair a tool call that failed to parse. */ experimental_repairToolCall?: ToolCallRepairFunction<NoInfer<TOOLS>>; /** Callback that is called when each step (LLM call) is finished, including intermediate steps. */ onStepFinish?: GenerateTextOnStepFinishCallback<NoInfer<TOOLS>>; /** * Context that is passed into tool calls. * * Experimental (can break in patch releases). * * @default undefined */ experimental_context?: unknown; /** * Internal. For test use only. May change without notice. */ _internal?: { generateId?: IdGenerator; currentDate?: () => Date; }; }; export class Agent< TOOLS extends ToolSet, OUTPUT = never, OUTPUT_PARTIAL = never, > { private readonly settings: AgentSettings<TOOLS, OUTPUT, OUTPUT_PARTIAL>; constructor(settings: AgentSettings<TOOLS, OUTPUT, OUTPUT_PARTIAL>) { this.settings = settings; } async generate( options: Prompt & { /** Additional provider-specific metadata. They are passed through from the provider to the AI SDK and enable provider-specific results that can be fully encapsulated in the provider. */ providerMetadata?: ProviderMetadata; }, ): Promise<GenerateTextResult<TOOLS, OUTPUT>> { return generateText({ ...this.settings, ...options }); } stream( options: Prompt & { /** Additional provider-specific metadata. They are passed through from the provider to the AI SDK and enable provider-specific results that can be fully encapsulated in the provider. */ providerMetadata?: ProviderMetadata; }, ): StreamTextResult<TOOLS, OUTPUT_PARTIAL> { return streamText({ ...this.settings, ...options }); } } --- File: /ai/packages/ai/src/agent/index.ts --- export { type AgentSettings as Experimental_AgentSettings, Agent as Experimental_Agent, } from './agent'; --- File: /ai/packages/ai/src/embed/embed-many-result.ts --- import { Embedding } from '../types'; import { EmbeddingModelUsage } from '../types/usage'; import { ProviderMetadata } from '../types'; /** The result of a `embedMany` call. It contains the embeddings, the values, and additional information. */ export interface EmbedManyResult<VALUE> { /** The values that were embedded. */ readonly values: Array<VALUE>; /** The embeddings. They are in the same order as the values. */ readonly embeddings: Array<Embedding>; /** The embedding token usage. */ readonly usage: EmbeddingModelUsage; /** Optional provider-specific metadata. */ readonly providerMetadata?: ProviderMetadata; /** Optional raw response data. */ readonly responses?: Array< | { /** Response headers. */ headers?: Record<string, string>; /** The response body. */ body?: unknown; } | undefined >; } --- File: /ai/packages/ai/src/embed/embed-many.test.ts --- import assert from 'node:assert'; import { MockEmbeddingModelV2, mockEmbed, } from '../test/mock-embedding-model-v2'; import { MockTracer } from '../test/mock-tracer'; import { createResolvablePromise } from '../util/create-resolvable-promise'; import { embedMany } from './embed-many'; const dummyEmbeddings = [ [0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9], ]; const testValues = [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ]; describe('model.supportsParallelCalls', () => { it('should not parallelize when false', async () => { const events: string[] = []; let callCount = 0; const resolvables = [ createResolvablePromise<void>(), createResolvablePromise<void>(), createResolvablePromise<void>(), ]; const embedManyPromise = embedMany({ model: new MockEmbeddingModelV2({ supportsParallelCalls: false, maxEmbeddingsPerCall: 1, doEmbed: async () => { const index = callCount++; events.push(`start-${index}`); await resolvables[index].promise; events.push(`end-${index}`); return { embeddings: [dummyEmbeddings[index]], response: { headers: {}, body: {} }, }; }, }), values: testValues, }); resolvables.forEach(resolvable => { resolvable.resolve(); }); const { embeddings } = await embedManyPromise; expect(events).toStrictEqual([ 'start-0', 'end-0', 'start-1', 'end-1', 'start-2', 'end-2', ]); expect(embeddings).toStrictEqual(dummyEmbeddings); }); it('should parallelize when true', async () => { const events: string[] = []; let callCount = 0; const resolvables = [ createResolvablePromise<void>(), createResolvablePromise<void>(), createResolvablePromise<void>(), ]; const embedManyPromise = embedMany({ model: new MockEmbeddingModelV2({ supportsParallelCalls: true, maxEmbeddingsPerCall: 1, doEmbed: async () => { const index = callCount++; events.push(`start-${index}`); await resolvables[index].promise; events.push(`end-${index}`); return { embeddings: [dummyEmbeddings[index]], response: { headers: {}, body: {} }, }; }, }), values: testValues, }); resolvables.forEach(resolvable => { resolvable.resolve(); }); const { embeddings } = await embedManyPromise; expect(events).toStrictEqual([ 'start-0', 'start-1', 'start-2', 'end-0', 'end-1', 'end-2', ]); expect(embeddings).toStrictEqual(dummyEmbeddings); }); it('should support maxParallelCalls', async () => { const events: string[] = []; let callCount = 0; const resolvables = [ createResolvablePromise<void>(), createResolvablePromise<void>(), createResolvablePromise<void>(), ]; const embedManyPromise = embedMany({ maxParallelCalls: 2, model: new MockEmbeddingModelV2({ supportsParallelCalls: true, maxEmbeddingsPerCall: 1, doEmbed: async () => { const index = callCount++; events.push(`start-${index}`); await resolvables[index].promise; events.push(`end-${index}`); return { embeddings: [dummyEmbeddings[index]], response: { headers: {}, body: {} }, }; }, }), values: testValues, }); resolvables.forEach(resolvable => { resolvable.resolve(); }); const { embeddings } = await embedManyPromise; expect(events).toStrictEqual([ 'start-0', 'start-1', 'end-0', 'end-1', 'start-2', 'end-2', ]); expect(embeddings).toStrictEqual(dummyEmbeddings); }); }); describe('result.embedding', () => { it('should generate embeddings', async () => { const result = await embedMany({ model: new MockEmbeddingModelV2({ maxEmbeddingsPerCall: 5, doEmbed: mockEmbed(testValues, dummyEmbeddings), }), values: testValues, }); assert.deepStrictEqual(result.embeddings, dummyEmbeddings); }); it('should generate embeddings when several calls are required', async () => { let callCount = 0; const result = await embedMany({ model: new MockEmbeddingModelV2({ maxEmbeddingsPerCall: 2, doEmbed: async ({ values }) => { switch (callCount++) { case 0: assert.deepStrictEqual(values, testValues.slice(0, 2)); return { embeddings: dummyEmbeddings.slice(0, 2) }; case 1: assert.deepStrictEqual(values, testValues.slice(2)); return { embeddings: dummyEmbeddings.slice(2) }; default: throw new Error('Unexpected call'); } }, }), values: testValues, }); assert.deepStrictEqual(result.embeddings, dummyEmbeddings); }); }); describe('result.responses', () => { it('should include responses in the result', async () => { let callCount = 0; const result = await embedMany({ model: new MockEmbeddingModelV2({ maxEmbeddingsPerCall: 1, doEmbed: async ({ values }) => { switch (callCount++) { case 0: assert.deepStrictEqual(values, [testValues[0]]); return { embeddings: dummyEmbeddings, response: { body: { first: true }, }, }; case 1: assert.deepStrictEqual(values, [testValues[1]]); return { embeddings: dummyEmbeddings, response: { body: { second: true }, }, }; case 2: assert.deepStrictEqual(values, [testValues[2]]); return { embeddings: dummyEmbeddings, response: { body: { third: true }, }, }; default: throw new Error('Unexpected call'); } }, }), values: testValues, }); expect(result.responses).toMatchSnapshot(); }); }); describe('result.values', () => { it('should include values in the result', async () => { const result = await embedMany({ model: new MockEmbeddingModelV2({ maxEmbeddingsPerCall: 5, doEmbed: mockEmbed(testValues, dummyEmbeddings), }), values: testValues, }); assert.deepStrictEqual(result.values, testValues); }); }); describe('result.usage', () => { it('should include usage in the result', async () => { let callCount = 0; const result = await embedMany({ model: new MockEmbeddingModelV2({ maxEmbeddingsPerCall: 2, doEmbed: async () => { switch (callCount++) { case 0: return { embeddings: dummyEmbeddings.slice(0, 2), usage: { tokens: 10 }, }; case 1: return { embeddings: dummyEmbeddings.slice(2), usage: { tokens: 20 }, }; default: throw new Error('Unexpected call'); } }, }), values: testValues, }); assert.deepStrictEqual(result.usage, { tokens: 30 }); }); }); describe('options.headers', () => { it('should set headers', async () => { const result = await embedMany({ model: new MockEmbeddingModelV2({ maxEmbeddingsPerCall: 5, doEmbed: async ({ headers }) => { assert.deepStrictEqual(headers, { 'custom-request-header': 'request-header-value', }); return { embeddings: dummyEmbeddings }; }, }), values: testValues, headers: { 'custom-request-header': 'request-header-value' }, }); assert.deepStrictEqual(result.embeddings, dummyEmbeddings); }); }); describe('options.providerOptions', () => { it('should pass provider options to model', async () => { const model = new MockEmbeddingModelV2({ doEmbed: async ({ providerOptions }) => { return { embeddings: [[1, 2, 3]] }; }, }); vi.spyOn(model, 'doEmbed'); await embedMany({ model, values: ['test-input'], providerOptions: { aProvider: { someKey: 'someValue' }, }, }); expect(model.doEmbed).toHaveBeenCalledWith({ abortSignal: undefined, headers: undefined, providerOptions: { aProvider: { someKey: 'someValue' }, }, values: ['test-input'], }); }); }); describe('telemetry', () => { let tracer: MockTracer; beforeEach(() => { tracer = new MockTracer(); }); it('should not record any telemetry data when not explicitly enabled', async () => { await embedMany({ model: new MockEmbeddingModelV2({ maxEmbeddingsPerCall: 5, doEmbed: mockEmbed(testValues, dummyEmbeddings), }), values: testValues, }); assert.deepStrictEqual(tracer.jsonSpans, []); }); it('should record telemetry data when enabled (multiple calls path)', async () => { let callCount = 0; await embedMany({ model: new MockEmbeddingModelV2({ maxEmbeddingsPerCall: 2, doEmbed: async ({ values }) => { switch (callCount++) { case 0: assert.deepStrictEqual(values, testValues.slice(0, 2)); return { embeddings: dummyEmbeddings.slice(0, 2), usage: { tokens: 10 }, }; case 1: assert.deepStrictEqual(values, testValues.slice(2)); return { embeddings: dummyEmbeddings.slice(2), usage: { tokens: 20 }, }; default: throw new Error('Unexpected call'); } }, }), values: testValues, experimental_telemetry: { isEnabled: true, functionId: 'test-function-id', metadata: { test1: 'value1', test2: false, }, tracer, }, }); expect(tracer.jsonSpans).toMatchSnapshot(); }); it('should record telemetry data when enabled (single call path)', async () => { await embedMany({ model: new MockEmbeddingModelV2({ maxEmbeddingsPerCall: null, doEmbed: mockEmbed(testValues, dummyEmbeddings, { tokens: 10 }), }), values: testValues, experimental_telemetry: { isEnabled: true, functionId: 'test-function-id', metadata: { test1: 'value1', test2: false, }, tracer, }, }); expect(tracer.jsonSpans).toMatchSnapshot(); }); it('should not record telemetry inputs / outputs when disabled', async () => { await embedMany({ model: new MockEmbeddingModelV2({ maxEmbeddingsPerCall: null, doEmbed: mockEmbed(testValues, dummyEmbeddings, { tokens: 10 }), }), values: testValues, experimental_telemetry: { isEnabled: true, recordInputs: false, recordOutputs: false, tracer, }, }); expect(tracer.jsonSpans).toMatchSnapshot(); }); }); describe('result.providerMetadata', () => { it('should include provider metadata when returned by the model', async () => { const providerMetadata = { gateway: { routing: { resolvedProvider: 'test-provider' } }, }; const result = await embedMany({ model: new MockEmbeddingModelV2({ supportsParallelCalls: false, maxEmbeddingsPerCall: 3, doEmbed: mockEmbed( testValues, dummyEmbeddings, undefined, { headers: {}, body: {}, }, providerMetadata, ), }), values: testValues, }); expect(result.providerMetadata).toStrictEqual(providerMetadata); }); }); --- File: /ai/packages/ai/src/embed/embed-many.ts --- import { ProviderOptions } from '@ai-sdk/provider-utils'; import { prepareRetries } from '../util/prepare-retries'; import { splitArray } from '../util/split-array'; import { UnsupportedModelVersionError } from '../error/unsupported-model-version-error'; import { assembleOperationName } from '../telemetry/assemble-operation-name'; import { getBaseTelemetryAttributes } from '../telemetry/get-base-telemetry-attributes'; import { getTracer } from '../telemetry/get-tracer'; import { recordSpan } from '../telemetry/record-span'; import { selectTelemetryAttributes } from '../telemetry/select-telemetry-attributes'; import { TelemetrySettings } from '../telemetry/telemetry-settings'; import { Embedding, EmbeddingModel, ProviderMetadata } from '../types'; import { resolveEmbeddingModel } from '../model/resolve-model'; import { EmbedManyResult } from './embed-many-result'; /** Embed several values using an embedding model. The type of the value is defined by the embedding model. `embedMany` automatically splits large requests into smaller chunks if the model has a limit on how many embeddings can be generated in a single call. @param model - The embedding model to use. @param values - The values that should be embedded. @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2. @param abortSignal - An optional abort signal that can be used to cancel the call. @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. @returns A result object that contains the embeddings, the value, and additional information. */ export async function embedMany<VALUE = string>({ model: modelArg, values, maxParallelCalls = Infinity, maxRetries: maxRetriesArg, abortSignal, headers, providerOptions, experimental_telemetry: telemetry, }: { /** The embedding model to use. */ model: EmbeddingModel<VALUE>; /** The values that should be embedded. */ values: Array<VALUE>; /** Maximum number of retries per embedding model call. Set to 0 to disable retries. @default 2 */ maxRetries?: number; /** Abort signal. */ abortSignal?: AbortSignal; /** Additional headers to include in the request. Only applicable for HTTP-based providers. */ headers?: Record<string, string>; /** * Optional telemetry configuration (experimental). */ experimental_telemetry?: TelemetrySettings; /** Additional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; /** * Maximum number of concurrent requests. * * @default Infinity */ maxParallelCalls?: number; }): Promise<EmbedManyResult<VALUE>> { const model = resolveEmbeddingModel<VALUE>(modelArg); const { maxRetries, retry } = prepareRetries({ maxRetries: maxRetriesArg, abortSignal, }); const baseTelemetryAttributes = getBaseTelemetryAttributes({ model, telemetry, headers, settings: { maxRetries }, }); const tracer = getTracer(telemetry); return recordSpan({ name: 'ai.embedMany', attributes: selectTelemetryAttributes({ telemetry, attributes: { ...assembleOperationName({ operationId: 'ai.embedMany', telemetry }), ...baseTelemetryAttributes, // specific settings that only make sense on the outer level: 'ai.values': { input: () => values.map(value => JSON.stringify(value)), }, }, }), tracer, fn: async span => { const [maxEmbeddingsPerCall, supportsParallelCalls] = await Promise.all([ model.maxEmbeddingsPerCall, model.supportsParallelCalls, ]); // the model has not specified limits on // how many embeddings can be generated in a single call if (maxEmbeddingsPerCall == null || maxEmbeddingsPerCall === Infinity) { const { embeddings, usage, response, providerMetadata } = await retry( () => { // nested spans to align with the embedMany telemetry data: return recordSpan({ name: 'ai.embedMany.doEmbed', attributes: selectTelemetryAttributes({ telemetry, attributes: { ...assembleOperationName({ operationId: 'ai.embedMany.doEmbed', telemetry, }), ...baseTelemetryAttributes, // specific settings that only make sense on the outer level: 'ai.values': { input: () => values.map(value => JSON.stringify(value)), }, }, }), tracer, fn: async doEmbedSpan => { const modelResponse = await model.doEmbed({ values, abortSignal, headers, providerOptions, }); const embeddings = modelResponse.embeddings; const usage = modelResponse.usage ?? { tokens: NaN }; doEmbedSpan.setAttributes( selectTelemetryAttributes({ telemetry, attributes: { 'ai.embeddings': { output: () => embeddings.map(embedding => JSON.stringify(embedding), ), }, 'ai.usage.tokens': usage.tokens, }, }), ); return { embeddings, usage, providerMetadata: modelResponse.providerMetadata, response: modelResponse.response, }; }, }); }, ); span.setAttributes( selectTelemetryAttributes({ telemetry, attributes: { 'ai.embeddings': { output: () => embeddings.map(embedding => JSON.stringify(embedding)), }, 'ai.usage.tokens': usage.tokens, }, }), ); return new DefaultEmbedManyResult({ values, embeddings, usage, providerMetadata, responses: [response], }); } // split the values into chunks that are small enough for the model: const valueChunks = splitArray(values, maxEmbeddingsPerCall); // serially embed the chunks: const embeddings: Array<Embedding> = []; const responses: Array< | { headers?: Record<string, string>; body?: unknown; } | undefined > = []; let tokens = 0; let providerMetadata: ProviderMetadata | undefined; const parallelChunks = splitArray( valueChunks, supportsParallelCalls ? maxParallelCalls : 1, ); for (const parallelChunk of parallelChunks) { const results = await Promise.all( parallelChunk.map(chunk => { return retry(() => { // nested spans to align with the embedMany telemetry data: return recordSpan({ name: 'ai.embedMany.doEmbed', attributes: selectTelemetryAttributes({ telemetry, attributes: { ...assembleOperationName({ operationId: 'ai.embedMany.doEmbed', telemetry, }), ...baseTelemetryAttributes, // specific settings that only make sense on the outer level: 'ai.values': { input: () => chunk.map(value => JSON.stringify(value)), }, }, }), tracer, fn: async doEmbedSpan => { const modelResponse = await model.doEmbed({ values: chunk, abortSignal, headers, providerOptions, }); const embeddings = modelResponse.embeddings; const usage = modelResponse.usage ?? { tokens: NaN }; doEmbedSpan.setAttributes( selectTelemetryAttributes({ telemetry, attributes: { 'ai.embeddings': { output: () => embeddings.map(embedding => JSON.stringify(embedding), ), }, 'ai.usage.tokens': usage.tokens, }, }), ); return { embeddings, usage, providerMetadata: modelResponse.providerMetadata, response: modelResponse.response, }; }, }); }); }), ); for (const result of results) { embeddings.push(...result.embeddings); responses.push(result.response); tokens += result.usage.tokens; if (result.providerMetadata) { if (!providerMetadata) { providerMetadata = { ...result.providerMetadata }; } else { for (const [providerName, metadata] of Object.entries( result.providerMetadata, )) { providerMetadata[providerName] = { ...(providerMetadata[providerName] ?? {}), ...metadata, }; } } } } } span.setAttributes( selectTelemetryAttributes({ telemetry, attributes: { 'ai.embeddings': { output: () => embeddings.map(embedding => JSON.stringify(embedding)), }, 'ai.usage.tokens': tokens, }, }), ); return new DefaultEmbedManyResult({ values, embeddings, usage: { tokens }, providerMetadata: providerMetadata, responses, }); }, }); } class DefaultEmbedManyResult<VALUE> implements EmbedManyResult<VALUE> { readonly values: EmbedManyResult<VALUE>['values']; readonly embeddings: EmbedManyResult<VALUE>['embeddings']; readonly usage: EmbedManyResult<VALUE>['usage']; readonly providerMetadata: EmbedManyResult<VALUE>['providerMetadata']; readonly responses: EmbedManyResult<VALUE>['responses']; constructor(options: { values: EmbedManyResult<VALUE>['values']; embeddings: EmbedManyResult<VALUE>['embeddings']; usage: EmbedManyResult<VALUE>['usage']; providerMetadata?: EmbedManyResult<VALUE>['providerMetadata']; responses?: EmbedManyResult<VALUE>['responses']; }) { this.values = options.values; this.embeddings = options.embeddings; this.usage = options.usage; this.providerMetadata = options.providerMetadata; this.responses = options.responses; } } --- File: /ai/packages/ai/src/embed/embed-result.ts --- import { Embedding } from '../types'; import { EmbeddingModelUsage } from '../types/usage'; import { ProviderMetadata } from '../types'; /** The result of an `embed` call. It contains the embedding, the value, and additional information. */ export interface EmbedResult<VALUE> { /** The value that was embedded. */ readonly value: VALUE; /** The embedding of the value. */ readonly embedding: Embedding; /** The embedding token usage. */ readonly usage: EmbeddingModelUsage; /** Optional provider-specific metadata. */ readonly providerMetadata?: ProviderMetadata; /** Optional response data. */ readonly response?: { /** Response headers. */ headers?: Record<string, string>; /** The response body. */ body?: unknown; }; } --- File: /ai/packages/ai/src/embed/embed.test.ts --- import assert from 'node:assert'; import { MockEmbeddingModelV2, mockEmbed, } from '../test/mock-embedding-model-v2'; import { MockTracer } from '../test/mock-tracer'; import { embed } from './embed'; const dummyEmbedding = [0.1, 0.2, 0.3]; const testValue = 'sunny day at the beach'; describe('result.embedding', () => { it('should generate embedding', async () => { const result = await embed({ model: new MockEmbeddingModelV2({ doEmbed: mockEmbed([testValue], [dummyEmbedding]), }), value: testValue, }); assert.deepStrictEqual(result.embedding, dummyEmbedding); }); }); describe('result.response', () => { it('should include response in the result', async () => { const result = await embed({ model: new MockEmbeddingModelV2({ doEmbed: mockEmbed([testValue], [dummyEmbedding], undefined, { body: { foo: 'bar' }, headers: { foo: 'bar' }, }), }), value: testValue, }); expect(result.response?.body).toMatchInlineSnapshot(` { "foo": "bar", } `); expect(result.response?.headers).toMatchInlineSnapshot(` { "foo": "bar", } `); }); }); describe('result.value', () => { it('should include value in the result', async () => { const result = await embed({ model: new MockEmbeddingModelV2({ doEmbed: mockEmbed([testValue], [dummyEmbedding]), }), value: testValue, }); assert.deepStrictEqual(result.value, testValue); }); }); describe('result.usage', () => { it('should include usage in the result', async () => { const result = await embed({ model: new MockEmbeddingModelV2({ doEmbed: mockEmbed([testValue], [dummyEmbedding], { tokens: 10 }), }), value: testValue, }); assert.deepStrictEqual(result.usage, { tokens: 10 }); }); }); describe('result.providerMetadata', () => { it('should include provider metadata when returned by the model', async () => { const providerMetadata = { gateway: { routing: { resolvedProvider: 'test-provider', }, }, }; const result = await embed({ model: new MockEmbeddingModelV2({ doEmbed: mockEmbed( [testValue], [dummyEmbedding], undefined, { headers: {}, body: {}, }, providerMetadata, ), }), value: testValue, }); expect(result.providerMetadata).toStrictEqual(providerMetadata); }); }); describe('options.headers', () => { it('should set headers', async () => { const result = await embed({ model: new MockEmbeddingModelV2({ doEmbed: async ({ headers }) => { assert.deepStrictEqual(headers, { 'custom-request-header': 'request-header-value', }); return { embeddings: [dummyEmbedding] }; }, }), value: testValue, headers: { 'custom-request-header': 'request-header-value' }, }); assert.deepStrictEqual(result.embedding, dummyEmbedding); }); }); describe('options.providerOptions', () => { it('should pass provider options to model', async () => { const result = await embed({ model: new MockEmbeddingModelV2({ doEmbed: async ({ providerOptions }) => { expect(providerOptions).toStrictEqual({ aProvider: { someKey: 'someValue' }, }); return { embeddings: [[1, 2, 3]] }; }, }), value: 'test-input', providerOptions: { aProvider: { someKey: 'someValue' }, }, }); expect(result.embedding).toStrictEqual([1, 2, 3]); }); }); describe('telemetry', () => { let tracer: MockTracer; beforeEach(() => { tracer = new MockTracer(); }); it('should not record any telemetry data when not explicitly enabled', async () => { await embed({ model: new MockEmbeddingModelV2({ doEmbed: mockEmbed([testValue], [dummyEmbedding]), }), value: testValue, experimental_telemetry: { tracer }, }); expect(tracer.jsonSpans).toMatchSnapshot(); }); it('should record telemetry data when enabled', async () => { await embed({ model: new MockEmbeddingModelV2({ doEmbed: mockEmbed([testValue], [dummyEmbedding], { tokens: 10 }), }), value: testValue, experimental_telemetry: { isEnabled: true, functionId: 'test-function-id', metadata: { test1: 'value1', test2: false, }, tracer, }, }); expect(tracer.jsonSpans).toMatchSnapshot(); }); it('should not record telemetry inputs / outputs when disabled', async () => { await embed({ model: new MockEmbeddingModelV2({ doEmbed: mockEmbed([testValue], [dummyEmbedding], { tokens: 10 }), }), value: testValue, experimental_telemetry: { isEnabled: true, recordInputs: false, recordOutputs: false, tracer, }, }); expect(tracer.jsonSpans).toMatchSnapshot(); }); }); --- File: /ai/packages/ai/src/embed/embed.ts --- import { ProviderOptions } from '@ai-sdk/provider-utils'; import { resolveEmbeddingModel } from '../model/resolve-model'; import { assembleOperationName } from '../telemetry/assemble-operation-name'; import { getBaseTelemetryAttributes } from '../telemetry/get-base-telemetry-attributes'; import { getTracer } from '../telemetry/get-tracer'; import { recordSpan } from '../telemetry/record-span'; import { selectTelemetryAttributes } from '../telemetry/select-telemetry-attributes'; import { TelemetrySettings } from '../telemetry/telemetry-settings'; import { EmbeddingModel } from '../types'; import { prepareRetries } from '../util/prepare-retries'; import { EmbedResult } from './embed-result'; /** Embed a value using an embedding model. The type of the value is defined by the embedding model. @param model - The embedding model to use. @param value - The value that should be embedded. @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2. @param abortSignal - An optional abort signal that can be used to cancel the call. @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. @returns A result object that contains the embedding, the value, and additional information. */ export async function embed<VALUE = string>({ model: modelArg, value, providerOptions, maxRetries: maxRetriesArg, abortSignal, headers, experimental_telemetry: telemetry, }: { /** The embedding model to use. */ model: EmbeddingModel<VALUE>; /** The value that should be embedded. */ value: VALUE; /** Maximum number of retries per embedding model call. Set to 0 to disable retries. @default 2 */ maxRetries?: number; /** Abort signal. */ abortSignal?: AbortSignal; /** Additional headers to include in the request. Only applicable for HTTP-based providers. */ headers?: Record<string, string>; /** Additional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; /** * Optional telemetry configuration (experimental). */ experimental_telemetry?: TelemetrySettings; }): Promise<EmbedResult<VALUE>> { const model = resolveEmbeddingModel<VALUE>(modelArg); const { maxRetries, retry } = prepareRetries({ maxRetries: maxRetriesArg, abortSignal, }); const baseTelemetryAttributes = getBaseTelemetryAttributes({ model: model, telemetry, headers, settings: { maxRetries }, }); const tracer = getTracer(telemetry); return recordSpan({ name: 'ai.embed', attributes: selectTelemetryAttributes({ telemetry, attributes: { ...assembleOperationName({ operationId: 'ai.embed', telemetry }), ...baseTelemetryAttributes, 'ai.value': { input: () => JSON.stringify(value) }, }, }), tracer, fn: async span => { const { embedding, usage, response, providerMetadata } = await retry(() => // nested spans to align with the embedMany telemetry data: recordSpan({ name: 'ai.embed.doEmbed', attributes: selectTelemetryAttributes({ telemetry, attributes: { ...assembleOperationName({ operationId: 'ai.embed.doEmbed', telemetry, }), ...baseTelemetryAttributes, // specific settings that only make sense on the outer level: 'ai.values': { input: () => [JSON.stringify(value)] }, }, }), tracer, fn: async doEmbedSpan => { const modelResponse = await model.doEmbed({ values: [value], abortSignal, headers, providerOptions, }); const embedding = modelResponse.embeddings[0]; const usage = modelResponse.usage ?? { tokens: NaN }; doEmbedSpan.setAttributes( selectTelemetryAttributes({ telemetry, attributes: { 'ai.embeddings': { output: () => modelResponse.embeddings.map(embedding => JSON.stringify(embedding), ), }, 'ai.usage.tokens': usage.tokens, }, }), ); return { embedding, usage, providerMetadata: modelResponse.providerMetadata, response: modelResponse.response, }; }, }), ); span.setAttributes( selectTelemetryAttributes({ telemetry, attributes: { 'ai.embedding': { output: () => JSON.stringify(embedding) }, 'ai.usage.tokens': usage.tokens, }, }), ); return new DefaultEmbedResult({ value, embedding, usage, providerMetadata, response, }); }, }); } class DefaultEmbedResult<VALUE> implements EmbedResult<VALUE> { readonly value: EmbedResult<VALUE>['value']; readonly embedding: EmbedResult<VALUE>['embedding']; readonly usage: EmbedResult<VALUE>['usage']; readonly providerMetadata: EmbedResult<VALUE>['providerMetadata']; readonly response: EmbedResult<VALUE>['response']; constructor(options: { value: EmbedResult<VALUE>['value']; embedding: EmbedResult<VALUE>['embedding']; usage: EmbedResult<VALUE>['usage']; providerMetadata?: EmbedResult<VALUE>['providerMetadata']; response?: EmbedResult<VALUE>['response']; }) { this.value = options.value; this.embedding = options.embedding; this.usage = options.usage; this.providerMetadata = options.providerMetadata; this.response = options.response; } } --- File: /ai/packages/ai/src/embed/index.ts --- export * from './embed'; export * from './embed-many'; export * from './embed-many-result'; export * from './embed-result'; --- File: /ai/packages/ai/src/error/index.ts --- export { AISDKError, APICallError, EmptyResponseBodyError, InvalidPromptError, InvalidResponseDataError, JSONParseError, LoadAPIKeyError, NoContentGeneratedError, NoSuchModelError, TypeValidationError, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; export { InvalidArgumentError } from './invalid-argument-error'; export { InvalidStreamPartError } from './invalid-stream-part-error'; export { InvalidToolInputError } from './invalid-tool-input-error'; export { MCPClientError } from './mcp-client-error'; export { NoImageGeneratedError } from './no-image-generated-error'; export { NoObjectGeneratedError } from './no-object-generated-error'; export { NoOutputSpecifiedError } from './no-output-specified-error'; export { NoSuchToolError } from './no-such-tool-error'; export { ToolCallRepairError } from './tool-call-repair-error'; export { UnsupportedModelVersionError } from './unsupported-model-version-error'; export { InvalidDataContentError } from '../prompt/invalid-data-content-error'; export { InvalidMessageRoleError } from '../prompt/invalid-message-role-error'; export { MessageConversionError } from '../prompt/message-conversion-error'; export { DownloadError } from '../util/download-error'; export { RetryError } from '../util/retry-error'; --- File: /ai/packages/ai/src/error/invalid-argument-error.ts --- import { AISDKError } from '@ai-sdk/provider'; const name = 'AI_InvalidArgumentError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class InvalidArgumentError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly parameter: string; readonly value: unknown; constructor({ parameter, value, message, }: { parameter: string; value: unknown; message: string; }) { super({ name, message: `Invalid argument for parameter ${parameter}: ${message}`, }); this.parameter = parameter; this.value = value; } static isInstance(error: unknown): error is InvalidArgumentError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/ai/src/error/invalid-stream-part-error.ts --- import { AISDKError } from '@ai-sdk/provider'; import { SingleRequestTextStreamPart } from '../generate-text/run-tools-transformation'; const name = 'AI_InvalidStreamPartError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class InvalidStreamPartError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly chunk: SingleRequestTextStreamPart<any>; constructor({ chunk, message, }: { chunk: SingleRequestTextStreamPart<any>; message: string; }) { super({ name, message }); this.chunk = chunk; } static isInstance(error: unknown): error is InvalidStreamPartError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/ai/src/error/invalid-tool-input-error.ts --- import { AISDKError, getErrorMessage } from '@ai-sdk/provider'; const name = 'AI_InvalidToolInputError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class InvalidToolInputError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly toolName: string; readonly toolInput: string; constructor({ toolInput, toolName, cause, message = `Invalid input for tool ${toolName}: ${getErrorMessage(cause)}`, }: { message?: string; toolInput: string; toolName: string; cause: unknown; }) { super({ name, message, cause }); this.toolInput = toolInput; this.toolName = toolName; } static isInstance(error: unknown): error is InvalidToolInputError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/ai/src/error/mcp-client-error.ts --- import { AISDKError } from '@ai-sdk/provider'; const name = 'AI_MCPClientError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); /** * An error occurred with the MCP client. */ export class MCPClientError extends AISDKError { private readonly [symbol] = true; constructor({ name = 'MCPClientError', message, cause, }: { name?: string; message: string; cause?: unknown; }) { super({ name, message, cause }); } static isInstance(error: unknown): error is MCPClientError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/ai/src/error/no-image-generated-error.ts --- import { AISDKError } from '@ai-sdk/provider'; import { ImageModelResponseMetadata } from '../types/image-model-response-metadata'; const name = 'AI_NoImageGeneratedError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); /** Thrown when no image could be generated. This can have multiple causes: - The model failed to generate a response. - The model generated a response that could not be parsed. */ export class NoImageGeneratedError extends AISDKError { private readonly [symbol] = true; // used in isInstance /** The response metadata for each call. */ readonly responses: Array<ImageModelResponseMetadata> | undefined; constructor({ message = 'No image generated.', cause, responses, }: { message?: string; cause?: Error; responses?: Array<ImageModelResponseMetadata>; }) { super({ name, message, cause }); this.responses = responses; } static isInstance(error: unknown): error is NoImageGeneratedError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/ai/src/error/no-object-generated-error.ts --- import { AISDKError } from '@ai-sdk/provider'; import { FinishReason } from '../types/language-model'; import { LanguageModelResponseMetadata } from '../types/language-model-response-metadata'; import { LanguageModelUsage } from '../types/usage'; const name = 'AI_NoObjectGeneratedError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); /** Thrown when no object could be generated. This can have several causes: - The model failed to generate a response. - The model generated a response that could not be parsed. - The model generated a response that could not be validated against the schema. The error contains the following properties: - `text`: The text that was generated by the model. This can be the raw text or the tool call text, depending on the model. */ export class NoObjectGeneratedError extends AISDKError { private readonly [symbol] = true; // used in isInstance /** The text that was generated by the model. This can be the raw text or the tool call text, depending on the model. */ readonly text: string | undefined; /** The response metadata. */ readonly response: LanguageModelResponseMetadata | undefined; /** The usage of the model. */ readonly usage: LanguageModelUsage | undefined; /** Reason why the model finished generating a response. */ readonly finishReason: FinishReason | undefined; constructor({ message = 'No object generated.', cause, text, response, usage, finishReason, }: { message?: string; cause?: Error; text?: string; response: LanguageModelResponseMetadata; usage: LanguageModelUsage; finishReason: FinishReason; }) { super({ name, message, cause }); this.text = text; this.response = response; this.usage = usage; this.finishReason = finishReason; } static isInstance(error: unknown): error is NoObjectGeneratedError { return AISDKError.hasMarker(error, marker); } } export function verifyNoObjectGeneratedError( error: unknown, expected: { message: string; response: LanguageModelResponseMetadata & { body?: string; }; usage: LanguageModelUsage; finishReason: FinishReason; }, ) { expect(NoObjectGeneratedError.isInstance(error)).toBeTruthy(); const noObjectGeneratedError = error as NoObjectGeneratedError; expect(noObjectGeneratedError.message).toEqual(expected.message); expect(noObjectGeneratedError.response).toEqual(expected.response); expect(noObjectGeneratedError.usage).toEqual(expected.usage); expect(noObjectGeneratedError.finishReason).toEqual(expected.finishReason); } --- File: /ai/packages/ai/src/error/no-output-specified-error.ts --- import { AISDKError } from '@ai-sdk/provider'; const name = 'AI_NoOutputSpecifiedError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); /** Thrown when no output type is specified and output-related methods are called. */ export class NoOutputSpecifiedError extends AISDKError { private readonly [symbol] = true; // used in isInstance constructor({ message = 'No output specified.' }: { message?: string } = {}) { super({ name, message }); } static isInstance(error: unknown): error is NoOutputSpecifiedError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/ai/src/error/no-speech-generated-error.ts --- import { AISDKError } from '@ai-sdk/provider'; import { SpeechModelResponseMetadata } from '../types/speech-model-response-metadata'; /** Error that is thrown when no speech audio was generated. */ export class NoSpeechGeneratedError extends AISDKError { readonly responses: Array<SpeechModelResponseMetadata>; constructor(options: { responses: Array<SpeechModelResponseMetadata> }) { super({ name: 'AI_NoSpeechGeneratedError', message: 'No speech audio generated.', }); this.responses = options.responses; } } --- File: /ai/packages/ai/src/error/no-such-tool-error.ts --- import { AISDKError } from '@ai-sdk/provider'; const name = 'AI_NoSuchToolError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class NoSuchToolError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly toolName: string; readonly availableTools: string[] | undefined; constructor({ toolName, availableTools = undefined, message = `Model tried to call unavailable tool '${toolName}'. ${ availableTools === undefined ? 'No tools are available.' : `Available tools: ${availableTools.join(', ')}.` }`, }: { toolName: string; availableTools?: string[] | undefined; message?: string; }) { super({ name, message }); this.toolName = toolName; this.availableTools = availableTools; } static isInstance(error: unknown): error is NoSuchToolError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/ai/src/error/no-transcript-generated-error.ts --- import { AISDKError } from '@ai-sdk/provider'; import { TranscriptionModelResponseMetadata } from '../types/transcription-model-response-metadata'; /** Error that is thrown when no transcript was generated. */ export class NoTranscriptGeneratedError extends AISDKError { readonly responses: Array<TranscriptionModelResponseMetadata>; constructor(options: { responses: Array<TranscriptionModelResponseMetadata>; }) { super({ name: 'AI_NoTranscriptGeneratedError', message: 'No transcript generated.', }); this.responses = options.responses; } } --- File: /ai/packages/ai/src/error/tool-call-repair-error.ts --- import { AISDKError, getErrorMessage } from '@ai-sdk/provider'; import { InvalidToolInputError } from './invalid-tool-input-error'; import { NoSuchToolError } from './no-such-tool-error'; const name = 'AI_ToolCallRepairError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class ToolCallRepairError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly originalError: NoSuchToolError | InvalidToolInputError; constructor({ cause, originalError, message = `Error repairing tool call: ${getErrorMessage(cause)}`, }: { message?: string; cause: unknown; originalError: NoSuchToolError | InvalidToolInputError; }) { super({ name, message, cause }); this.originalError = originalError; } static isInstance(error: unknown): error is ToolCallRepairError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/ai/src/error/unsupported-model-version-error.ts --- import { AISDKError } from '@ai-sdk/provider'; /** Error that is thrown when a model with an unsupported version is used. */ export class UnsupportedModelVersionError extends AISDKError { readonly version: string; readonly provider: string; readonly modelId: string; constructor(options: { version: string; provider: string; modelId: string }) { super({ name: 'AI_UnsupportedModelVersionError', message: `Unsupported model version ${options.version} for provider "${options.provider}" and model "${options.modelId}". ` + `AI SDK 5 only supports models that implement specification version "v2".`, }); this.version = options.version; this.provider = options.provider; this.modelId = options.modelId; } } --- File: /ai/packages/ai/src/generate-image/generate-image-result.ts --- import { GeneratedFile } from '../generate-text'; import { ImageGenerationWarning, ImageModelProviderMetadata, } from '../types/image-model'; import { ImageModelResponseMetadata } from '../types/image-model-response-metadata'; /** The result of a `generateImage` call. It contains the images and additional information. */ export interface GenerateImageResult { /** The first image that was generated. */ readonly image: GeneratedFile; /** The images that were generated. */ readonly images: Array<GeneratedFile>; /** Warnings for the call, e.g. unsupported settings. */ readonly warnings: Array<ImageGenerationWarning>; /** Response metadata from the provider. There may be multiple responses if we made multiple calls to the model. */ readonly responses: Array<ImageModelResponseMetadata>; /** * Provider-specific metadata. They are passed through from the provider to the AI SDK and enable provider-specific * results that can be fully encapsulated in the provider. */ readonly providerMetadata: ImageModelProviderMetadata; } --- File: /ai/packages/ai/src/generate-image/generate-image.test.ts --- import { ImageModelV2, ImageModelV2CallWarning, ImageModelV2ProviderMetadata, } from '@ai-sdk/provider'; import { MockImageModelV2 } from '../test/mock-image-model-v2'; import { generateImage } from './generate-image'; import { convertBase64ToUint8Array, convertUint8ArrayToBase64, } from '@ai-sdk/provider-utils'; const prompt = 'sunny day at the beach'; const testDate = new Date(2024, 0, 1); const pngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg=='; // 1x1 transparent PNG const jpegBase64 = '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k='; // 1x1 black JPEG const gifBase64 = 'R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='; // 1x1 transparent GIF const createMockResponse = (options: { images: string[] | Uint8Array[]; warnings?: ImageModelV2CallWarning[]; timestamp?: Date; modelId?: string; providerMetaData?: ImageModelV2ProviderMetadata; headers?: Record<string, string>; }) => ({ images: options.images, warnings: options.warnings ?? [], providerMetadata: options.providerMetaData ?? { testProvider: { images: options.images.map(() => null), }, }, response: { timestamp: options.timestamp ?? new Date(), modelId: options.modelId ?? 'test-model-id', headers: options.headers ?? {}, }, }); describe('generateImage', () => { it('should send args to doGenerate', async () => { const abortController = new AbortController(); const abortSignal = abortController.signal; let capturedArgs!: Parameters<ImageModelV2['doGenerate']>[0]; await generateImage({ model: new MockImageModelV2({ doGenerate: async args => { capturedArgs = args; return createMockResponse({ images: [pngBase64], }); }, }), prompt, size: '1024x1024', aspectRatio: '16:9', seed: 12345, providerOptions: { 'mock-provider': { style: 'vivid', }, }, headers: { 'custom-request-header': 'request-header-value' }, abortSignal, }); expect(capturedArgs).toStrictEqual({ n: 1, prompt, size: '1024x1024', aspectRatio: '16:9', seed: 12345, providerOptions: { 'mock-provider': { style: 'vivid' } }, headers: { 'custom-request-header': 'request-header-value' }, abortSignal, }); }); it('should return warnings', async () => { const result = await generateImage({ model: new MockImageModelV2({ doGenerate: async () => createMockResponse({ images: [pngBase64], warnings: [ { type: 'other', message: 'Setting is not supported', }, ], }), }), prompt, }); expect(result.warnings).toStrictEqual([ { type: 'other', message: 'Setting is not supported', }, ]); }); describe('base64 image data', () => { it('should return generated images with correct mime types', async () => { const result = await generateImage({ model: new MockImageModelV2({ doGenerate: async () => createMockResponse({ images: [pngBase64, jpegBase64], }), }), prompt, }); expect( result.images.map(image => ({ base64: image.base64, uint8Array: image.uint8Array, mediaType: image.mediaType, })), ).toStrictEqual([ { base64: pngBase64, uint8Array: convertBase64ToUint8Array(pngBase64), mediaType: 'image/png', }, { base64: jpegBase64, uint8Array: convertBase64ToUint8Array(jpegBase64), mediaType: 'image/jpeg', }, ]); }); it('should return the first image with correct mime type', async () => { const result = await generateImage({ model: new MockImageModelV2({ doGenerate: async () => createMockResponse({ images: [pngBase64, jpegBase64], }), }), prompt, }); expect({ base64: result.image.base64, uint8Array: result.image.uint8Array, mediaType: result.image.mediaType, }).toStrictEqual({ base64: pngBase64, uint8Array: convertBase64ToUint8Array(pngBase64), mediaType: 'image/png', }); }); }); describe('uint8array image data', () => { it('should return generated images', async () => { const uint8ArrayImages = [ convertBase64ToUint8Array(pngBase64), convertBase64ToUint8Array(jpegBase64), ]; const result = await generateImage({ model: new MockImageModelV2({ doGenerate: async () => createMockResponse({ images: uint8ArrayImages, }), }), prompt, }); expect( result.images.map(image => ({ base64: image.base64, uint8Array: image.uint8Array, })), ).toStrictEqual([ { base64: convertUint8ArrayToBase64(uint8ArrayImages[0]), uint8Array: uint8ArrayImages[0], }, { base64: convertUint8ArrayToBase64(uint8ArrayImages[1]), uint8Array: uint8ArrayImages[1], }, ]); }); }); describe('when several calls are required', () => { it('should generate images', async () => { const base64Images = [pngBase64, jpegBase64, gifBase64]; let callCount = 0; const result = await generateImage({ model: new MockImageModelV2({ maxImagesPerCall: 2, doGenerate: async options => { switch (callCount++) { case 0: expect(options).toStrictEqual({ prompt, n: 2, seed: 12345, size: '1024x1024', aspectRatio: '16:9', providerOptions: { 'mock-provider': { style: 'vivid' }, }, headers: { 'custom-request-header': 'request-header-value' }, abortSignal: undefined, }); return createMockResponse({ images: base64Images.slice(0, 2), }); case 1: expect(options).toStrictEqual({ prompt, n: 1, seed: 12345, size: '1024x1024', aspectRatio: '16:9', providerOptions: { 'mock-provider': { style: 'vivid' } }, headers: { 'custom-request-header': 'request-header-value' }, abortSignal: undefined, }); return createMockResponse({ images: base64Images.slice(2), }); default: throw new Error('Unexpected call'); } }, }), prompt, n: 3, size: '1024x1024', aspectRatio: '16:9', seed: 12345, providerOptions: { 'mock-provider': { style: 'vivid' } }, headers: { 'custom-request-header': 'request-header-value' }, }); expect(result.images.map(image => image.base64)).toStrictEqual( base64Images, ); }); it('should aggregate warnings', async () => { const base64Images = [pngBase64, jpegBase64, gifBase64]; let callCount = 0; const result = await generateImage({ model: new MockImageModelV2({ maxImagesPerCall: 2, doGenerate: async options => { switch (callCount++) { case 0: expect(options).toStrictEqual({ prompt, n: 2, seed: 12345, size: '1024x1024', aspectRatio: '16:9', providerOptions: { 'mock-provider': { style: 'vivid' } }, headers: { 'custom-request-header': 'request-header-value' }, abortSignal: undefined, }); return createMockResponse({ images: base64Images.slice(0, 2), warnings: [{ type: 'other', message: '1' }], }); case 1: expect(options).toStrictEqual({ prompt, n: 1, seed: 12345, size: '1024x1024', aspectRatio: '16:9', providerOptions: { 'mock-provider': { style: 'vivid' } }, headers: { 'custom-request-header': 'request-header-value' }, abortSignal: undefined, }); return createMockResponse({ images: base64Images.slice(2), warnings: [{ type: 'other', message: '2' }], }); default: throw new Error('Unexpected call'); } }, }), prompt, n: 3, size: '1024x1024', aspectRatio: '16:9', seed: 12345, providerOptions: { 'mock-provider': { style: 'vivid' } }, headers: { 'custom-request-header': 'request-header-value' }, }); expect(result.warnings).toStrictEqual([ { type: 'other', message: '1' }, { type: 'other', message: '2' }, ]); }); test.each([ ['sync method', () => 2], ['async method', async () => 2], ])( 'should generate with maxImagesPerCall = %s', async (_, maxImagesPerCall) => { const base64Images = [pngBase64, jpegBase64, gifBase64]; let callCount = 0; const maxImagesPerCallMock = vitest.fn(maxImagesPerCall); const result = await generateImage({ model: new MockImageModelV2({ maxImagesPerCall: maxImagesPerCallMock, doGenerate: async options => { switch (callCount++) { case 0: expect(options).toStrictEqual({ prompt, n: 2, seed: 12345, size: '1024x1024', aspectRatio: '16:9', providerOptions: { 'mock-provider': { style: 'vivid' }, }, headers: { 'custom-request-header': 'request-header-value', }, abortSignal: undefined, }); return createMockResponse({ images: base64Images.slice(0, 2), }); case 1: expect(options).toStrictEqual({ prompt, n: 1, seed: 12345, size: '1024x1024', aspectRatio: '16:9', providerOptions: { 'mock-provider': { style: 'vivid' } }, headers: { 'custom-request-header': 'request-header-value', }, abortSignal: undefined, }); return createMockResponse({ images: base64Images.slice(2), }); default: throw new Error('Unexpected call'); } }, }), prompt, n: 3, size: '1024x1024', aspectRatio: '16:9', seed: 12345, providerOptions: { 'mock-provider': { style: 'vivid' } }, headers: { 'custom-request-header': 'request-header-value' }, }); expect(result.images.map(image => image.base64)).toStrictEqual( base64Images, ); expect(maxImagesPerCallMock).toHaveBeenCalledTimes(1); expect(maxImagesPerCallMock).toHaveBeenCalledWith({ modelId: 'mock-model-id', }); }, ); }); describe('error handling', () => { it('should throw NoImageGeneratedError when no images are returned', async () => { await expect( generateImage({ model: new MockImageModelV2({ doGenerate: async () => createMockResponse({ images: [], timestamp: testDate, }), }), prompt, }), ).rejects.toMatchObject({ name: 'AI_NoImageGeneratedError', message: 'No image generated.', responses: [ { timestamp: testDate, modelId: expect.any(String), }, ], }); }); it('should include response headers in error when no images generated', async () => { await expect( generateImage({ model: new MockImageModelV2({ doGenerate: async () => createMockResponse({ images: [], timestamp: testDate, headers: { 'custom-response-header': 'response-header-value', }, }), }), prompt, }), ).rejects.toMatchObject({ name: 'AI_NoImageGeneratedError', message: 'No image generated.', responses: [ { timestamp: testDate, modelId: expect.any(String), headers: { 'custom-response-header': 'response-header-value', }, }, ], }); }); }); it('should return response metadata', async () => { const testHeaders = { 'x-test': 'value' }; const result = await generateImage({ model: new MockImageModelV2({ doGenerate: async () => createMockResponse({ images: [pngBase64], timestamp: testDate, modelId: 'test-model', headers: testHeaders, }), }), prompt, }); expect(result.responses).toStrictEqual([ { timestamp: testDate, modelId: 'test-model', headers: testHeaders, }, ]); }); it('should return provider metadata', async () => { const result = await generateImage({ model: new MockImageModelV2({ doGenerate: async () => createMockResponse({ images: [pngBase64, pngBase64], timestamp: testDate, modelId: 'test-model', providerMetaData: { testProvider: { images: [{ revisedPrompt: 'test-revised-prompt' }, null], }, }, headers: {}, }), }), prompt, }); expect(result.providerMetadata).toStrictEqual({ testProvider: { images: [{ revisedPrompt: 'test-revised-prompt' }, null], }, }); }); }); --- File: /ai/packages/ai/src/generate-image/generate-image.ts --- import { ImageModelV2, ImageModelV2ProviderMetadata } from '@ai-sdk/provider'; import { ProviderOptions } from '@ai-sdk/provider-utils'; import { NoImageGeneratedError } from '../error/no-image-generated-error'; import { detectMediaType, imageMediaTypeSignatures, } from '../util/detect-media-type'; import { prepareRetries } from '../util/prepare-retries'; import { UnsupportedModelVersionError } from '../error/unsupported-model-version-error'; import { DefaultGeneratedFile, GeneratedFile, } from '../generate-text/generated-file'; import { ImageGenerationWarning } from '../types/image-model'; import { ImageModelResponseMetadata } from '../types/image-model-response-metadata'; import { GenerateImageResult } from './generate-image-result'; /** Generates images using an image model. @param model - The image model to use. @param prompt - The prompt that should be used to generate the image. @param n - Number of images to generate. Default: 1. @param size - Size of the images to generate. Must have the format `{width}x{height}`. @param aspectRatio - Aspect ratio of the images to generate. Must have the format `{width}:{height}`. @param seed - Seed for the image generation. @param providerOptions - Additional provider-specific options that are passed through to the provider as body parameters. @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2. @param abortSignal - An optional abort signal that can be used to cancel the call. @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. @returns A result object that contains the generated images. */ export async function generateImage({ model, prompt, n = 1, maxImagesPerCall, size, aspectRatio, seed, providerOptions, maxRetries: maxRetriesArg, abortSignal, headers, }: { /** The image model to use. */ model: ImageModelV2; /** The prompt that should be used to generate the image. */ prompt: string; /** Number of images to generate. */ n?: number; /** Number of images to generate. */ maxImagesPerCall?: number; /** Size of the images to generate. Must have the format `{width}x{height}`. If not provided, the default size will be used. */ size?: `${number}x${number}`; /** Aspect ratio of the images to generate. Must have the format `{width}:{height}`. If not provided, the default aspect ratio will be used. */ aspectRatio?: `${number}:${number}`; /** Seed for the image generation. If not provided, the default seed will be used. */ seed?: number; /** Additional provider-specific options that are passed through to the provider as body parameters. The outer record is keyed by the provider name, and the inner record is keyed by the provider-specific metadata key. ```ts { "openai": { "style": "vivid" } } ``` */ providerOptions?: ProviderOptions; /** Maximum number of retries per embedding model call. Set to 0 to disable retries. @default 2 */ maxRetries?: number; /** Abort signal. */ abortSignal?: AbortSignal; /** Additional headers to include in the request. Only applicable for HTTP-based providers. */ headers?: Record<string, string>; }): Promise<GenerateImageResult> { if (model.specificationVersion !== 'v2') { throw new UnsupportedModelVersionError({ version: model.specificationVersion, provider: model.provider, modelId: model.modelId, }); } const { retry } = prepareRetries({ maxRetries: maxRetriesArg, abortSignal, }); // default to 1 if the model has not specified limits on // how many images can be generated in a single call const maxImagesPerCallWithDefault = maxImagesPerCall ?? (await invokeModelMaxImagesPerCall(model)) ?? 1; // parallelize calls to the model: const callCount = Math.ceil(n / maxImagesPerCallWithDefault); const callImageCounts = Array.from({ length: callCount }, (_, i) => { if (i < callCount - 1) { return maxImagesPerCallWithDefault; } const remainder = n % maxImagesPerCallWithDefault; return remainder === 0 ? maxImagesPerCallWithDefault : remainder; }); const results = await Promise.all( callImageCounts.map(async callImageCount => retry(() => model.doGenerate({ prompt, n: callImageCount, abortSignal, headers, size, aspectRatio, seed, providerOptions: providerOptions ?? {}, }), ), ), ); // collect result images, warnings, and response metadata const images: Array<DefaultGeneratedFile> = []; const warnings: Array<ImageGenerationWarning> = []; const responses: Array<ImageModelResponseMetadata> = []; const providerMetadata: ImageModelV2ProviderMetadata = {}; for (const result of results) { images.push( ...result.images.map( image => new DefaultGeneratedFile({ data: image, mediaType: detectMediaType({ data: image, signatures: imageMediaTypeSignatures, }) ?? 'image/png', }), ), ); warnings.push(...result.warnings); if (result.providerMetadata) { for (const [providerName, metadata] of Object.entries<{ images: unknown; }>(result.providerMetadata)) { providerMetadata[providerName] ??= { images: [] }; providerMetadata[providerName].images.push( ...result.providerMetadata[providerName].images, ); } } responses.push(result.response); } if (!images.length) { throw new NoImageGeneratedError({ responses }); } return new DefaultGenerateImageResult({ images, warnings, responses, providerMetadata, }); } class DefaultGenerateImageResult implements GenerateImageResult { readonly images: Array<GeneratedFile>; readonly warnings: Array<ImageGenerationWarning>; readonly responses: Array<ImageModelResponseMetadata>; readonly providerMetadata: ImageModelV2ProviderMetadata; constructor(options: { images: Array<GeneratedFile>; warnings: Array<ImageGenerationWarning>; responses: Array<ImageModelResponseMetadata>; providerMetadata: ImageModelV2ProviderMetadata; }) { this.images = options.images; this.warnings = options.warnings; this.responses = options.responses; this.providerMetadata = options.providerMetadata; } get image() { return this.images[0]; } } async function invokeModelMaxImagesPerCall(model: ImageModelV2) { const isFunction = model.maxImagesPerCall instanceof Function; if (!isFunction) { return model.maxImagesPerCall; } return model.maxImagesPerCall({ modelId: model.modelId, }); } --- File: /ai/packages/ai/src/generate-image/index.ts --- export { generateImage as experimental_generateImage } from './generate-image'; export type { GenerateImageResult as Experimental_GenerateImageResult } from './generate-image-result'; --- File: /ai/packages/ai/src/generate-object/generate-object-result.ts --- import { CallWarning, FinishReason, LanguageModelRequestMetadata, LanguageModelResponseMetadata, ProviderMetadata, } from '../types'; import { LanguageModelUsage } from '../types/usage'; /** The result of a `generateObject` call. */ export interface GenerateObjectResult<OBJECT> { /** The generated object (typed according to the schema). */ readonly object: OBJECT; /** The reason why the generation finished. */ readonly finishReason: FinishReason; /** The token usage of the generated text. */ readonly usage: LanguageModelUsage; /** Warnings from the model provider (e.g. unsupported settings). */ readonly warnings: CallWarning[] | undefined; /** Additional request information. */ readonly request: LanguageModelRequestMetadata; /** Additional response information. */ readonly response: LanguageModelResponseMetadata & { /** Response body (available only for providers that use HTTP requests). */ body?: unknown; }; /** Additional provider-specific metadata. They are passed through from the provider to the AI SDK and enable provider-specific results that can be fully encapsulated in the provider. */ readonly providerMetadata: ProviderMetadata | undefined; /** Converts the object to a JSON response. The response will have a status code of 200 and a content type of `application/json; charset=utf-8`. */ toJsonResponse(init?: ResponseInit): Response; } --- File: /ai/packages/ai/src/generate-object/generate-object.test-d.ts --- import { expectTypeOf } from 'vitest'; import { generateObject } from './generate-object'; import { z } from 'zod/v4'; import { JSONValue } from '@ai-sdk/provider'; describe('generateObject', () => { it('should support enum types', async () => { const result = await generateObject({ output: 'enum', enum: ['a', 'b', 'c'] as const, model: undefined!, }); expectTypeOf<typeof result.object>().toEqualTypeOf<'a' | 'b' | 'c'>; }); it('should support schema types', async () => { const result = await generateObject({ schema: z.object({ number: z.number() }), model: undefined!, }); expectTypeOf<typeof result.object>().toEqualTypeOf<{ number: number }>(); }); it('should support no-schema output mode', async () => { const result = await generateObject({ output: 'no-schema', model: undefined!, }); expectTypeOf<typeof result.object>().toEqualTypeOf<JSONValue>(); }); it('should support array output mode', async () => { const result = await generateObject({ output: 'array', schema: z.number(), model: undefined!, }); expectTypeOf<typeof result.object>().toEqualTypeOf<number[]>(); }); }); --- File: /ai/packages/ai/src/generate-object/generate-object.test.ts --- import { JSONParseError, TypeValidationError } from '@ai-sdk/provider'; import { jsonSchema } from '@ai-sdk/provider-utils'; import { convertReadableStreamToArray } from '@ai-sdk/provider-utils/test'; import assert, { fail } from 'node:assert'; import { z } from 'zod/v4'; import { verifyNoObjectGeneratedError as originalVerifyNoObjectGeneratedError } from '../error/no-object-generated-error'; import { MockLanguageModelV2 } from '../test/mock-language-model-v2'; import { MockTracer } from '../test/mock-tracer'; import { generateObject } from './generate-object'; const dummyResponseValues = { finishReason: 'stop' as const, usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30, reasoningTokens: undefined, cachedInputTokens: undefined, }, response: { id: 'id-1', timestamp: new Date(123), modelId: 'm-1' }, warnings: [], }; describe('output = "object"', () => { describe('result.object', () => { it('should generate object', async () => { const model = new MockLanguageModelV2({ doGenerate: { ...dummyResponseValues, content: [{ type: 'text', text: '{ "content": "Hello, world!" }' }], }, }); const result = await generateObject({ model, schema: z.object({ content: z.string() }), prompt: 'prompt', }); expect(result.object).toMatchInlineSnapshot(` { "content": "Hello, world!", } `); expect(model.doGenerateCalls[0].prompt).toMatchInlineSnapshot(` [ { "content": [ { "text": "prompt", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, ] `); expect(model.doGenerateCalls[0].responseFormat).toMatchInlineSnapshot(` { "description": undefined, "name": undefined, "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "content": { "type": "string", }, }, "required": [ "content", ], "type": "object", }, "type": "json", } `); }); it('should use name and description', async () => { const result = await generateObject({ model: new MockLanguageModelV2({ doGenerate: async ({ prompt, responseFormat }) => { expect(responseFormat).toStrictEqual({ type: 'json', name: 'test-name', description: 'test description', schema: { $schema: 'http://json-schema.org/draft-07/schema#', additionalProperties: false, properties: { content: { type: 'string' } }, required: ['content'], type: 'object', }, }); expect(prompt).toStrictEqual([ { role: 'user', content: [{ type: 'text', text: 'prompt' }], providerOptions: undefined, }, ]); return { ...dummyResponseValues, content: [ { type: 'text', text: '{ "content": "Hello, world!" }' }, ], }; }, }), schema: z.object({ content: z.string() }), schemaName: 'test-name', schemaDescription: 'test description', prompt: 'prompt', }); assert.deepStrictEqual(result.object, { content: 'Hello, world!' }); }); }); describe('result.request', () => { it('should contain request information', async () => { const result = await generateObject({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [{ type: 'text', text: '{ "content": "Hello, world!" }' }], request: { body: 'test body', }, }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); expect(result.request).toStrictEqual({ body: 'test body', }); }); }); describe('result.response', () => { it('should contain response information', async () => { const result = await generateObject({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [{ type: 'text', text: '{ "content": "Hello, world!" }' }], response: { id: 'test-id-from-model', timestamp: new Date(10000), modelId: 'test-response-model-id', headers: { 'custom-response-header': 'response-header-value', }, body: 'test body', }, }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); expect(result.response).toStrictEqual({ id: 'test-id-from-model', timestamp: new Date(10000), modelId: 'test-response-model-id', headers: { 'custom-response-header': 'response-header-value', }, body: 'test body', }); }); }); describe('zod schema', () => { it('should generate object when using zod transform', async () => { const model = new MockLanguageModelV2({ doGenerate: { ...dummyResponseValues, content: [{ type: 'text', text: '{ "content": "Hello, world!" }' }], }, }); const result = await generateObject({ model, schema: z.object({ content: z .string() .transform(value => value.length) .pipe(z.number()), }), prompt: 'prompt', }); expect(result.object).toMatchInlineSnapshot(` { "content": 13, } `); expect(model.doGenerateCalls[0].prompt).toMatchInlineSnapshot(` [ { "content": [ { "text": "prompt", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, ] `); expect(model.doGenerateCalls[0].responseFormat).toMatchInlineSnapshot(` { "description": undefined, "name": undefined, "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "content": { "type": "number", }, }, "required": [ "content", ], "type": "object", }, "type": "json", } `); }); it('should generate object when using zod prePreprocess', async () => { const model = new MockLanguageModelV2({ doGenerate: { ...dummyResponseValues, content: [{ type: 'text', text: '{ "content": "Hello, world!" }' }], }, }); const result = await generateObject({ model, schema: z.object({ content: z.preprocess( val => (typeof val === 'number' ? String(val) : val), z.string(), ), }), prompt: 'prompt', }); expect(result.object).toMatchInlineSnapshot(` { "content": "Hello, world!", } `); expect(model.doGenerateCalls[0].prompt).toMatchInlineSnapshot(` [ { "content": [ { "text": "prompt", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, ] `); expect(model.doGenerateCalls[0].responseFormat).toMatchInlineSnapshot(` { "description": undefined, "name": undefined, "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "content": { "type": "string", }, }, "required": [ "content", ], "type": "object", }, "type": "json", } `); }); }); describe('custom schema', () => { it('should generate object', async () => { const model = new MockLanguageModelV2({ doGenerate: { ...dummyResponseValues, content: [{ type: 'text', text: '{ "content": "Hello, world!" }' }], }, }); const result = await generateObject({ model, schema: jsonSchema({ type: 'object', properties: { content: { type: 'string' } }, required: ['content'], additionalProperties: false, }), prompt: 'prompt', }); expect(result.object).toMatchInlineSnapshot(` { "content": "Hello, world!", } `); expect(model.doGenerateCalls[0].prompt).toMatchInlineSnapshot(` [ { "content": [ { "text": "prompt", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, ] `); expect(model.doGenerateCalls[0].responseFormat).toMatchInlineSnapshot(` { "description": undefined, "name": undefined, "schema": { "additionalProperties": false, "properties": { "content": { "type": "string", }, }, "required": [ "content", ], "type": "object", }, "type": "json", } `); }); }); describe('result.toJsonResponse', () => { it('should return JSON response', async () => { const result = await generateObject({ model: new MockLanguageModelV2({ doGenerate: async ({}) => ({ ...dummyResponseValues, content: [{ type: 'text', text: '{ "content": "Hello, world!" }' }], }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); const response = result.toJsonResponse(); assert.strictEqual(response.status, 200); assert.strictEqual( response.headers.get('Content-Type'), 'application/json; charset=utf-8', ); assert.deepStrictEqual( await convertReadableStreamToArray( response.body!.pipeThrough(new TextDecoderStream()), ), ['{"content":"Hello, world!"}'], ); }); }); describe('result.providerMetadata', () => { it('should contain provider metadata', async () => { const result = await generateObject({ model: new MockLanguageModelV2({ doGenerate: async ({}) => ({ ...dummyResponseValues, content: [{ type: 'text', text: '{ "content": "Hello, world!" }' }], providerMetadata: { exampleProvider: { a: 10, b: 20, }, }, }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); expect(result.providerMetadata).toStrictEqual({ exampleProvider: { a: 10, b: 20, }, }); }); }); describe('options.headers', () => { it('should pass headers to model', async () => { const result = await generateObject({ model: new MockLanguageModelV2({ doGenerate: async ({ headers }) => { expect(headers).toStrictEqual({ 'custom-request-header': 'request-header-value', }); return { ...dummyResponseValues, content: [ { type: 'text', text: '{ "content": "headers test" }' }, ], }; }, }), schema: z.object({ content: z.string() }), prompt: 'prompt', headers: { 'custom-request-header': 'request-header-value' }, }); expect(result.object).toStrictEqual({ content: 'headers test' }); }); }); describe('options.repairText', () => { it('should be able to repair a JSONParseError', async () => { const result = await generateObject({ model: new MockLanguageModelV2({ doGenerate: async ({}) => { return { ...dummyResponseValues, content: [ { type: 'text', text: '{ "content": "provider metadata test" ', }, ], }; }, }), schema: z.object({ content: z.string() }), prompt: 'prompt', experimental_repairText: async ({ text, error }) => { expect(error).toBeInstanceOf(JSONParseError); expect(text).toStrictEqual('{ "content": "provider metadata test" '); return text + '}'; }, }); expect(result.object).toStrictEqual({ content: 'provider metadata test', }); }); it('should be able to repair a TypeValidationError', async () => { const result = await generateObject({ model: new MockLanguageModelV2({ doGenerate: async ({}) => { return { ...dummyResponseValues, content: [ { type: 'text', text: '{ "content-a": "provider metadata test" }', }, ], }; }, }), schema: z.object({ content: z.string() }), prompt: 'prompt', experimental_repairText: async ({ text, error }) => { expect(error).toBeInstanceOf(TypeValidationError); expect(text).toStrictEqual( '{ "content-a": "provider metadata test" }', ); return `{ "content": "provider metadata test" }`; }, }); expect(result.object).toStrictEqual({ content: 'provider metadata test', }); }); it('should be able to handle repair that returns null', async () => { const result = generateObject({ model: new MockLanguageModelV2({ doGenerate: async ({}) => { return { ...dummyResponseValues, content: [ { type: 'text', text: '{ "content-a": "provider metadata test" }', }, ], }; }, }), schema: z.object({ content: z.string() }), prompt: 'prompt', experimental_repairText: async ({ text, error }) => { expect(error).toBeInstanceOf(TypeValidationError); expect(text).toStrictEqual( '{ "content-a": "provider metadata test" }', ); return null; }, }); expect(result).rejects.toThrow( 'No object generated: response did not match schema.', ); }); }); describe('options.providerOptions', () => { it('should pass provider options to model', async () => { const result = await generateObject({ model: new MockLanguageModelV2({ doGenerate: async ({ providerOptions }) => { expect(providerOptions).toStrictEqual({ aProvider: { someKey: 'someValue' }, }); return { ...dummyResponseValues, content: [ { type: 'text', text: '{ "content": "provider metadata test" }', }, ], }; }, }), schema: z.object({ content: z.string() }), prompt: 'prompt', providerOptions: { aProvider: { someKey: 'someValue' }, }, }); expect(result.object).toStrictEqual({ content: 'provider metadata test', }); }); }); describe('error handling', () => { function verifyNoObjectGeneratedError( error: unknown, { message }: { message: string }, ) { originalVerifyNoObjectGeneratedError(error, { message, response: { id: 'id-1', timestamp: new Date(123), modelId: 'm-1', }, usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30, reasoningTokens: undefined, cachedInputTokens: undefined, }, finishReason: 'stop', }); } it('should throw NoObjectGeneratedError when schema validation fails', async () => { try { await generateObject({ model: new MockLanguageModelV2({ doGenerate: async ({}) => ({ ...dummyResponseValues, content: [{ type: 'text', text: '{ "content": 123 }' }], }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); fail('must throw error'); } catch (error) { verifyNoObjectGeneratedError(error, { message: 'No object generated: response did not match schema.', }); } }); it('should throw NoObjectGeneratedError when parsing fails', async () => { try { await generateObject({ model: new MockLanguageModelV2({ doGenerate: async ({}) => ({ ...dummyResponseValues, content: [{ type: 'text', text: '{ broken json' }], }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); fail('must throw error'); } catch (error) { verifyNoObjectGeneratedError(error, { message: 'No object generated: could not parse the response.', }); } }); it('should throw NoObjectGeneratedError when parsing fails with repairResponse', async () => { try { await generateObject({ model: new MockLanguageModelV2({ doGenerate: async ({}) => ({ ...dummyResponseValues, content: [{ type: 'text', text: '{ broken json' }], }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', experimental_repairText: async ({ text }) => text + '{', }); fail('must throw error'); } catch (error) { verifyNoObjectGeneratedError(error, { message: 'No object generated: could not parse the response.', }); } }); it('should throw NoObjectGeneratedError when no text is available', async () => { try { await generateObject({ model: new MockLanguageModelV2({ doGenerate: async ({}) => ({ ...dummyResponseValues, content: [], }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); fail('must throw error'); } catch (error) { verifyNoObjectGeneratedError(error, { message: 'No object generated: the model did not return a response.', }); } }); }); }); describe('output = "array"', () => { it('should generate an array with 3 elements', async () => { const model = new MockLanguageModelV2({ doGenerate: { ...dummyResponseValues, content: [ { type: 'text', text: JSON.stringify({ elements: [ { content: 'element 1' }, { content: 'element 2' }, { content: 'element 3' }, ], }), }, ], }, }); const result = await generateObject({ model, schema: z.object({ content: z.string() }), output: 'array', prompt: 'prompt', }); expect(result.object).toMatchInlineSnapshot(` [ { "content": "element 1", }, { "content": "element 2", }, { "content": "element 3", }, ] `); expect(model.doGenerateCalls[0].prompt).toMatchInlineSnapshot(` [ { "content": [ { "text": "prompt", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, ] `); expect(model.doGenerateCalls[0].responseFormat).toMatchInlineSnapshot(` { "description": undefined, "name": undefined, "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "elements": { "items": { "additionalProperties": false, "properties": { "content": { "type": "string", }, }, "required": [ "content", ], "type": "object", }, "type": "array", }, }, "required": [ "elements", ], "type": "object", }, "type": "json", } `); }); }); describe('output = "enum"', () => { it('should generate an enum value', async () => { const model = new MockLanguageModelV2({ doGenerate: { ...dummyResponseValues, content: [ { type: 'text', text: JSON.stringify({ result: 'sunny' }), }, ], }, }); const result = await generateObject({ model, output: 'enum', enum: ['sunny', 'rainy', 'snowy'], prompt: 'prompt', }); expect(result.object).toEqual('sunny'); expect(model.doGenerateCalls[0].prompt).toMatchInlineSnapshot(` [ { "content": [ { "text": "prompt", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, ] `); expect(model.doGenerateCalls[0].responseFormat).toMatchInlineSnapshot(` { "description": undefined, "name": undefined, "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "result": { "enum": [ "sunny", "rainy", "snowy", ], "type": "string", }, }, "required": [ "result", ], "type": "object", }, "type": "json", } `); }); }); describe('output = "no-schema"', () => { it('should generate object', async () => { const model = new MockLanguageModelV2({ doGenerate: { ...dummyResponseValues, content: [{ type: 'text', text: '{ "content": "Hello, world!" }' }], }, }); const result = await generateObject({ model, output: 'no-schema', prompt: 'prompt', }); expect(result.object).toMatchInlineSnapshot(` { "content": "Hello, world!", } `); expect(model.doGenerateCalls[0].prompt).toMatchInlineSnapshot(` [ { "content": [ { "text": "prompt", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, ] `); expect(model.doGenerateCalls[0].responseFormat).toMatchInlineSnapshot(` { "description": undefined, "name": undefined, "schema": undefined, "type": "json", } `); }); }); describe('telemetry', () => { let tracer: MockTracer; beforeEach(() => { tracer = new MockTracer(); }); it('should not record any telemetry data when not explicitly enabled', async () => { await generateObject({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [{ type: 'text', text: '{ "content": "Hello, world!" }' }], }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); assert.deepStrictEqual(tracer.jsonSpans, []); }); it('should record telemetry data when enabled', async () => { await generateObject({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [{ type: 'text', text: '{ "content": "Hello, world!" }' }], response: { id: 'test-id-from-model', timestamp: new Date(10000), modelId: 'test-response-model-id', }, providerMetadata: { testProvider: { testKey: 'testValue', }, }, }), }), schema: z.object({ content: z.string() }), schemaName: 'test-name', schemaDescription: 'test description', prompt: 'prompt', topK: 0.1, topP: 0.2, frequencyPenalty: 0.3, presencePenalty: 0.4, temperature: 0.5, headers: { header1: 'value1', header2: 'value2', }, experimental_telemetry: { isEnabled: true, functionId: 'test-function-id', metadata: { test1: 'value1', test2: false, }, tracer, }, }); expect(tracer.jsonSpans).toMatchSnapshot(); }); it('should not record telemetry inputs / outputs when disabled', async () => { await generateObject({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [{ type: 'text', text: '{ "content": "Hello, world!" }' }], response: { id: 'test-id-from-model', timestamp: new Date(10000), modelId: 'test-response-model-id', }, }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', experimental_telemetry: { isEnabled: true, recordInputs: false, recordOutputs: false, tracer, }, }); expect(tracer.jsonSpans).toMatchSnapshot(); }); }); describe('options.messages', () => { it('should support models that use "this" context in supportedUrls', async () => { let supportedUrlsCalled = false; class MockLanguageModelWithImageSupport extends MockLanguageModelV2 { constructor() { super({ supportedUrls: () => { supportedUrlsCalled = true; // Reference 'this' to verify context return this.modelId === 'mock-model-id' ? ({ 'image/*': [/^https:\/\/.*$/] } as Record<string, RegExp[]>) : {}; }, doGenerate: async () => ({ ...dummyResponseValues, content: [{ type: 'text', text: '{ "content": "Hello, world!" }' }], }), }); } } const model = new MockLanguageModelWithImageSupport(); const result = await generateObject({ model, schema: z.object({ content: z.string() }), messages: [ { role: 'user', content: [{ type: 'image', image: 'https://example.com/test.jpg' }], }, ], }); expect(result.object).toStrictEqual({ content: 'Hello, world!' }); expect(supportedUrlsCalled).toBe(true); }); }); --- File: /ai/packages/ai/src/generate-object/generate-object.ts --- import { JSONValue } from '@ai-sdk/provider'; import { createIdGenerator, InferSchema, ProviderOptions, Schema, } from '@ai-sdk/provider-utils'; import * as z3 from 'zod/v3'; import * as z4 from 'zod/v4'; import { NoObjectGeneratedError } from '../error/no-object-generated-error'; import { extractContentText } from '../generate-text/extract-content-text'; import { resolveLanguageModel } from '../model/resolve-model'; import { CallSettings } from '../prompt/call-settings'; import { convertToLanguageModelPrompt } from '../prompt/convert-to-language-model-prompt'; import { prepareCallSettings } from '../prompt/prepare-call-settings'; import { Prompt } from '../prompt/prompt'; import { standardizePrompt } from '../prompt/standardize-prompt'; import { wrapGatewayError } from '../prompt/wrap-gateway-error'; import { assembleOperationName } from '../telemetry/assemble-operation-name'; import { getBaseTelemetryAttributes } from '../telemetry/get-base-telemetry-attributes'; import { getTracer } from '../telemetry/get-tracer'; import { recordSpan } from '../telemetry/record-span'; import { selectTelemetryAttributes } from '../telemetry/select-telemetry-attributes'; import { stringifyForTelemetry } from '../telemetry/stringify-for-telemetry'; import { TelemetrySettings } from '../telemetry/telemetry-settings'; import { CallWarning, FinishReason, LanguageModel, } from '../types/language-model'; import { LanguageModelRequestMetadata } from '../types/language-model-request-metadata'; import { LanguageModelResponseMetadata } from '../types/language-model-response-metadata'; import { ProviderMetadata } from '../types/provider-metadata'; import { LanguageModelUsage } from '../types/usage'; import { prepareHeaders } from '../util/prepare-headers'; import { prepareRetries } from '../util/prepare-retries'; import { GenerateObjectResult } from './generate-object-result'; import { getOutputStrategy } from './output-strategy'; import { parseAndValidateObjectResultWithRepair } from './parse-and-validate-object-result'; import { RepairTextFunction } from './repair-text'; import { validateObjectGenerationInput } from './validate-object-generation-input'; const originalGenerateId = createIdGenerator({ prefix: 'aiobj', size: 24 }); /** Generate a structured, typed object for a given prompt and schema using a language model. This function does not stream the output. If you want to stream the output, use `streamObject` instead. @param model - The language model to use. @param tools - Tools that are accessible to and can be called by the model. The model needs to support calling tools. @param system - A system message that will be part of the prompt. @param prompt - A simple text prompt. You can either use `prompt` or `messages` but not both. @param messages - A list of messages. You can either use `prompt` or `messages` but not both. @param maxOutputTokens - Maximum number of tokens to generate. @param temperature - Temperature setting. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both. @param topP - Nucleus sampling. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both. @param topK - Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature. @param presencePenalty - Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The value is passed through to the provider. The range depends on the provider and model. @param frequencyPenalty - Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The value is passed through to the provider. The range depends on the provider and model. @param stopSequences - Stop sequences. If set, the model will stop generating text when one of the stop sequences is generated. @param seed - The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results. @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2. @param abortSignal - An optional abort signal that can be used to cancel the call. @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. @param schema - The schema of the object that the model should generate. @param schemaName - Optional name of the output that should be generated. Used by some providers for additional LLM guidance, e.g. via tool or schema name. @param schemaDescription - Optional description of the output that should be generated. Used by some providers for additional LLM guidance, e.g. via tool or schema description. @param output - The type of the output. - 'object': The output is an object. - 'array': The output is an array. - 'enum': The output is an enum. - 'no-schema': The output is not a schema. @param experimental_repairText - A function that attempts to repair the raw output of the model to enable JSON parsing. @param experimental_telemetry - Optional telemetry configuration (experimental). @param providerOptions - Additional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. @returns A result object that contains the generated object, the finish reason, the token usage, and additional information. */ export async function generateObject< SCHEMA extends | z3.Schema | z4.core.$ZodType | Schema = z4.core.$ZodType<JSONValue>, OUTPUT extends | 'object' | 'array' | 'enum' | 'no-schema' = InferSchema<SCHEMA> extends string ? 'enum' : 'object', RESULT = OUTPUT extends 'array' ? Array<InferSchema<SCHEMA>> : InferSchema<SCHEMA>, >( options: Omit<CallSettings, 'stopSequences'> & Prompt & (OUTPUT extends 'enum' ? { /** The enum values that the model should use. */ enum: Array<RESULT>; mode?: 'json'; output: 'enum'; } : OUTPUT extends 'no-schema' ? {} : { /** The schema of the object that the model should generate. */ schema: SCHEMA; /** Optional name of the output that should be generated. Used by some providers for additional LLM guidance, e.g. via tool or schema name. */ schemaName?: string; /** Optional description of the output that should be generated. Used by some providers for additional LLM guidance, e.g. via tool or schema description. */ schemaDescription?: string; /** The mode to use for object generation. The schema is converted into a JSON schema and used in one of the following ways - 'auto': The provider will choose the best mode for the model. - 'tool': A tool with the JSON schema as parameters is provided and the provider is instructed to use it. - 'json': The JSON schema and an instruction are injected into the prompt. If the provider supports JSON mode, it is enabled. If the provider supports JSON grammars, the grammar is used. Please note that most providers do not support all modes. Default and recommended: 'auto' (best mode for the model). */ mode?: 'auto' | 'json' | 'tool'; }) & { output?: OUTPUT; /** The language model to use. */ model: LanguageModel; /** A function that attempts to repair the raw output of the model to enable JSON parsing. */ experimental_repairText?: RepairTextFunction; /** Optional telemetry configuration (experimental). */ experimental_telemetry?: TelemetrySettings; /** Additional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; /** * Internal. For test use only. May change without notice. */ _internal?: { generateId?: () => string; currentDate?: () => Date; }; }, ): Promise<GenerateObjectResult<RESULT>> { const { model: modelArg, output = 'object', system, prompt, messages, maxRetries: maxRetriesArg, abortSignal, headers, experimental_repairText: repairText, experimental_telemetry: telemetry, providerOptions, _internal: { generateId = originalGenerateId, currentDate = () => new Date(), } = {}, ...settings } = options; const model = resolveLanguageModel(modelArg); const enumValues = 'enum' in options ? options.enum : undefined; const { schema: inputSchema, schemaDescription, schemaName, } = 'schema' in options ? options : {}; validateObjectGenerationInput({ output, schema: inputSchema, schemaName, schemaDescription, enumValues, }); const { maxRetries, retry } = prepareRetries({ maxRetries: maxRetriesArg, abortSignal, }); const outputStrategy = getOutputStrategy({ output, schema: inputSchema, enumValues, }); const callSettings = prepareCallSettings(settings); const baseTelemetryAttributes = getBaseTelemetryAttributes({ model, telemetry, headers, settings: { ...callSettings, maxRetries }, }); const tracer = getTracer(telemetry); try { return await recordSpan({ name: 'ai.generateObject', attributes: selectTelemetryAttributes({ telemetry, attributes: { ...assembleOperationName({ operationId: 'ai.generateObject', telemetry, }), ...baseTelemetryAttributes, // specific settings that only make sense on the outer level: 'ai.prompt': { input: () => JSON.stringify({ system, prompt, messages }), }, 'ai.schema': outputStrategy.jsonSchema != null ? { input: () => JSON.stringify(outputStrategy.jsonSchema) } : undefined, 'ai.schema.name': schemaName, 'ai.schema.description': schemaDescription, 'ai.settings.output': outputStrategy.type, }, }), tracer, fn: async span => { let result: string; let finishReason: FinishReason; let usage: LanguageModelUsage; let warnings: CallWarning[] | undefined; let response: LanguageModelResponseMetadata; let request: LanguageModelRequestMetadata; let resultProviderMetadata: ProviderMetadata | undefined; const standardizedPrompt = await standardizePrompt({ system, prompt, messages, }); const promptMessages = await convertToLanguageModelPrompt({ prompt: standardizedPrompt, supportedUrls: await model.supportedUrls, }); const generateResult = await retry(() => recordSpan({ name: 'ai.generateObject.doGenerate', attributes: selectTelemetryAttributes({ telemetry, attributes: { ...assembleOperationName({ operationId: 'ai.generateObject.doGenerate', telemetry, }), ...baseTelemetryAttributes, 'ai.prompt.messages': { input: () => stringifyForTelemetry(promptMessages), }, // standardized gen-ai llm span attributes: 'gen_ai.system': model.provider, 'gen_ai.request.model': model.modelId, 'gen_ai.request.frequency_penalty': callSettings.frequencyPenalty, 'gen_ai.request.max_tokens': callSettings.maxOutputTokens, 'gen_ai.request.presence_penalty': callSettings.presencePenalty, 'gen_ai.request.temperature': callSettings.temperature, 'gen_ai.request.top_k': callSettings.topK, 'gen_ai.request.top_p': callSettings.topP, }, }), tracer, fn: async span => { const result = await model.doGenerate({ responseFormat: { type: 'json', schema: outputStrategy.jsonSchema, name: schemaName, description: schemaDescription, }, ...prepareCallSettings(settings), prompt: promptMessages, providerOptions, abortSignal, headers, }); const responseData = { id: result.response?.id ?? generateId(), timestamp: result.response?.timestamp ?? currentDate(), modelId: result.response?.modelId ?? model.modelId, headers: result.response?.headers, body: result.response?.body, }; const text = extractContentText(result.content); if (text === undefined) { throw new NoObjectGeneratedError({ message: 'No object generated: the model did not return a response.', response: responseData, usage: result.usage, finishReason: result.finishReason, }); } // Add response information to the span: span.setAttributes( selectTelemetryAttributes({ telemetry, attributes: { 'ai.response.finishReason': result.finishReason, 'ai.response.object': { output: () => text }, 'ai.response.id': responseData.id, 'ai.response.model': responseData.modelId, 'ai.response.timestamp': responseData.timestamp.toISOString(), 'ai.response.providerMetadata': JSON.stringify( result.providerMetadata, ), // TODO rename telemetry attributes to inputTokens and outputTokens 'ai.usage.promptTokens': result.usage.inputTokens, 'ai.usage.completionTokens': result.usage.outputTokens, // standardized gen-ai llm span attributes: 'gen_ai.response.finish_reasons': [result.finishReason], 'gen_ai.response.id': responseData.id, 'gen_ai.response.model': responseData.modelId, 'gen_ai.usage.input_tokens': result.usage.inputTokens, 'gen_ai.usage.output_tokens': result.usage.outputTokens, }, }), ); return { ...result, objectText: text, responseData }; }, }), ); result = generateResult.objectText; finishReason = generateResult.finishReason; usage = generateResult.usage; warnings = generateResult.warnings; resultProviderMetadata = generateResult.providerMetadata; request = generateResult.request ?? {}; response = generateResult.responseData; const object = await parseAndValidateObjectResultWithRepair( result, outputStrategy, repairText, { response, usage, finishReason, }, ); // Add response information to the span: span.setAttributes( selectTelemetryAttributes({ telemetry, attributes: { 'ai.response.finishReason': finishReason, 'ai.response.object': { output: () => JSON.stringify(object), }, 'ai.response.providerMetadata': JSON.stringify( resultProviderMetadata, ), // TODO rename telemetry attributes to inputTokens and outputTokens 'ai.usage.promptTokens': usage.inputTokens, 'ai.usage.completionTokens': usage.outputTokens, }, }), ); return new DefaultGenerateObjectResult({ object, finishReason, usage, warnings, request, response, providerMetadata: resultProviderMetadata, }); }, }); } catch (error) { throw wrapGatewayError(error); } } class DefaultGenerateObjectResult<T> implements GenerateObjectResult<T> { readonly object: GenerateObjectResult<T>['object']; readonly finishReason: GenerateObjectResult<T>['finishReason']; readonly usage: GenerateObjectResult<T>['usage']; readonly warnings: GenerateObjectResult<T>['warnings']; readonly providerMetadata: GenerateObjectResult<T>['providerMetadata']; readonly response: GenerateObjectResult<T>['response']; readonly request: GenerateObjectResult<T>['request']; constructor(options: { object: GenerateObjectResult<T>['object']; finishReason: GenerateObjectResult<T>['finishReason']; usage: GenerateObjectResult<T>['usage']; warnings: GenerateObjectResult<T>['warnings']; providerMetadata: GenerateObjectResult<T>['providerMetadata']; response: GenerateObjectResult<T>['response']; request: GenerateObjectResult<T>['request']; }) { this.object = options.object; this.finishReason = options.finishReason; this.usage = options.usage; this.warnings = options.warnings; this.providerMetadata = options.providerMetadata; this.response = options.response; this.request = options.request; } toJsonResponse(init?: ResponseInit): Response { return new Response(JSON.stringify(this.object), { status: init?.status ?? 200, headers: prepareHeaders(init?.headers, { 'content-type': 'application/json; charset=utf-8', }), }); } } --- File: /ai/packages/ai/src/generate-object/index.ts --- export { generateObject } from './generate-object'; export type { RepairTextFunction } from './repair-text'; export type { GenerateObjectResult } from './generate-object-result'; export { streamObject } from './stream-object'; export type { StreamObjectOnFinishCallback } from './stream-object'; export type { ObjectStreamPart, StreamObjectResult, } from './stream-object-result'; --- File: /ai/packages/ai/src/generate-object/inject-json-instruction.test.ts --- import { JSONSchema7 } from '@ai-sdk/provider'; import { injectJsonInstruction } from './inject-json-instruction'; const basicSchema: JSONSchema7 = { type: 'object', properties: { name: { type: 'string' }, age: { type: 'number' }, }, required: ['name', 'age'], }; it('should handle basic case with prompt and schema', () => { const result = injectJsonInstruction({ prompt: 'Generate a person', schema: basicSchema, }); expect(result).toBe( 'Generate a person\n\n' + 'JSON schema:\n' + '{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"}},"required":["name","age"]}\n' + 'You MUST answer with a JSON object that matches the JSON schema above.', ); }); it('should handle only prompt, no schema', () => { const result = injectJsonInstruction({ prompt: 'Generate a person', }); expect(result).toBe('Generate a person\n\nYou MUST answer with JSON.'); }); it('should handle only schema, no prompt', () => { const result = injectJsonInstruction({ schema: basicSchema, }); expect(result).toBe( 'JSON schema:\n' + '{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"}},"required":["name","age"]}\n' + 'You MUST answer with a JSON object that matches the JSON schema above.', ); }); it('should handle no prompt, no schema', () => { const result = injectJsonInstruction({}); expect(result).toBe('You MUST answer with JSON.'); }); it('should handle custom schemaPrefix and schemaSuffix', () => { const result = injectJsonInstruction({ prompt: 'Generate a person', schema: basicSchema, schemaPrefix: 'Custom prefix:', schemaSuffix: 'Custom suffix', }); expect(result).toBe( 'Generate a person\n\n' + 'Custom prefix:\n' + '{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"}},"required":["name","age"]}\n' + 'Custom suffix', ); }); it('should handle empty string prompt', () => { const result = injectJsonInstruction({ prompt: '', schema: basicSchema, }); expect(result).toBe( 'JSON schema:\n' + '{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"}},"required":["name","age"]}\n' + 'You MUST answer with a JSON object that matches the JSON schema above.', ); }); it('should handle empty object schema', () => { const result = injectJsonInstruction({ prompt: 'Generate something', schema: {}, }); expect(result).toBe( 'Generate something\n\n' + 'JSON schema:\n' + '{}\n' + 'You MUST answer with a JSON object that matches the JSON schema above.', ); }); it('should handle complex nested schema', () => { const complexSchema: JSONSchema7 = { type: 'object', properties: { person: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'number' }, address: { type: 'object', properties: { street: { type: 'string' }, city: { type: 'string' }, }, }, }, }, }, }; const result = injectJsonInstruction({ prompt: 'Generate a complex person', schema: complexSchema, }); expect(result).toBe( 'Generate a complex person\n\n' + 'JSON schema:\n' + '{"type":"object","properties":{"person":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"},"address":{"type":"object","properties":{"street":{"type":"string"},"city":{"type":"string"}}}}}}}\n' + 'You MUST answer with a JSON object that matches the JSON schema above.', ); }); it('should handle schema with special characters', () => { const specialSchema: JSONSchema7 = { type: 'object', properties: { 'special@property': { type: 'string' }, 'emoji😊': { type: 'string' }, }, }; const result = injectJsonInstruction({ schema: specialSchema, }); expect(result).toBe( 'JSON schema:\n' + '{"type":"object","properties":{"special@property":{"type":"string"},"emoji😊":{"type":"string"}}}\n' + 'You MUST answer with a JSON object that matches the JSON schema above.', ); }); it('should handle very long prompt and schema', () => { const longPrompt = 'A'.repeat(1000); const longSchema: JSONSchema7 = { type: 'object', properties: {}, }; for (let i = 0; i < 100; i++) { longSchema.properties![`prop${i}`] = { type: 'string' }; } const result = injectJsonInstruction({ prompt: longPrompt, schema: longSchema, }); expect(result).toBe( longPrompt + '\n\n' + 'JSON schema:\n' + JSON.stringify(longSchema) + '\n' + 'You MUST answer with a JSON object that matches the JSON schema above.', ); }); it('should handle null values for optional parameters', () => { const result = injectJsonInstruction({ prompt: null as any, schema: null as any, schemaPrefix: null as any, schemaSuffix: null as any, }); expect(result).toBe(''); }); it('should handle undefined values for optional parameters', () => { const result = injectJsonInstruction({ prompt: undefined, schema: undefined, schemaPrefix: undefined, schemaSuffix: undefined, }); expect(result).toBe('You MUST answer with JSON.'); }); --- File: /ai/packages/ai/src/generate-object/inject-json-instruction.ts --- import { JSONSchema7 } from '@ai-sdk/provider'; const DEFAULT_SCHEMA_PREFIX = 'JSON schema:'; const DEFAULT_SCHEMA_SUFFIX = 'You MUST answer with a JSON object that matches the JSON schema above.'; const DEFAULT_GENERIC_SUFFIX = 'You MUST answer with JSON.'; export function injectJsonInstruction({ prompt, schema, schemaPrefix = schema != null ? DEFAULT_SCHEMA_PREFIX : undefined, schemaSuffix = schema != null ? DEFAULT_SCHEMA_SUFFIX : DEFAULT_GENERIC_SUFFIX, }: { prompt?: string; schema?: JSONSchema7; schemaPrefix?: string; schemaSuffix?: string; }): string { return [ prompt != null && prompt.length > 0 ? prompt : undefined, prompt != null && prompt.length > 0 ? '' : undefined, // add a newline if prompt is not null schemaPrefix, schema != null ? JSON.stringify(schema) : undefined, schemaSuffix, ] .filter(line => line != null) .join('\n'); } --- File: /ai/packages/ai/src/generate-object/output-strategy.ts --- import { isJSONArray, isJSONObject, JSONObject, JSONSchema7, JSONValue, TypeValidationError, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { asSchema, safeValidateTypes, Schema, ValidationResult, } from '@ai-sdk/provider-utils'; import * as z3 from 'zod/v3'; import * as z4 from 'zod/v4'; import { NoObjectGeneratedError } from '../error/no-object-generated-error'; import { AsyncIterableStream, createAsyncIterableStream, } from '../util/async-iterable-stream'; import { DeepPartial } from '../util/deep-partial'; import { FinishReason, LanguageModelResponseMetadata, LanguageModelUsage, } from '../types'; import { ObjectStreamPart } from './stream-object-result'; export interface OutputStrategy<PARTIAL, RESULT, ELEMENT_STREAM> { readonly type: 'object' | 'array' | 'enum' | 'no-schema'; readonly jsonSchema: JSONSchema7 | undefined; validatePartialResult({ value, textDelta, isFinalDelta, }: { value: JSONValue; textDelta: string; isFirstDelta: boolean; isFinalDelta: boolean; latestObject: PARTIAL | undefined; }): Promise< ValidationResult<{ partial: PARTIAL; textDelta: string; }> >; validateFinalResult( value: JSONValue | undefined, context: { text: string; response: LanguageModelResponseMetadata; usage: LanguageModelUsage; }, ): Promise<ValidationResult<RESULT>>; createElementStream( originalStream: ReadableStream<ObjectStreamPart<PARTIAL>>, ): ELEMENT_STREAM; } const noSchemaOutputStrategy: OutputStrategy<JSONValue, JSONValue, never> = { type: 'no-schema', jsonSchema: undefined, async validatePartialResult({ value, textDelta }) { return { success: true, value: { partial: value, textDelta } }; }, async validateFinalResult( value: JSONValue | undefined, context: { text: string; response: LanguageModelResponseMetadata; usage: LanguageModelUsage; finishReason: FinishReason; }, ): Promise<ValidationResult<JSONValue>> { return value === undefined ? { success: false, error: new NoObjectGeneratedError({ message: 'No object generated: response did not match schema.', text: context.text, response: context.response, usage: context.usage, finishReason: context.finishReason, }), } : { success: true, value }; }, createElementStream() { throw new UnsupportedFunctionalityError({ functionality: 'element streams in no-schema mode', }); }, }; const objectOutputStrategy = <OBJECT>( schema: Schema<OBJECT>, ): OutputStrategy<DeepPartial<OBJECT>, OBJECT, never> => ({ type: 'object', jsonSchema: schema.jsonSchema, async validatePartialResult({ value, textDelta }) { return { success: true, value: { // Note: currently no validation of partial results: partial: value as DeepPartial<OBJECT>, textDelta, }, }; }, async validateFinalResult( value: JSONValue | undefined, ): Promise<ValidationResult<OBJECT>> { return safeValidateTypes({ value, schema }); }, createElementStream() { throw new UnsupportedFunctionalityError({ functionality: 'element streams in object mode', }); }, }); const arrayOutputStrategy = <ELEMENT>( schema: Schema<ELEMENT>, ): OutputStrategy<ELEMENT[], ELEMENT[], AsyncIterableStream<ELEMENT>> => { // remove $schema from schema.jsonSchema: const { $schema, ...itemSchema } = schema.jsonSchema; return { type: 'enum', // wrap in object that contains array of elements, since most LLMs will not // be able to generate an array directly: // possible future optimization: use arrays directly when model supports grammar-guided generation jsonSchema: { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { elements: { type: 'array', items: itemSchema }, }, required: ['elements'], additionalProperties: false, }, async validatePartialResult({ value, latestObject, isFirstDelta, isFinalDelta, }) { // check that the value is an object that contains an array of elements: if (!isJSONObject(value) || !isJSONArray(value.elements)) { return { success: false, error: new TypeValidationError({ value, cause: 'value must be an object that contains an array of elements', }), }; } const inputArray = value.elements as Array<JSONObject>; const resultArray: Array<ELEMENT> = []; for (let i = 0; i < inputArray.length; i++) { const element = inputArray[i]; const result = await safeValidateTypes({ value: element, schema }); // special treatment for last processed element: // ignore parse or validation failures, since they indicate that the // last element is incomplete and should not be included in the result, // unless it is the final delta if (i === inputArray.length - 1 && !isFinalDelta) { continue; } if (!result.success) { return result; } resultArray.push(result.value); } // calculate delta: const publishedElementCount = latestObject?.length ?? 0; let textDelta = ''; if (isFirstDelta) { textDelta += '['; } if (publishedElementCount > 0) { textDelta += ','; } textDelta += resultArray .slice(publishedElementCount) // only new elements .map(element => JSON.stringify(element)) .join(','); if (isFinalDelta) { textDelta += ']'; } return { success: true, value: { partial: resultArray, textDelta, }, }; }, async validateFinalResult( value: JSONValue | undefined, ): Promise<ValidationResult<Array<ELEMENT>>> { // check that the value is an object that contains an array of elements: if (!isJSONObject(value) || !isJSONArray(value.elements)) { return { success: false, error: new TypeValidationError({ value, cause: 'value must be an object that contains an array of elements', }), }; } const inputArray = value.elements as Array<JSONObject>; // check that each element in the array is of the correct type: for (const element of inputArray) { const result = await safeValidateTypes({ value: element, schema }); if (!result.success) { return result; } } return { success: true, value: inputArray as Array<ELEMENT> }; }, createElementStream( originalStream: ReadableStream<ObjectStreamPart<ELEMENT[]>>, ) { let publishedElements = 0; return createAsyncIterableStream( originalStream.pipeThrough( new TransformStream<ObjectStreamPart<ELEMENT[]>, ELEMENT>({ transform(chunk, controller) { switch (chunk.type) { case 'object': { const array = chunk.object; // publish new elements one by one: for ( ; publishedElements < array.length; publishedElements++ ) { controller.enqueue(array[publishedElements]); } break; } case 'text-delta': case 'finish': case 'error': // suppress error (use onError instead) break; default: { const _exhaustiveCheck: never = chunk; throw new Error( `Unsupported chunk type: ${_exhaustiveCheck}`, ); } } }, }), ), ); }, }; }; const enumOutputStrategy = <ENUM extends string>( enumValues: Array<ENUM>, ): OutputStrategy<string, ENUM, never> => { return { type: 'enum', // wrap in object that contains result, since most LLMs will not // be able to generate an enum value directly: // possible future optimization: use enums directly when model supports top-level enums jsonSchema: { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { result: { type: 'string', enum: enumValues }, }, required: ['result'], additionalProperties: false, }, async validateFinalResult( value: JSONValue | undefined, ): Promise<ValidationResult<ENUM>> { // check that the value is an object that contains an array of elements: if (!isJSONObject(value) || typeof value.result !== 'string') { return { success: false, error: new TypeValidationError({ value, cause: 'value must be an object that contains a string in the "result" property.', }), }; } const result = value.result as string; return enumValues.includes(result as ENUM) ? { success: true, value: result as ENUM } : { success: false, error: new TypeValidationError({ value, cause: 'value must be a string in the enum', }), }; }, async validatePartialResult({ value, textDelta }) { if (!isJSONObject(value) || typeof value.result !== 'string') { return { success: false, error: new TypeValidationError({ value, cause: 'value must be an object that contains a string in the "result" property.', }), }; } const result = value.result as string; const possibleEnumValues = enumValues.filter(enumValue => enumValue.startsWith(result), ); if (value.result.length === 0 || possibleEnumValues.length === 0) { return { success: false, error: new TypeValidationError({ value, cause: 'value must be a string in the enum', }), }; } return { success: true, value: { partial: possibleEnumValues.length > 1 ? result : possibleEnumValues[0], textDelta, }, }; }, createElementStream() { // no streaming in enum mode throw new UnsupportedFunctionalityError({ functionality: 'element streams in enum mode', }); }, }; }; export function getOutputStrategy<SCHEMA>({ output, schema, enumValues, }: { output: 'object' | 'array' | 'enum' | 'no-schema'; schema?: | z4.core.$ZodType<SCHEMA, any> | z3.Schema<SCHEMA, z3.ZodTypeDef, any> | Schema<SCHEMA>; enumValues?: Array<SCHEMA>; }): OutputStrategy<any, any, any> { switch (output) { case 'object': return objectOutputStrategy(asSchema(schema!)); case 'array': return arrayOutputStrategy(asSchema(schema!)); case 'enum': return enumOutputStrategy(enumValues! as Array<string>); case 'no-schema': return noSchemaOutputStrategy; default: { const _exhaustiveCheck: never = output; throw new Error(`Unsupported output: ${_exhaustiveCheck}`); } } } --- File: /ai/packages/ai/src/generate-object/parse-and-validate-object-result.ts --- import { JSONParseError, TypeValidationError } from '@ai-sdk/provider'; import { safeParseJSON } from '@ai-sdk/provider-utils'; import { NoObjectGeneratedError } from '../error/no-object-generated-error'; import type { FinishReason, LanguageModelResponseMetadata, LanguageModelUsage, } from '../types'; import type { OutputStrategy } from './output-strategy'; import { RepairTextFunction } from './repair-text'; /** * Parses and validates a result string by parsing it as JSON and validating against the output strategy. * * @param result - The result string to parse and validate * @param outputStrategy - The output strategy containing validation logic * @param context - Additional context for error reporting * @returns The validated result * @throws NoObjectGeneratedError if parsing or validation fails */ async function parseAndValidateObjectResult<RESULT>( result: string, outputStrategy: OutputStrategy<any, RESULT, any>, context: { response: LanguageModelResponseMetadata; usage: LanguageModelUsage; finishReason: FinishReason; }, ): Promise<RESULT> { const parseResult = await safeParseJSON({ text: result }); if (!parseResult.success) { throw new NoObjectGeneratedError({ message: 'No object generated: could not parse the response.', cause: parseResult.error, text: result, response: context.response, usage: context.usage, finishReason: context.finishReason, }); } const validationResult = await outputStrategy.validateFinalResult( parseResult.value, { text: result, response: context.response, usage: context.usage, }, ); if (!validationResult.success) { throw new NoObjectGeneratedError({ message: 'No object generated: response did not match schema.', cause: validationResult.error, text: result, response: context.response, usage: context.usage, finishReason: context.finishReason, }); } return validationResult.value; } /** * Parses and validates a result string by parsing it as JSON and validating against the output strategy. * If the result cannot be parsed, it attempts to repair the result using the repairText function. * * @param result - The result string to parse and validate * @param outputStrategy - The output strategy containing validation logic * @param repairText - A function that attempts to repair the result string * @param context - Additional context for error reporting * @returns The validated result * @throws NoObjectGeneratedError if parsing or validation fails */ export async function parseAndValidateObjectResultWithRepair<RESULT>( result: string, outputStrategy: OutputStrategy<any, RESULT, any>, repairText: RepairTextFunction | undefined, context: { response: LanguageModelResponseMetadata; usage: LanguageModelUsage; finishReason: FinishReason; }, ): Promise<RESULT> { try { return await parseAndValidateObjectResult(result, outputStrategy, context); } catch (error) { if ( repairText != null && NoObjectGeneratedError.isInstance(error) && (JSONParseError.isInstance(error.cause) || TypeValidationError.isInstance(error.cause)) ) { const repairedText = await repairText({ text: result, error: error.cause, }); if (repairedText === null) { throw error; } return await parseAndValidateObjectResult( repairedText, outputStrategy, context, ); } throw error; } } --- File: /ai/packages/ai/src/generate-object/repair-text.ts --- import { JSONParseError, TypeValidationError } from '@ai-sdk/provider'; /** A function that attempts to repair the raw output of the model to enable JSON parsing. Should return the repaired text or null if the text cannot be repaired. */ export type RepairTextFunction = (options: { text: string; error: JSONParseError | TypeValidationError; }) => Promise<string | null>; --- File: /ai/packages/ai/src/generate-object/stream-object-result.ts --- import { ServerResponse } from 'http'; import { AsyncIterableStream } from '../util/async-iterable-stream'; import { CallWarning, FinishReason, LanguageModelRequestMetadata, LanguageModelResponseMetadata, ProviderMetadata, } from '../types'; import { LanguageModelUsage } from '../types/usage'; /** The result of a `streamObject` call that contains the partial object stream and additional information. */ export interface StreamObjectResult<PARTIAL, RESULT, ELEMENT_STREAM> { /** Warnings from the model provider (e.g. unsupported settings) */ readonly warnings: Promise<CallWarning[] | undefined>; /** The token usage of the generated response. Resolved when the response is finished. */ readonly usage: Promise<LanguageModelUsage>; /** Additional provider-specific metadata. They are passed through from the provider to the AI SDK and enable provider-specific results that can be fully encapsulated in the provider. */ readonly providerMetadata: Promise<ProviderMetadata | undefined>; /** Additional request information from the last step. */ readonly request: Promise<LanguageModelRequestMetadata>; /** Additional response information. */ readonly response: Promise<LanguageModelResponseMetadata>; /** The reason why the generation finished. Taken from the last step. Resolved when the response is finished. */ readonly finishReason: Promise<FinishReason>; /** The generated object (typed according to the schema). Resolved when the response is finished. */ readonly object: Promise<RESULT>; /** Stream of partial objects. It gets more complete as the stream progresses. Note that the partial object is not validated. If you want to be certain that the actual content matches your schema, you need to implement your own validation for partial results. */ readonly partialObjectStream: AsyncIterableStream<PARTIAL>; /** * Stream over complete array elements. Only available if the output strategy is set to `array`. */ readonly elementStream: ELEMENT_STREAM; /** Text stream of the JSON representation of the generated object. It contains text chunks. When the stream is finished, the object is valid JSON that can be parsed. */ readonly textStream: AsyncIterableStream<string>; /** Stream of different types of events, including partial objects, errors, and finish events. Only errors that stop the stream, such as network errors, are thrown. */ readonly fullStream: AsyncIterableStream<ObjectStreamPart<PARTIAL>>; /** Writes text delta output to a Node.js response-like object. It sets a `Content-Type` header to `text/plain; charset=utf-8` and writes each text delta as a separate chunk. @param response A Node.js response-like object (ServerResponse). @param init Optional headers, status code, and status text. */ pipeTextStreamToResponse(response: ServerResponse, init?: ResponseInit): void; /** Creates a simple text stream response. The response has a `Content-Type` header set to `text/plain; charset=utf-8`. Each text delta is encoded as UTF-8 and sent as a separate chunk. Non-text-delta events are ignored. @param init Optional headers, status code, and status text. */ toTextStreamResponse(init?: ResponseInit): Response; } export type ObjectStreamPart<PARTIAL> = | { type: 'object'; object: PARTIAL; } | { type: 'text-delta'; textDelta: string; } | { type: 'error'; error: unknown; } | { type: 'finish'; finishReason: FinishReason; usage: LanguageModelUsage; response: LanguageModelResponseMetadata; providerMetadata?: ProviderMetadata; }; --- File: /ai/packages/ai/src/generate-object/stream-object.test-d.ts --- import { JSONValue } from '@ai-sdk/provider'; import { expectTypeOf } from 'vitest'; import { z } from 'zod/v4'; import { AsyncIterableStream } from '../util/async-iterable-stream'; import { FinishReason } from '../types'; import { streamObject } from './stream-object'; describe('streamObject', () => { it('should have finishReason property with correct type', () => { const result = streamObject({ schema: z.object({ number: z.number() }), model: undefined!, }); expectTypeOf<typeof result.finishReason>().toEqualTypeOf< Promise<FinishReason> >(); }); it('should support enum types', async () => { const result = await streamObject({ output: 'enum', enum: ['a', 'b', 'c'] as const, model: undefined!, }); expectTypeOf<typeof result.object>().toEqualTypeOf< Promise<'a' | 'b' | 'c'> >; for await (const text of result.partialObjectStream) { expectTypeOf(text).toEqualTypeOf<string>(); } }); it('should support schema types', async () => { const result = streamObject({ schema: z.object({ number: z.number() }), model: undefined!, }); expectTypeOf<typeof result.object>().toEqualTypeOf< Promise<{ number: number }> >(); }); it('should support no-schema output mode', async () => { const result = streamObject({ output: 'no-schema', model: undefined!, }); expectTypeOf<typeof result.object>().toEqualTypeOf<Promise<JSONValue>>(); }); it('should support array output mode', async () => { const result = streamObject({ output: 'array', schema: z.number(), model: undefined!, }); expectTypeOf<typeof result.partialObjectStream>().toEqualTypeOf< AsyncIterableStream<number[]> >(); expectTypeOf<typeof result.object>().toEqualTypeOf<Promise<number[]>>(); }); }); --- File: /ai/packages/ai/src/generate-object/stream-object.test.ts --- import { jsonSchema } from '@ai-sdk/provider-utils'; import { convertArrayToReadableStream, convertAsyncIterableToArray, convertReadableStreamToArray, } from '@ai-sdk/provider-utils/test'; import assert, { fail } from 'node:assert'; import { z } from 'zod/v4'; import { NoObjectGeneratedError, verifyNoObjectGeneratedError, } from '../error/no-object-generated-error'; import { AsyncIterableStream } from '../util/async-iterable-stream'; import { MockLanguageModelV2 } from '../test/mock-language-model-v2'; import { createMockServerResponse } from '../test/mock-server-response'; import { MockTracer } from '../test/mock-tracer'; import { streamObject } from './stream-object'; import { StreamObjectResult } from './stream-object-result'; import { JSONParseError, LanguageModelV2, LanguageModelV2CallWarning, LanguageModelV2StreamPart, TypeValidationError, } from '@ai-sdk/provider'; const testUsage = { inputTokens: 3, outputTokens: 10, totalTokens: 13, reasoningTokens: undefined, cachedInputTokens: undefined, }; function createTestModel({ warnings = [], stream = convertArrayToReadableStream([ { type: 'stream-start', warnings, }, { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"content": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world` }, { type: 'text-delta', id: '1', delta: `!"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, providerMetadata: { testProvider: { testKey: 'testValue', }, }, }, ]), request = undefined, response = undefined, }: { stream?: ReadableStream<LanguageModelV2StreamPart>; request?: { body: string }; response?: { headers: Record<string, string> }; warnings?: LanguageModelV2CallWarning[]; } = {}) { return new MockLanguageModelV2({ doStream: async () => ({ stream, request, response, warnings }), }); } describe('streamObject', () => { describe('output = "object"', () => { describe('result.objectStream', () => { it('should send object deltas', async () => { const mockModel = createTestModel(); const result = streamObject({ model: mockModel, schema: z.object({ content: z.string() }), prompt: 'prompt', }); expect(await convertAsyncIterableToArray(result.partialObjectStream)) .toMatchInlineSnapshot(` [ {}, { "content": "Hello, ", }, { "content": "Hello, world", }, { "content": "Hello, world!", }, ] `); expect(mockModel.doStreamCalls[0].responseFormat) .toMatchInlineSnapshot(` { "description": undefined, "name": undefined, "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "content": { "type": "string", }, }, "required": [ "content", ], "type": "object", }, "type": "json", } `); }); it('should use name and description', async () => { const model = createTestModel(); const result = streamObject({ model, schema: z.object({ content: z.string() }), schemaName: 'test-name', schemaDescription: 'test description', prompt: 'prompt', }); expect(await convertAsyncIterableToArray(result.partialObjectStream)) .toMatchInlineSnapshot(` [ {}, { "content": "Hello, ", }, { "content": "Hello, world", }, { "content": "Hello, world!", }, ] `); expect(model.doStreamCalls[0].prompt).toMatchInlineSnapshot(` [ { "content": [ { "text": "prompt", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, ] `); expect(model.doStreamCalls[0].responseFormat).toMatchInlineSnapshot(` { "description": "test description", "name": "test-name", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "content": { "type": "string", }, }, "required": [ "content", ], "type": "object", }, "type": "json", } `); }); it('should suppress error in partialObjectStream', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => { throw new Error('test error'); }, }), schema: z.object({ content: z.string() }), prompt: 'prompt', onError: () => {}, }); expect( await convertAsyncIterableToArray(result.partialObjectStream), ).toStrictEqual([]); }); it('should invoke onError callback with Error', async () => { const result: Array<{ error: unknown }> = []; const resultObject = streamObject({ model: new MockLanguageModelV2({ doStream: async () => { throw new Error('test error'); }, }), schema: z.object({ content: z.string() }), prompt: 'prompt', onError(event) { result.push(event); }, }); // consume stream await convertAsyncIterableToArray(resultObject.partialObjectStream); expect(result).toStrictEqual([{ error: new Error('test error') }]); }); }); describe('result.fullStream', () => { it('should send full stream data', async () => { const result = streamObject({ model: createTestModel(), schema: z.object({ content: z.string() }), prompt: 'prompt', }); expect( await convertAsyncIterableToArray(result.fullStream), ).toMatchSnapshot(); }); }); describe('result.textStream', () => { it('should send text stream', async () => { const result = streamObject({ model: createTestModel(), schema: z.object({ content: z.string() }), prompt: 'prompt', }); assert.deepStrictEqual( await convertAsyncIterableToArray(result.textStream), ['{ ', '"content": "Hello, ', 'world', '!"', ' }'], ); }); }); describe('result.toTextStreamResponse', () => { it('should create a Response with a text stream', async () => { const result = streamObject({ model: createTestModel(), schema: z.object({ content: z.string() }), prompt: 'prompt', }); const response = result.toTextStreamResponse(); assert.strictEqual(response.status, 200); assert.strictEqual( response.headers.get('Content-Type'), 'text/plain; charset=utf-8', ); assert.deepStrictEqual( await convertReadableStreamToArray( response.body!.pipeThrough(new TextDecoderStream()), ), ['{ ', '"content": "Hello, ', 'world', '!"', ' }'], ); }); }); describe('result.pipeTextStreamToResponse', async () => { it('should write text deltas to a Node.js response-like object', async () => { const mockResponse = createMockServerResponse(); const result = streamObject({ model: createTestModel(), schema: z.object({ content: z.string() }), prompt: 'prompt', }); result.pipeTextStreamToResponse(mockResponse); await mockResponse.waitForEnd(); expect(mockResponse.statusCode).toBe(200); expect(mockResponse.headers).toMatchInlineSnapshot(` { "content-type": "text/plain; charset=utf-8", } `); expect(mockResponse.getDecodedChunks()).toMatchInlineSnapshot(` [ "{ ", ""content": "Hello, ", "world", "!"", " }", ] `); }); }); describe('result.usage', () => { it('should resolve with token usage', async () => { const result = streamObject({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ "content": "Hello, world!" }', }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage }, ]), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); // consume stream (runs in parallel) convertAsyncIterableToArray(result.partialObjectStream); expect(await result.usage).toMatchInlineSnapshot(` { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, } `); }); }); describe('result.providerMetadata', () => { it('should resolve with provider metadata', async () => { const result = streamObject({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ "content": "Hello, world!" }', }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, providerMetadata: { testProvider: { testKey: 'testValue' }, }, }, ]), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); // consume stream (runs in parallel) convertAsyncIterableToArray(result.partialObjectStream); expect(await result.providerMetadata).toStrictEqual({ testProvider: { testKey: 'testValue' }, }); }); }); describe('result.response', () => { it('should resolve with response information', async () => { const result = streamObject({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{"content": "Hello, world!"}', }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), response: { headers: { call: '2' } }, }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); // consume stream (runs in parallel) convertAsyncIterableToArray(result.partialObjectStream); expect(await result.response).toStrictEqual({ id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), headers: { call: '2' }, }); }); }); describe('result.request', () => { it('should contain request information', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{"content": "Hello, world!"}', }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), request: { body: 'test body' }, }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); // consume stream (runs in parallel) await convertAsyncIterableToArray(result.partialObjectStream); expect(await result.request).toStrictEqual({ body: 'test body', }); }); }); describe('result.object', () => { it('should resolve with typed object', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"content": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world` }, { type: 'text-delta', id: '1', delta: `!"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); // consume stream (runs in parallel) convertAsyncIterableToArray(result.partialObjectStream); assert.deepStrictEqual(await result.object, { content: 'Hello, world!', }); }); it('should reject object promise when the streamed object does not match the schema', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"invalid": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world` }, { type: 'text-delta', id: '1', delta: `!"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); // consume stream (runs in parallel) convertAsyncIterableToArray(result.partialObjectStream); expect(result.object).rejects.toThrow(NoObjectGeneratedError); }); it('should not lead to unhandled promise rejections when the streamed object does not match the schema', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"invalid": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world` }, { type: 'text-delta', id: '1', delta: `!"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); // consume stream (runs in parallel) convertAsyncIterableToArray(result.partialObjectStream); // unhandled promise rejection should not be thrown (Vitest does this automatically) }); }); describe('result.finishReason', () => { it('should resolve with finish reason', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"content": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world` }, { type: 'text-delta', id: '1', delta: `!"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); // consume stream (runs in parallel) convertAsyncIterableToArray(result.partialObjectStream); expect(await result.finishReason).toStrictEqual('stop'); }); }); describe('options.onFinish', () => { it('should be called when a valid object is generated', async () => { let result: Parameters< Required<Parameters<typeof streamObject>[0]>['onFinish'] >[0]; const { partialObjectStream } = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ "content": "Hello, world!" }', }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, providerMetadata: { testProvider: { testKey: 'testValue' }, }, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', onFinish: async event => { result = event as unknown as typeof result; }, }); // consume stream await convertAsyncIterableToArray(partialObjectStream); expect(result!).toMatchSnapshot(); }); it("should be called when object doesn't match the schema", async () => { let result: Parameters< Required<Parameters<typeof streamObject>[0]>['onFinish'] >[0]; const { partialObjectStream, object } = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"invalid": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world` }, { type: 'text-delta', id: '1', delta: `!"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', onFinish: async event => { result = event as unknown as typeof result; }, }); // consume stream await convertAsyncIterableToArray(partialObjectStream); // consume expected error rejection await object.catch(() => {}); expect(result!).toMatchSnapshot(); }); }); describe('options.headers', () => { it('should pass headers to model', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async ({ headers }) => { expect(headers).toStrictEqual({ 'custom-request-header': 'request-header-value', }); return { stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: `{ "content": "headers test" }`, }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }), schema: z.object({ content: z.string() }), prompt: 'prompt', headers: { 'custom-request-header': 'request-header-value' }, }); expect( await convertAsyncIterableToArray(result.partialObjectStream), ).toStrictEqual([{ content: 'headers test' }]); }); }); describe('options.providerOptions', () => { it('should pass provider options to model', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async ({ providerOptions }) => { expect(providerOptions).toStrictEqual({ aProvider: { someKey: 'someValue' }, }); return { stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: `{ "content": "provider metadata test" }`, }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }), schema: z.object({ content: z.string() }), prompt: 'prompt', providerOptions: { aProvider: { someKey: 'someValue' }, }, }); expect( await convertAsyncIterableToArray(result.partialObjectStream), ).toStrictEqual([{ content: 'provider metadata test' }]); }); }); describe('custom schema', () => { it('should send object deltas', async () => { const mockModel = createTestModel(); const result = streamObject({ model: mockModel, schema: jsonSchema({ type: 'object', properties: { content: { type: 'string' } }, required: ['content'], additionalProperties: false, }), prompt: 'prompt', }); expect(await convertAsyncIterableToArray(result.partialObjectStream)) .toMatchInlineSnapshot(` [ {}, { "content": "Hello, ", }, { "content": "Hello, world", }, { "content": "Hello, world!", }, ] `); expect(mockModel.doStreamCalls[0].responseFormat) .toMatchInlineSnapshot(` { "description": undefined, "name": undefined, "schema": { "additionalProperties": false, "properties": { "content": { "type": "string", }, }, "required": [ "content", ], "type": "object", }, "type": "json", } `); }); }); describe('error handling', () => { it('should throw NoObjectGeneratedError when schema validation fails', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ "content": 123 }' }, { type: 'text-end', id: '1' }, { type: 'response-metadata', id: 'id-1', timestamp: new Date(123), modelId: 'model-1', }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); try { await convertAsyncIterableToArray(result.partialObjectStream); await result.object; fail('must throw error'); } catch (error) { verifyNoObjectGeneratedError(error, { message: 'No object generated: response did not match schema.', response: { id: 'id-1', timestamp: new Date(123), modelId: 'model-1', }, usage: testUsage, finishReason: 'stop', }); } }); it('should throw NoObjectGeneratedError when parsing fails', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ broken json' }, { type: 'text-end', id: '1' }, { type: 'response-metadata', id: 'id-1', timestamp: new Date(123), modelId: 'model-1', }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); try { await convertAsyncIterableToArray(result.partialObjectStream); await result.object; fail('must throw error'); } catch (error) { verifyNoObjectGeneratedError(error, { message: 'No object generated: could not parse the response.', response: { id: 'id-1', timestamp: new Date(123), modelId: 'model-1', }, usage: testUsage, finishReason: 'stop', }); } }); it('should throw NoObjectGeneratedError when no text is generated', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-1', timestamp: new Date(123), modelId: 'model-1', }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', }); try { await convertAsyncIterableToArray(result.partialObjectStream); await result.object; fail('must throw error'); } catch (error) { verifyNoObjectGeneratedError(error, { message: 'No object generated: could not parse the response.', response: { id: 'id-1', timestamp: new Date(123), modelId: 'model-1', }, usage: testUsage, finishReason: 'stop', }); } }); }); }); describe('output = "array"', () => { describe('array with 3 elements', () => { let result: StreamObjectResult< { content: string }[], { content: string }[], AsyncIterableStream<{ content: string }> >; let onFinishResult: Parameters< Required<Parameters<typeof streamObject>[0]>['onFinish'] >[0]; beforeEach(async () => { result = streamObject({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{"elements":[' }, // first element: { type: 'text-delta', id: '1', delta: '{' }, { type: 'text-delta', id: '1', delta: '"content":' }, { type: 'text-delta', id: '1', delta: `"element 1"` }, { type: 'text-delta', id: '1', delta: '},' }, // second element: { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"content": ' }, { type: 'text-delta', id: '1', delta: `"element 2"` }, { type: 'text-delta', id: '1', delta: '},' }, // third element: { type: 'text-delta', id: '1', delta: '{' }, { type: 'text-delta', id: '1', delta: '"content":' }, { type: 'text-delta', id: '1', delta: `"element 3"` }, { type: 'text-delta', id: '1', delta: '}' }, // end of array { type: 'text-delta', id: '1', delta: ']' }, { type: 'text-delta', id: '1', delta: '}' }, { type: 'text-end', id: '1' }, // finish { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), schema: z.object({ content: z.string() }), output: 'array', prompt: 'prompt', onFinish: async event => { onFinishResult = event as unknown as typeof onFinishResult; }, }); }); it('should stream only complete objects in partialObjectStream', async () => { assert.deepStrictEqual( await convertAsyncIterableToArray(result.partialObjectStream), [ [], [{ content: 'element 1' }], [{ content: 'element 1' }, { content: 'element 2' }], [ { content: 'element 1' }, { content: 'element 2' }, { content: 'element 3' }, ], ], ); }); it('should stream only complete objects in textStream', async () => { assert.deepStrictEqual( await convertAsyncIterableToArray(result.textStream), [ '[', '{"content":"element 1"}', ',{"content":"element 2"}', ',{"content":"element 3"}]', ], ); }); it('should have the correct object result', async () => { // consume stream await convertAsyncIterableToArray(result.partialObjectStream); expect(await result.object).toStrictEqual([ { content: 'element 1' }, { content: 'element 2' }, { content: 'element 3' }, ]); }); it('should call onFinish callback with full array', async () => { expect(onFinishResult.object).toStrictEqual([ { content: 'element 1' }, { content: 'element 2' }, { content: 'element 3' }, ]); }); it('should stream elements individually in elementStream', async () => { assert.deepStrictEqual( await convertAsyncIterableToArray(result.elementStream), [ { content: 'element 1' }, { content: 'element 2' }, { content: 'element 3' }, ], ); }); }); describe('array with 2 elements streamed in 1 chunk', () => { let result: StreamObjectResult< { content: string }[], { content: string }[], AsyncIterableStream<{ content: string }> >; let onFinishResult: Parameters< Required<Parameters<typeof streamObject>[0]>['onFinish'] >[0]; beforeEach(async () => { result = streamObject({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1', }, { type: 'text-delta', id: '1', delta: '{"elements":[{"content":"element 1"},{"content":"element 2"}]}', }, { type: 'text-end', id: '1', }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), schema: z.object({ content: z.string() }), output: 'array', prompt: 'prompt', onFinish: async event => { onFinishResult = event as unknown as typeof onFinishResult; }, }); }); it('should stream only complete objects in partialObjectStream', async () => { assert.deepStrictEqual( await convertAsyncIterableToArray(result.partialObjectStream), [[{ content: 'element 1' }, { content: 'element 2' }]], ); }); it('should stream only complete objects in textStream', async () => { assert.deepStrictEqual( await convertAsyncIterableToArray(result.textStream), ['[{"content":"element 1"},{"content":"element 2"}]'], ); }); it('should have the correct object result', async () => { // consume stream await convertAsyncIterableToArray(result.partialObjectStream); expect(await result.object).toStrictEqual([ { content: 'element 1' }, { content: 'element 2' }, ]); }); it('should call onFinish callback with full array', async () => { expect(onFinishResult.object).toStrictEqual([ { content: 'element 1' }, { content: 'element 2' }, ]); }); it('should stream elements individually in elementStream', async () => { assert.deepStrictEqual( await convertAsyncIterableToArray(result.elementStream), [{ content: 'element 1' }, { content: 'element 2' }], ); }); }); }); describe('output = "enum"', () => { it('should stream an enum value', async () => { const mockModel = createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"result": ' }, { type: 'text-delta', id: '1', delta: `"su` }, { type: 'text-delta', id: '1', delta: `nny` }, { type: 'text-delta', id: '1', delta: `"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage }, ]), }); const result = streamObject({ model: mockModel, output: 'enum', enum: ['sunny', 'rainy', 'snowy'], prompt: 'prompt', }); expect(await convertAsyncIterableToArray(result.partialObjectStream)) .toMatchInlineSnapshot(` [ "sunny", ] `); expect(mockModel.doStreamCalls[0].responseFormat).toMatchInlineSnapshot(` { "description": undefined, "name": undefined, "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "result": { "enum": [ "sunny", "rainy", "snowy", ], "type": "string", }, }, "required": [ "result", ], "type": "object", }, "type": "json", } `); }); it('should not stream incorrect values', async () => { const mockModel = new MockLanguageModelV2({ doStream: { stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"result": ' }, { type: 'text-delta', id: '1', delta: `"foo` }, { type: 'text-delta', id: '1', delta: `bar` }, { type: 'text-delta', id: '1', delta: `"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }, }); const result = streamObject({ model: mockModel, output: 'enum', enum: ['sunny', 'rainy', 'snowy'], prompt: 'prompt', }); expect( await convertAsyncIterableToArray(result.partialObjectStream), ).toMatchInlineSnapshot(`[]`); }); it('should handle ambiguous values', async () => { const mockModel = createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"result": ' }, { type: 'text-delta', id: '1', delta: `"foo` }, { type: 'text-delta', id: '1', delta: `bar` }, { type: 'text-delta', id: '1', delta: `"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }); const result = streamObject({ model: mockModel, output: 'enum', enum: ['foobar', 'foobar2'], prompt: 'prompt', }); expect(await convertAsyncIterableToArray(result.partialObjectStream)) .toMatchInlineSnapshot(` [ "foo", "foobar", ] `); }); it('should handle non-ambiguous values', async () => { const mockModel = createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"result": ' }, { type: 'text-delta', id: '1', delta: `"foo` }, { type: 'text-delta', id: '1', delta: `bar` }, { type: 'text-delta', id: '1', delta: `"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }); const result = streamObject({ model: mockModel, output: 'enum', enum: ['foobar', 'barfoo'], prompt: 'prompt', }); expect(await convertAsyncIterableToArray(result.partialObjectStream)) .toMatchInlineSnapshot(` [ "foobar", ] `); }); }); describe('output = "no-schema"', () => { it('should send object deltas', async () => { const mockModel = createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"content": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world` }, { type: 'text-delta', id: '1', delta: `!"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }); const result = streamObject({ model: mockModel, output: 'no-schema', prompt: 'prompt', }); expect(await convertAsyncIterableToArray(result.partialObjectStream)) .toMatchInlineSnapshot(` [ {}, { "content": "Hello, ", }, { "content": "Hello, world", }, { "content": "Hello, world!", }, ] `); expect(mockModel.doStreamCalls[0].responseFormat).toMatchInlineSnapshot(` { "description": undefined, "name": undefined, "schema": undefined, "type": "json", } `); }); }); describe('telemetry', () => { let tracer: MockTracer; beforeEach(() => { tracer = new MockTracer(); }); it('should not record any telemetry data when not explicitly enabled', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"content": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world` }, { type: 'text-delta', id: '1', delta: `!"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', _internal: { now: () => 0 }, }); // consume stream await convertAsyncIterableToArray(result.partialObjectStream); expect(tracer.jsonSpans).toMatchSnapshot(); }); it('should record telemetry data when enabled', async () => { const result = streamObject({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"content": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world` }, { type: 'text-delta', id: '1', delta: `!"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, providerMetadata: { testProvider: { testKey: 'testValue', }, }, }, ]), }), schema: z.object({ content: z.string() }), schemaName: 'test-name', schemaDescription: 'test description', prompt: 'prompt', topK: 0.1, topP: 0.2, frequencyPenalty: 0.3, presencePenalty: 0.4, temperature: 0.5, headers: { header1: 'value1', header2: 'value2', }, experimental_telemetry: { isEnabled: true, functionId: 'test-function-id', metadata: { test1: 'value1', test2: false, }, tracer, }, _internal: { now: () => 0 }, }); // consume stream await convertAsyncIterableToArray(result.partialObjectStream); expect(tracer.jsonSpans).toMatchSnapshot(); }); it('should not record telemetry inputs / outputs when disabled', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"content": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world` }, { type: 'text-delta', id: '1', delta: `!"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', experimental_telemetry: { isEnabled: true, recordInputs: false, recordOutputs: false, tracer, }, _internal: { now: () => 0 }, }); // consume stream await convertAsyncIterableToArray(result.partialObjectStream); expect(tracer.jsonSpans).toMatchSnapshot(); }); }); describe('options.messages', () => { it('should support models that use "this" context in supportedUrls', async () => { let supportedUrlsCalled = false; class MockLanguageModelWithImageSupport extends MockLanguageModelV2 { constructor() { super({ supportedUrls: () => { supportedUrlsCalled = true; // Reference 'this' to verify context return this.modelId === 'mock-model-id' ? ({ 'image/*': [/^https:\/\/.*$/] } as Record< string, RegExp[] >) : {}; }, doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ "content": "Hello, world!" }', }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage }, ]), }), }); } } const model = new MockLanguageModelWithImageSupport(); const result = streamObject({ model, schema: z.object({ content: z.string() }), messages: [ { role: 'user', content: [{ type: 'image', image: 'https://example.com/test.jpg' }], }, ], }); const chunks = await convertAsyncIterableToArray(result.textStream); expect(chunks.join('')).toBe('{ "content": "Hello, world!" }'); expect(supportedUrlsCalled).toBe(true); }); }); describe('options.experimental_repairText', () => { it('should be able to repair a JSONParseError', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ "content": "provider metadata test" ', }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', experimental_repairText: async ({ text, error }) => { expect(error).toBeInstanceOf(JSONParseError); expect(text).toStrictEqual('{ "content": "provider metadata test" '); return text + '}'; }, }); // consume stream await convertAsyncIterableToArray(result.partialObjectStream); expect(await result.object).toStrictEqual({ content: 'provider metadata test', }); }); it('should be able to repair a TypeValidationError', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ "content-a": "provider metadata test" }', }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', experimental_repairText: async ({ text, error }) => { expect(error).toBeInstanceOf(TypeValidationError); expect(text).toStrictEqual( '{ "content-a": "provider metadata test" }', ); return `{ "content": "provider metadata test" }`; }, }); // consume stream await convertAsyncIterableToArray(result.partialObjectStream); expect(await result.object).toStrictEqual({ content: 'provider metadata test', }); }); it('should be able to handle repair that returns null', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ "content-a": "provider metadata test" }', }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', experimental_repairText: async ({ text, error }) => { expect(error).toBeInstanceOf(TypeValidationError); expect(text).toStrictEqual( '{ "content-a": "provider metadata test" }', ); return null; }, }); // consume stream await convertAsyncIterableToArray(result.partialObjectStream); expect(result.object).rejects.toThrow( 'No object generated: response did not match schema.', ); }); it('should be able to repair JSON wrapped with markdown code blocks', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '```json\n{ "content": "test message" }\n```', }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', experimental_repairText: async ({ text, error }) => { expect(error).toBeInstanceOf(JSONParseError); expect(text).toStrictEqual( '```json\n{ "content": "test message" }\n```', ); // Remove markdown code block wrapper const cleaned = text .replace(/^```json\s*/, '') .replace(/\s*```$/, ''); return cleaned; }, }); // consume stream await convertAsyncIterableToArray(result.partialObjectStream); expect(await result.object).toStrictEqual({ content: 'test message', }); }); it('should throw NoObjectGeneratedError when parsing fails with repairText', async () => { const result = streamObject({ model: new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ broken json' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }), schema: z.object({ content: z.string() }), prompt: 'prompt', experimental_repairText: async ({ text }) => text + '{', }); try { await convertAsyncIterableToArray(result.partialObjectStream); await result.object; fail('must throw error'); } catch (error) { verifyNoObjectGeneratedError(error, { message: 'No object generated: could not parse the response.', response: { id: 'id-0', timestamp: new Date(0), modelId: 'mock-model-id', }, usage: testUsage, finishReason: 'stop', }); } }); }); }); --- File: /ai/packages/ai/src/generate-object/stream-object.ts --- import { JSONValue, LanguageModelV2CallWarning, LanguageModelV2FinishReason, LanguageModelV2StreamPart, LanguageModelV2Usage, SharedV2ProviderMetadata, } from '@ai-sdk/provider'; import { createIdGenerator, ProviderOptions, type InferSchema, type Schema, } from '@ai-sdk/provider-utils'; import { ServerResponse } from 'http'; import * as z3 from 'zod/v3'; import * as z4 from 'zod/v4'; import { resolveLanguageModel } from '../model/resolve-model'; import { CallSettings } from '../prompt/call-settings'; import { convertToLanguageModelPrompt } from '../prompt/convert-to-language-model-prompt'; import { prepareCallSettings } from '../prompt/prepare-call-settings'; import { Prompt } from '../prompt/prompt'; import { standardizePrompt } from '../prompt/standardize-prompt'; import { wrapGatewayError } from '../prompt/wrap-gateway-error'; import { assembleOperationName } from '../telemetry/assemble-operation-name'; import { getBaseTelemetryAttributes } from '../telemetry/get-base-telemetry-attributes'; import { getTracer } from '../telemetry/get-tracer'; import { recordSpan } from '../telemetry/record-span'; import { selectTelemetryAttributes } from '../telemetry/select-telemetry-attributes'; import { stringifyForTelemetry } from '../telemetry/stringify-for-telemetry'; import { TelemetrySettings } from '../telemetry/telemetry-settings'; import { createTextStreamResponse } from '../text-stream/create-text-stream-response'; import { pipeTextStreamToResponse } from '../text-stream/pipe-text-stream-to-response'; import { CallWarning, FinishReason, LanguageModel, } from '../types/language-model'; import { LanguageModelRequestMetadata } from '../types/language-model-request-metadata'; import { LanguageModelResponseMetadata } from '../types/language-model-response-metadata'; import { ProviderMetadata } from '../types/provider-metadata'; import { LanguageModelUsage } from '../types/usage'; import { DeepPartial, isDeepEqualData, parsePartialJson } from '../util'; import { AsyncIterableStream, createAsyncIterableStream, } from '../util/async-iterable-stream'; import { createStitchableStream } from '../util/create-stitchable-stream'; import { DelayedPromise } from '../util/delayed-promise'; import { now as originalNow } from '../util/now'; import { prepareRetries } from '../util/prepare-retries'; import { getOutputStrategy, OutputStrategy } from './output-strategy'; import { parseAndValidateObjectResultWithRepair } from './parse-and-validate-object-result'; import { RepairTextFunction } from './repair-text'; import { ObjectStreamPart, StreamObjectResult } from './stream-object-result'; import { validateObjectGenerationInput } from './validate-object-generation-input'; const originalGenerateId = createIdGenerator({ prefix: 'aiobj', size: 24 }); /** Callback that is set using the `onError` option. @param event - The event that is passed to the callback. */ export type StreamObjectOnErrorCallback = (event: { error: unknown; }) => Promise<void> | void; /** Callback that is set using the `onFinish` option. @param event - The event that is passed to the callback. */ export type StreamObjectOnFinishCallback<RESULT> = (event: { /** The token usage of the generated response. */ usage: LanguageModelUsage; /** The generated object. Can be undefined if the final object does not match the schema. */ object: RESULT | undefined; /** Optional error object. This is e.g. a TypeValidationError when the final object does not match the schema. */ error: unknown | undefined; /** Response metadata. */ response: LanguageModelResponseMetadata; /** Warnings from the model provider (e.g. unsupported settings). */ warnings?: CallWarning[]; /** Additional provider-specific metadata. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerMetadata: ProviderMetadata | undefined; }) => Promise<void> | void; /** Generate a structured, typed object for a given prompt and schema using a language model. This function streams the output. If you do not want to stream the output, use `generateObject` instead. @param model - The language model to use. @param tools - Tools that are accessible to and can be called by the model. The model needs to support calling tools. @param system - A system message that will be part of the prompt. @param prompt - A simple text prompt. You can either use `prompt` or `messages` but not both. @param messages - A list of messages. You can either use `prompt` or `messages` but not both. @param maxOutputTokens - Maximum number of tokens to generate. @param temperature - Temperature setting. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both. @param topP - Nucleus sampling. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both. @param topK - Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature. @param presencePenalty - Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The value is passed through to the provider. The range depends on the provider and model. @param frequencyPenalty - Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The value is passed through to the provider. The range depends on the provider and model. @param stopSequences - Stop sequences. If set, the model will stop generating text when one of the stop sequences is generated. @param seed - The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results. @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2. @param abortSignal - An optional abort signal that can be used to cancel the call. @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. @param schema - The schema of the object that the model should generate. @param schemaName - Optional name of the output that should be generated. Used by some providers for additional LLM guidance, e.g. via tool or schema name. @param schemaDescription - Optional description of the output that should be generated. Used by some providers for additional LLM guidance, e.g. via tool or schema description. @param output - The type of the output. - 'object': The output is an object. - 'array': The output is an array. - 'enum': The output is an enum. - 'no-schema': The output is not a schema. @param experimental_telemetry - Optional telemetry configuration (experimental). @param providerOptions - Additional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. @returns A result object for accessing the partial object stream and additional information. */ export function streamObject< SCHEMA extends | z3.Schema | z4.core.$ZodType | Schema = z4.core.$ZodType<JSONValue>, OUTPUT extends | 'object' | 'array' | 'enum' | 'no-schema' = InferSchema<SCHEMA> extends string ? 'enum' : 'object', RESULT = OUTPUT extends 'array' ? Array<InferSchema<SCHEMA>> : InferSchema<SCHEMA>, >( options: Omit<CallSettings, 'stopSequences'> & Prompt & (OUTPUT extends 'enum' ? { /** The enum values that the model should use. */ enum: Array<RESULT>; mode?: 'json'; output: 'enum'; } : OUTPUT extends 'no-schema' ? {} : { /** The schema of the object that the model should generate. */ schema: SCHEMA; /** Optional name of the output that should be generated. Used by some providers for additional LLM guidance, e.g. via tool or schema name. */ schemaName?: string; /** Optional description of the output that should be generated. Used by some providers for additional LLM guidance, e.g. via tool or schema description. */ schemaDescription?: string; /** The mode to use for object generation. The schema is converted into a JSON schema and used in one of the following ways - 'auto': The provider will choose the best mode for the model. - 'tool': A tool with the JSON schema as parameters is provided and the provider is instructed to use it. - 'json': The JSON schema and an instruction are injected into the prompt. If the provider supports JSON mode, it is enabled. If the provider supports JSON grammars, the grammar is used. Please note that most providers do not support all modes. Default and recommended: 'auto' (best mode for the model). */ mode?: 'auto' | 'json' | 'tool'; }) & { output?: OUTPUT; /** The language model to use. */ model: LanguageModel; /** A function that attempts to repair the raw output of the model to enable JSON parsing. */ experimental_repairText?: RepairTextFunction; /** Optional telemetry configuration (experimental). */ experimental_telemetry?: TelemetrySettings; /** Additional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; /** Callback that is invoked when an error occurs during streaming. You can use it to log errors. The stream processing will pause until the callback promise is resolved. */ onError?: StreamObjectOnErrorCallback; /** Callback that is called when the LLM response and the final object validation are finished. */ onFinish?: StreamObjectOnFinishCallback<RESULT>; /** * Internal. For test use only. May change without notice. */ _internal?: { generateId?: () => string; currentDate?: () => Date; now?: () => number; }; }, ): StreamObjectResult< OUTPUT extends 'enum' ? string : OUTPUT extends 'array' ? RESULT : DeepPartial<RESULT>, OUTPUT extends 'array' ? RESULT : RESULT, OUTPUT extends 'array' ? RESULT extends Array<infer U> ? AsyncIterableStream<U> : never : never > { const { model, output = 'object', system, prompt, messages, maxRetries, abortSignal, headers, experimental_repairText: repairText, experimental_telemetry: telemetry, providerOptions, onError = ({ error }: { error: unknown }) => { console.error(error); }, onFinish, _internal: { generateId = originalGenerateId, currentDate = () => new Date(), now = originalNow, } = {}, ...settings } = options; const enumValues = 'enum' in options && options.enum ? options.enum : undefined; const { schema: inputSchema, schemaDescription, schemaName, } = 'schema' in options ? options : {}; validateObjectGenerationInput({ output, schema: inputSchema, schemaName, schemaDescription, enumValues, }); const outputStrategy = getOutputStrategy({ output, schema: inputSchema, enumValues, }); return new DefaultStreamObjectResult({ model, telemetry, headers, settings, maxRetries, abortSignal, outputStrategy, system, prompt, messages, schemaName, schemaDescription, providerOptions, repairText, onError, onFinish, generateId, currentDate, now, }); } class DefaultStreamObjectResult<PARTIAL, RESULT, ELEMENT_STREAM> implements StreamObjectResult<PARTIAL, RESULT, ELEMENT_STREAM> { private readonly _object = new DelayedPromise<RESULT>(); private readonly _usage = new DelayedPromise<LanguageModelUsage>(); private readonly _providerMetadata = new DelayedPromise< ProviderMetadata | undefined >(); private readonly _warnings = new DelayedPromise<CallWarning[] | undefined>(); private readonly _request = new DelayedPromise<LanguageModelRequestMetadata>(); private readonly _response = new DelayedPromise<LanguageModelResponseMetadata>(); private readonly _finishReason = new DelayedPromise<FinishReason>(); private readonly baseStream: ReadableStream<ObjectStreamPart<PARTIAL>>; private readonly outputStrategy: OutputStrategy< PARTIAL, RESULT, ELEMENT_STREAM >; constructor({ model: modelArg, headers, telemetry, settings, maxRetries: maxRetriesArg, abortSignal, outputStrategy, system, prompt, messages, schemaName, schemaDescription, providerOptions, repairText, onError, onFinish, generateId, currentDate, now, }: { model: LanguageModel; telemetry: TelemetrySettings | undefined; headers: Record<string, string | undefined> | undefined; settings: Omit<CallSettings, 'abortSignal' | 'headers'>; maxRetries: number | undefined; abortSignal: AbortSignal | undefined; outputStrategy: OutputStrategy<PARTIAL, RESULT, ELEMENT_STREAM>; system: Prompt['system']; prompt: Prompt['prompt']; messages: Prompt['messages']; schemaName: string | undefined; schemaDescription: string | undefined; providerOptions: ProviderOptions | undefined; repairText: RepairTextFunction | undefined; onError: StreamObjectOnErrorCallback; onFinish: StreamObjectOnFinishCallback<RESULT> | undefined; generateId: () => string; currentDate: () => Date; now: () => number; }) { const model = resolveLanguageModel(modelArg); const { maxRetries, retry } = prepareRetries({ maxRetries: maxRetriesArg, abortSignal, }); const callSettings = prepareCallSettings(settings); const baseTelemetryAttributes = getBaseTelemetryAttributes({ model, telemetry, headers, settings: { ...callSettings, maxRetries }, }); const tracer = getTracer(telemetry); const self = this; const stitchableStream = createStitchableStream<ObjectStreamPart<PARTIAL>>(); const eventProcessor = new TransformStream< ObjectStreamPart<PARTIAL>, ObjectStreamPart<PARTIAL> >({ transform(chunk, controller) { controller.enqueue(chunk); if (chunk.type === 'error') { onError({ error: wrapGatewayError(chunk.error) }); } }, }); this.baseStream = stitchableStream.stream.pipeThrough(eventProcessor); recordSpan({ name: 'ai.streamObject', attributes: selectTelemetryAttributes({ telemetry, attributes: { ...assembleOperationName({ operationId: 'ai.streamObject', telemetry, }), ...baseTelemetryAttributes, // specific settings that only make sense on the outer level: 'ai.prompt': { input: () => JSON.stringify({ system, prompt, messages }), }, 'ai.schema': outputStrategy.jsonSchema != null ? { input: () => JSON.stringify(outputStrategy.jsonSchema) } : undefined, 'ai.schema.name': schemaName, 'ai.schema.description': schemaDescription, 'ai.settings.output': outputStrategy.type, }, }), tracer, endWhenDone: false, fn: async rootSpan => { const standardizedPrompt = await standardizePrompt({ system, prompt, messages, }); const callOptions = { responseFormat: { type: 'json' as const, schema: outputStrategy.jsonSchema, name: schemaName, description: schemaDescription, }, ...prepareCallSettings(settings), prompt: await convertToLanguageModelPrompt({ prompt: standardizedPrompt, supportedUrls: await model.supportedUrls, }), providerOptions, abortSignal, headers, includeRawChunks: false, }; const transformer: Transformer< LanguageModelV2StreamPart, ObjectStreamInputPart > = { transform: (chunk, controller) => { switch (chunk.type) { case 'text-delta': controller.enqueue(chunk.delta); break; case 'response-metadata': case 'finish': case 'error': controller.enqueue(chunk); break; } }, }; const { result: { stream, response, request }, doStreamSpan, startTimestampMs, } = await retry(() => recordSpan({ name: 'ai.streamObject.doStream', attributes: selectTelemetryAttributes({ telemetry, attributes: { ...assembleOperationName({ operationId: 'ai.streamObject.doStream', telemetry, }), ...baseTelemetryAttributes, 'ai.prompt.messages': { input: () => stringifyForTelemetry(callOptions.prompt), }, // standardized gen-ai llm span attributes: 'gen_ai.system': model.provider, 'gen_ai.request.model': model.modelId, 'gen_ai.request.frequency_penalty': callSettings.frequencyPenalty, 'gen_ai.request.max_tokens': callSettings.maxOutputTokens, 'gen_ai.request.presence_penalty': callSettings.presencePenalty, 'gen_ai.request.temperature': callSettings.temperature, 'gen_ai.request.top_k': callSettings.topK, 'gen_ai.request.top_p': callSettings.topP, }, }), tracer, endWhenDone: false, fn: async doStreamSpan => ({ startTimestampMs: now(), doStreamSpan, result: await model.doStream(callOptions), }), }), ); self._request.resolve(request ?? {}); // store information for onFinish callback: let warnings: LanguageModelV2CallWarning[] | undefined; let usage: LanguageModelUsage = { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, }; let finishReason: LanguageModelV2FinishReason | undefined; let providerMetadata: ProviderMetadata | undefined; let object: RESULT | undefined; let error: unknown | undefined; // pipe chunks through a transformation stream that extracts metadata: let accumulatedText = ''; let textDelta = ''; let fullResponse: { id: string; timestamp: Date; modelId: string; } = { id: generateId(), timestamp: currentDate(), modelId: model.modelId, }; // Keep track of raw parse result before type validation, since e.g. Zod might // change the object by mapping properties. let latestObjectJson: JSONValue | undefined = undefined; let latestObject: PARTIAL | undefined = undefined; let isFirstChunk = true; let isFirstDelta = true; const transformedStream = stream .pipeThrough(new TransformStream(transformer)) .pipeThrough( new TransformStream< string | ObjectStreamInputPart, ObjectStreamPart<PARTIAL> >({ async transform(chunk, controller): Promise<void> { if ( typeof chunk === 'object' && chunk.type === 'stream-start' ) { warnings = chunk.warnings; return; // stream start chunks are sent immediately and do not count as first chunk } // Telemetry event for first chunk: if (isFirstChunk) { const msToFirstChunk = now() - startTimestampMs; isFirstChunk = false; doStreamSpan.addEvent('ai.stream.firstChunk', { 'ai.stream.msToFirstChunk': msToFirstChunk, }); doStreamSpan.setAttributes({ 'ai.stream.msToFirstChunk': msToFirstChunk, }); } // process partial text chunks if (typeof chunk === 'string') { accumulatedText += chunk; textDelta += chunk; const { value: currentObjectJson, state: parseState } = await parsePartialJson(accumulatedText); if ( currentObjectJson !== undefined && !isDeepEqualData(latestObjectJson, currentObjectJson) ) { const validationResult = await outputStrategy.validatePartialResult({ value: currentObjectJson, textDelta, latestObject, isFirstDelta, isFinalDelta: parseState === 'successful-parse', }); if ( validationResult.success && !isDeepEqualData( latestObject, validationResult.value.partial, ) ) { // inside inner check to correctly parse the final element in array mode: latestObjectJson = currentObjectJson; latestObject = validationResult.value.partial; controller.enqueue({ type: 'object', object: latestObject, }); controller.enqueue({ type: 'text-delta', textDelta: validationResult.value.textDelta, }); textDelta = ''; isFirstDelta = false; } } return; } switch (chunk.type) { case 'response-metadata': { fullResponse = { id: chunk.id ?? fullResponse.id, timestamp: chunk.timestamp ?? fullResponse.timestamp, modelId: chunk.modelId ?? fullResponse.modelId, }; break; } case 'finish': { // send final text delta: if (textDelta !== '') { controller.enqueue({ type: 'text-delta', textDelta }); } // store finish reason for telemetry: finishReason = chunk.finishReason; // store usage and metadata for promises and onFinish callback: usage = chunk.usage; providerMetadata = chunk.providerMetadata; controller.enqueue({ ...chunk, usage, response: fullResponse, }); // resolve promises that can be resolved now: self._usage.resolve(usage); self._providerMetadata.resolve(providerMetadata); self._response.resolve({ ...fullResponse, headers: response?.headers, }); self._finishReason.resolve(finishReason ?? 'unknown'); try { object = await parseAndValidateObjectResultWithRepair( accumulatedText, outputStrategy, repairText, { response: fullResponse, usage, finishReason, }, ); self._object.resolve(object); } catch (e) { error = e; self._object.reject(e); } break; } default: { controller.enqueue(chunk); break; } } }, // invoke onFinish callback and resolve toolResults promise when the stream is about to close: async flush(controller) { try { const finalUsage = usage ?? { promptTokens: NaN, completionTokens: NaN, totalTokens: NaN, }; doStreamSpan.setAttributes( selectTelemetryAttributes({ telemetry, attributes: { 'ai.response.finishReason': finishReason, 'ai.response.object': { output: () => JSON.stringify(object), }, 'ai.response.id': fullResponse.id, 'ai.response.model': fullResponse.modelId, 'ai.response.timestamp': fullResponse.timestamp.toISOString(), 'ai.response.providerMetadata': JSON.stringify(providerMetadata), 'ai.usage.inputTokens': finalUsage.inputTokens, 'ai.usage.outputTokens': finalUsage.outputTokens, 'ai.usage.totalTokens': finalUsage.totalTokens, 'ai.usage.reasoningTokens': finalUsage.reasoningTokens, 'ai.usage.cachedInputTokens': finalUsage.cachedInputTokens, // standardized gen-ai llm span attributes: 'gen_ai.response.finish_reasons': [finishReason], 'gen_ai.response.id': fullResponse.id, 'gen_ai.response.model': fullResponse.modelId, 'gen_ai.usage.input_tokens': finalUsage.inputTokens, 'gen_ai.usage.output_tokens': finalUsage.outputTokens, }, }), ); // finish doStreamSpan before other operations for correct timing: doStreamSpan.end(); // Add response information to the root span: rootSpan.setAttributes( selectTelemetryAttributes({ telemetry, attributes: { 'ai.usage.inputTokens': finalUsage.inputTokens, 'ai.usage.outputTokens': finalUsage.outputTokens, 'ai.usage.totalTokens': finalUsage.totalTokens, 'ai.usage.reasoningTokens': finalUsage.reasoningTokens, 'ai.usage.cachedInputTokens': finalUsage.cachedInputTokens, 'ai.response.object': { output: () => JSON.stringify(object), }, 'ai.response.providerMetadata': JSON.stringify(providerMetadata), }, }), ); // call onFinish callback: await onFinish?.({ usage: finalUsage, object, error, response: { ...fullResponse, headers: response?.headers, }, warnings, providerMetadata, }); } catch (error) { controller.enqueue({ type: 'error', error }); } finally { rootSpan.end(); } }, }), ); stitchableStream.addStream(transformedStream); }, }) .catch(error => { // add an empty stream with an error to break the stream: stitchableStream.addStream( new ReadableStream({ start(controller) { controller.enqueue({ type: 'error', error }); controller.close(); }, }), ); }) .finally(() => { stitchableStream.close(); }); this.outputStrategy = outputStrategy; } get object() { return this._object.promise; } get usage() { return this._usage.promise; } get providerMetadata() { return this._providerMetadata.promise; } get warnings() { return this._warnings.promise; } get request() { return this._request.promise; } get response() { return this._response.promise; } get finishReason() { return this._finishReason.promise; } get partialObjectStream(): AsyncIterableStream<PARTIAL> { return createAsyncIterableStream( this.baseStream.pipeThrough( new TransformStream<ObjectStreamPart<PARTIAL>, PARTIAL>({ transform(chunk, controller) { switch (chunk.type) { case 'object': controller.enqueue(chunk.object); break; case 'text-delta': case 'finish': case 'error': // suppress error (use onError instead) break; default: { const _exhaustiveCheck: never = chunk; throw new Error(`Unsupported chunk type: ${_exhaustiveCheck}`); } } }, }), ), ); } get elementStream(): ELEMENT_STREAM { return this.outputStrategy.createElementStream(this.baseStream); } get textStream(): AsyncIterableStream<string> { return createAsyncIterableStream( this.baseStream.pipeThrough( new TransformStream<ObjectStreamPart<PARTIAL>, string>({ transform(chunk, controller) { switch (chunk.type) { case 'text-delta': controller.enqueue(chunk.textDelta); break; case 'object': case 'finish': case 'error': // suppress error (use onError instead) break; default: { const _exhaustiveCheck: never = chunk; throw new Error(`Unsupported chunk type: ${_exhaustiveCheck}`); } } }, }), ), ); } get fullStream(): AsyncIterableStream<ObjectStreamPart<PARTIAL>> { return createAsyncIterableStream(this.baseStream); } pipeTextStreamToResponse(response: ServerResponse, init?: ResponseInit) { pipeTextStreamToResponse({ response, textStream: this.textStream, ...init, }); } toTextStreamResponse(init?: ResponseInit): Response { return createTextStreamResponse({ textStream: this.textStream, ...init, }); } } export type ObjectStreamInputPart = | string | { type: 'stream-start'; warnings: LanguageModelV2CallWarning[]; } | { type: 'error'; error: unknown; } | { type: 'response-metadata'; id?: string; timestamp?: Date; modelId?: string; } | { type: 'finish'; finishReason: LanguageModelV2FinishReason; usage: LanguageModelV2Usage; providerMetadata?: SharedV2ProviderMetadata; }; --- File: /ai/packages/ai/src/generate-object/validate-object-generation-input.ts --- import { Schema } from '@ai-sdk/provider-utils'; import * as z3 from 'zod/v3'; import * as z4 from 'zod/v4'; import { InvalidArgumentError } from '../error/invalid-argument-error'; export function validateObjectGenerationInput({ output, schema, schemaName, schemaDescription, enumValues, }: { output?: 'object' | 'array' | 'enum' | 'no-schema'; schema?: z4.core.$ZodType | z3.Schema<any, z3.ZodTypeDef, any> | Schema<any>; schemaName?: string; schemaDescription?: string; enumValues?: Array<unknown>; }) { if ( output != null && output !== 'object' && output !== 'array' && output !== 'enum' && output !== 'no-schema' ) { throw new InvalidArgumentError({ parameter: 'output', value: output, message: 'Invalid output type.', }); } if (output === 'no-schema') { if (schema != null) { throw new InvalidArgumentError({ parameter: 'schema', value: schema, message: 'Schema is not supported for no-schema output.', }); } if (schemaDescription != null) { throw new InvalidArgumentError({ parameter: 'schemaDescription', value: schemaDescription, message: 'Schema description is not supported for no-schema output.', }); } if (schemaName != null) { throw new InvalidArgumentError({ parameter: 'schemaName', value: schemaName, message: 'Schema name is not supported for no-schema output.', }); } if (enumValues != null) { throw new InvalidArgumentError({ parameter: 'enumValues', value: enumValues, message: 'Enum values are not supported for no-schema output.', }); } } if (output === 'object') { if (schema == null) { throw new InvalidArgumentError({ parameter: 'schema', value: schema, message: 'Schema is required for object output.', }); } if (enumValues != null) { throw new InvalidArgumentError({ parameter: 'enumValues', value: enumValues, message: 'Enum values are not supported for object output.', }); } } if (output === 'array') { if (schema == null) { throw new InvalidArgumentError({ parameter: 'schema', value: schema, message: 'Element schema is required for array output.', }); } if (enumValues != null) { throw new InvalidArgumentError({ parameter: 'enumValues', value: enumValues, message: 'Enum values are not supported for array output.', }); } } if (output === 'enum') { if (schema != null) { throw new InvalidArgumentError({ parameter: 'schema', value: schema, message: 'Schema is not supported for enum output.', }); } if (schemaDescription != null) { throw new InvalidArgumentError({ parameter: 'schemaDescription', value: schemaDescription, message: 'Schema description is not supported for enum output.', }); } if (schemaName != null) { throw new InvalidArgumentError({ parameter: 'schemaName', value: schemaName, message: 'Schema name is not supported for enum output.', }); } if (enumValues == null) { throw new InvalidArgumentError({ parameter: 'enumValues', value: enumValues, message: 'Enum values are required for enum output.', }); } for (const value of enumValues) { if (typeof value !== 'string') { throw new InvalidArgumentError({ parameter: 'enumValues', value, message: 'Enum values must be strings.', }); } } } } --- File: /ai/packages/ai/src/generate-speech/generate-speech-result.ts --- import { JSONValue } from '@ai-sdk/provider'; import { SpeechModelResponseMetadata } from '../types/speech-model-response-metadata'; import { SpeechWarning } from '../types'; import { GeneratedAudioFile } from './generated-audio-file'; /** The result of a `generateSpeech` call. It contains the audio data and additional information. */ export interface SpeechResult { /** * The audio data as a base64 encoded string or binary data. */ readonly audio: GeneratedAudioFile; /** Warnings for the call, e.g. unsupported settings. */ readonly warnings: Array<SpeechWarning>; /** Response metadata from the provider. There may be multiple responses if we made multiple calls to the model. */ readonly responses: Array<SpeechModelResponseMetadata>; /** Provider metadata from the provider. */ readonly providerMetadata: Record<string, Record<string, JSONValue>>; } --- File: /ai/packages/ai/src/generate-speech/generate-speech.test.ts --- import { JSONValue, SpeechModelV2, SpeechModelV2CallWarning, } from '@ai-sdk/provider'; import { MockSpeechModelV2 } from '../test/mock-speech-model-v2'; import { generateSpeech } from './generate-speech'; import { GeneratedAudioFile, DefaultGeneratedAudioFile, } from './generated-audio-file'; const audio = new Uint8Array([1, 2, 3, 4]); // Sample audio data const testDate = new Date(2024, 0, 1); const mockFile = new DefaultGeneratedAudioFile({ data: audio, mediaType: 'audio/mp3', }); const sampleText = 'This is a sample text to convert to speech.'; const createMockResponse = (options: { audio: GeneratedAudioFile; warnings?: SpeechModelV2CallWarning[]; timestamp?: Date; modelId?: string; headers?: Record<string, string>; providerMetadata?: Record<string, Record<string, JSONValue>>; }) => ({ audio: options.audio.uint8Array, warnings: options.warnings ?? [], response: { timestamp: options.timestamp ?? new Date(), modelId: options.modelId ?? 'test-model-id', headers: options.headers ?? {}, }, providerMetadata: options.providerMetadata ?? {}, }); describe('generateSpeech', () => { it('should send args to doGenerate', async () => { const abortController = new AbortController(); const abortSignal = abortController.signal; let capturedArgs!: Parameters<SpeechModelV2['doGenerate']>[0]; await generateSpeech({ model: new MockSpeechModelV2({ doGenerate: async args => { capturedArgs = args; return createMockResponse({ audio: mockFile, }); }, }), text: sampleText, voice: 'test-voice', headers: { 'custom-request-header': 'request-header-value' }, abortSignal, }); expect(capturedArgs).toStrictEqual({ text: sampleText, voice: 'test-voice', headers: { 'custom-request-header': 'request-header-value' }, abortSignal, providerOptions: {}, outputFormat: undefined, instructions: undefined, speed: undefined, language: undefined, }); }); it('should return warnings', async () => { const result = await generateSpeech({ model: new MockSpeechModelV2({ doGenerate: async () => createMockResponse({ audio: mockFile, warnings: [ { type: 'other', message: 'Setting is not supported', }, ], providerMetadata: { 'test-provider': { 'test-key': 'test-value', }, }, }), }), text: sampleText, }); expect(result.warnings).toStrictEqual([ { type: 'other', message: 'Setting is not supported', }, ]); }); it('should return the audio data', async () => { const result = await generateSpeech({ model: new MockSpeechModelV2({ doGenerate: async () => createMockResponse({ audio: mockFile, }), }), text: sampleText, }); expect(result).toEqual({ audio: mockFile, warnings: [], responses: [ { timestamp: expect.any(Date), modelId: 'test-model-id', headers: {}, }, ], providerMetadata: {}, }); }); describe('error handling', () => { it('should throw NoSpeechGeneratedError when no audio is returned', async () => { await expect( generateSpeech({ model: new MockSpeechModelV2({ doGenerate: async () => createMockResponse({ audio: new DefaultGeneratedAudioFile({ data: new Uint8Array(), mediaType: 'audio/mp3', }), timestamp: testDate, }), }), text: sampleText, }), ).rejects.toMatchObject({ name: 'AI_NoSpeechGeneratedError', message: 'No speech audio generated.', responses: [ { timestamp: testDate, modelId: expect.any(String), }, ], }); }); it('should include response headers in error when no audio generated', async () => { await expect( generateSpeech({ model: new MockSpeechModelV2({ doGenerate: async () => createMockResponse({ audio: new DefaultGeneratedAudioFile({ data: new Uint8Array(), mediaType: 'audio/mp3', }), timestamp: testDate, headers: { 'custom-response-header': 'response-header-value', }, }), }), text: sampleText, }), ).rejects.toMatchObject({ name: 'AI_NoSpeechGeneratedError', message: 'No speech audio generated.', responses: [ { timestamp: testDate, modelId: expect.any(String), headers: { 'custom-response-header': 'response-header-value', }, }, ], }); }); }); it('should return response metadata', async () => { const testHeaders = { 'x-test': 'value' }; const result = await generateSpeech({ model: new MockSpeechModelV2({ doGenerate: async () => createMockResponse({ audio: mockFile, timestamp: testDate, modelId: 'test-model', headers: testHeaders, }), }), text: sampleText, }); expect(result.responses).toStrictEqual([ { timestamp: testDate, modelId: 'test-model', headers: testHeaders, }, ]); }); }); --- File: /ai/packages/ai/src/generate-speech/generate-speech.ts --- import { JSONValue, SpeechModelV2 } from '@ai-sdk/provider'; import { ProviderOptions } from '@ai-sdk/provider-utils'; import { NoSpeechGeneratedError } from '../error/no-speech-generated-error'; import { audioMediaTypeSignatures, detectMediaType, } from '../util/detect-media-type'; import { prepareRetries } from '../util/prepare-retries'; import { UnsupportedModelVersionError } from '../error/unsupported-model-version-error'; import { SpeechWarning } from '../types/speech-model'; import { SpeechModelResponseMetadata } from '../types/speech-model-response-metadata'; import { SpeechResult } from './generate-speech-result'; import { DefaultGeneratedAudioFile, GeneratedAudioFile, } from './generated-audio-file'; /** Generates speech audio using a speech model. @param model - The speech model to use. @param text - The text to convert to speech. @param voice - The voice to use for speech generation. @param outputFormat - The output format to use for speech generation e.g. "mp3", "wav", etc. @param instructions - Instructions for the speech generation e.g. "Speak in a slow and steady tone". @param speed - The speed of the speech generation. @param providerOptions - Additional provider-specific options that are passed through to the provider as body parameters. @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2. @param abortSignal - An optional abort signal that can be used to cancel the call. @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. @returns A result object that contains the generated audio data. */ export async function generateSpeech({ model, text, voice, outputFormat, instructions, speed, language, providerOptions = {}, maxRetries: maxRetriesArg, abortSignal, headers, }: { /** The speech model to use. */ model: SpeechModelV2; /** The text to convert to speech. */ text: string; /** The voice to use for speech generation. */ voice?: string; /** * The desired output format for the audio e.g. "mp3", "wav", etc. */ outputFormat?: 'mp3' | 'wav' | (string & {}); /** Instructions for the speech generation e.g. "Speak in a slow and steady tone". */ instructions?: string; /** The speed of the speech generation. */ speed?: number; /** The language for speech generation. This should be an ISO 639-1 language code (e.g. "en", "es", "fr") or "auto" for automatic language detection. Provider support varies. */ language?: string; /** Additional provider-specific options that are passed through to the provider as body parameters. The outer record is keyed by the provider name, and the inner record is keyed by the provider-specific metadata key. ```ts { "openai": {} } ``` */ providerOptions?: ProviderOptions; /** Maximum number of retries per speech model call. Set to 0 to disable retries. @default 2 */ maxRetries?: number; /** Abort signal. */ abortSignal?: AbortSignal; /** Additional headers to include in the request. Only applicable for HTTP-based providers. */ headers?: Record<string, string>; }): Promise<SpeechResult> { if (model.specificationVersion !== 'v2') { throw new UnsupportedModelVersionError({ version: model.specificationVersion, provider: model.provider, modelId: model.modelId, }); } const { retry } = prepareRetries({ maxRetries: maxRetriesArg, abortSignal, }); const result = await retry(() => model.doGenerate({ text, voice, outputFormat, instructions, speed, language, abortSignal, headers, providerOptions, }), ); if (!result.audio || result.audio.length === 0) { throw new NoSpeechGeneratedError({ responses: [result.response] }); } return new DefaultSpeechResult({ audio: new DefaultGeneratedAudioFile({ data: result.audio, mediaType: detectMediaType({ data: result.audio, signatures: audioMediaTypeSignatures, }) ?? 'audio/mp3', }), warnings: result.warnings, responses: [result.response], providerMetadata: result.providerMetadata, }); } class DefaultSpeechResult implements SpeechResult { readonly audio: GeneratedAudioFile; readonly warnings: Array<SpeechWarning>; readonly responses: Array<SpeechModelResponseMetadata>; readonly providerMetadata: Record<string, Record<string, JSONValue>>; constructor(options: { audio: GeneratedAudioFile; warnings: Array<SpeechWarning>; responses: Array<SpeechModelResponseMetadata>; providerMetadata: Record<string, Record<string, JSONValue>> | undefined; }) { this.audio = options.audio; this.warnings = options.warnings; this.responses = options.responses; this.providerMetadata = options.providerMetadata ?? {}; } } --- File: /ai/packages/ai/src/generate-speech/generated-audio-file.ts --- import { GeneratedFile, DefaultGeneratedFile, } from '../generate-text/generated-file'; /** * A generated audio file. */ export interface GeneratedAudioFile extends GeneratedFile { /** * Audio format of the file (e.g., 'mp3', 'wav', etc.) */ readonly format: string; } export class DefaultGeneratedAudioFile extends DefaultGeneratedFile implements GeneratedAudioFile { readonly format: string; constructor({ data, mediaType, }: { data: string | Uint8Array; mediaType: string; }) { super({ data, mediaType }); let format = 'mp3'; // If format is not provided, try to determine it from the media type if (mediaType) { const mediaTypeParts = mediaType.split('/'); if (mediaTypeParts.length === 2) { // Handle special cases for audio formats if (mediaType !== 'audio/mpeg') { format = mediaTypeParts[1]; } } } if (!format) { // TODO this should be an AI SDK error throw new Error( 'Audio format must be provided or determinable from media type', ); } this.format = format; } } export class DefaultGeneratedAudioFileWithType extends DefaultGeneratedAudioFile { readonly type = 'audio'; constructor(options: { data: string | Uint8Array; mediaType: string; format: string; }) { super(options); } } --- File: /ai/packages/ai/src/generate-speech/index.ts --- export { generateSpeech as experimental_generateSpeech } from './generate-speech'; export type { SpeechResult as Experimental_SpeechResult } from './generate-speech-result'; export type { GeneratedAudioFile } from './generated-audio-file'; --- File: /ai/packages/ai/src/generate-text/content-part.ts --- import { ProviderMetadata } from '../types'; import { Source } from '../types/language-model'; import { GeneratedFile } from './generated-file'; import { TypedToolCall } from './tool-call'; import { TypedToolError } from './tool-error'; import { TypedToolResult } from './tool-result'; import { ToolSet } from './tool-set'; export type ContentPart<TOOLS extends ToolSet> = | { type: 'text'; text: string; providerMetadata?: ProviderMetadata } | { type: 'reasoning'; text: string; providerMetadata?: ProviderMetadata } | ({ type: 'source' } & Source) | { type: 'file'; file: GeneratedFile; providerMetadata?: ProviderMetadata } // different because of GeneratedFile object | ({ type: 'tool-call' } & TypedToolCall<TOOLS> & { providerMetadata?: ProviderMetadata; }) | ({ type: 'tool-result' } & TypedToolResult<TOOLS> & { providerMetadata?: ProviderMetadata; }) | ({ type: 'tool-error' } & TypedToolError<TOOLS> & { providerMetadata?: ProviderMetadata; }); --- File: /ai/packages/ai/src/generate-text/extract-content-text.ts --- import { LanguageModelV2Content, LanguageModelV2Text } from '@ai-sdk/provider'; export function extractContentText( content: LanguageModelV2Content[], ): string | undefined { const parts = content.filter( (content): content is LanguageModelV2Text => content.type === 'text', ); if (parts.length === 0) { return undefined; } return parts.map(content => content.text).join(''); } --- File: /ai/packages/ai/src/generate-text/generate-text-result.ts --- import { ReasoningPart } from '@ai-sdk/provider-utils'; import { CallWarning, FinishReason, ProviderMetadata } from '../types'; import { Source } from '../types/language-model'; import { LanguageModelRequestMetadata } from '../types/language-model-request-metadata'; import { LanguageModelResponseMetadata } from '../types/language-model-response-metadata'; import { LanguageModelUsage } from '../types/usage'; import { ContentPart } from './content-part'; import { GeneratedFile } from './generated-file'; import { ResponseMessage } from './response-message'; import { StepResult } from './step-result'; import { DynamicToolCall, StaticToolCall, TypedToolCall } from './tool-call'; import { DynamicToolResult, StaticToolResult, TypedToolResult, } from './tool-result'; import { ToolSet } from './tool-set'; /** The result of a `generateText` call. It contains the generated text, the tool calls that were made during the generation, and the results of the tool calls. */ export interface GenerateTextResult<TOOLS extends ToolSet, OUTPUT> { /** The content that was generated in the last step. */ readonly content: Array<ContentPart<TOOLS>>; /** The text that was generated in the last step. */ readonly text: string; /** The full reasoning that the model has generated in the last step. */ readonly reasoning: Array<ReasoningPart>; /** The reasoning text that the model has generated in the last step. Can be undefined if the model has only generated text. */ readonly reasoningText: string | undefined; /** The files that were generated in the last step. Empty array if no files were generated. */ readonly files: Array<GeneratedFile>; /** Sources that have been used as references in the last step. */ readonly sources: Array<Source>; /** The tool calls that were made in the last step. */ readonly toolCalls: Array<TypedToolCall<TOOLS>>; /** The static tool calls that were made in the last step. */ readonly staticToolCalls: Array<StaticToolCall<TOOLS>>; /** The dynamic tool calls that were made in the last step. */ readonly dynamicToolCalls: Array<DynamicToolCall>; /** The results of the tool calls from the last step. */ readonly toolResults: Array<TypedToolResult<TOOLS>>; /** The static tool results that were made in the last step. */ readonly staticToolResults: Array<StaticToolResult<TOOLS>>; /** The dynamic tool results that were made in the last step. */ readonly dynamicToolResults: Array<DynamicToolResult>; /** The reason why the generation finished. */ readonly finishReason: FinishReason; /** The token usage of the last step. */ readonly usage: LanguageModelUsage; /** The total token usage of all steps. When there are multiple steps, the usage is the sum of all step usages. */ readonly totalUsage: LanguageModelUsage; /** Warnings from the model provider (e.g. unsupported settings) */ readonly warnings: CallWarning[] | undefined; /** Additional request information. */ readonly request: LanguageModelRequestMetadata; /** Additional response information. */ readonly response: LanguageModelResponseMetadata & { /** The response messages that were generated during the call. It consists of an assistant message, potentially containing tool calls. When there are tool results, there is an additional tool message with the tool results that are available. If there are tools that do not have execute functions, they are not included in the tool results and need to be added separately. */ messages: Array<ResponseMessage>; /** Response body (available only for providers that use HTTP requests). */ body?: unknown; }; /** Additional provider-specific metadata. They are passed through from the provider to the AI SDK and enable provider-specific results that can be fully encapsulated in the provider. */ readonly providerMetadata: ProviderMetadata | undefined; /** Details for all steps. You can use this to get information about intermediate steps, such as the tool calls or the response headers. */ readonly steps: Array<StepResult<TOOLS>>; /** The generated structured output. It uses the `experimental_output` specification. */ readonly experimental_output: OUTPUT; } --- File: /ai/packages/ai/src/generate-text/generate-text.test.ts --- import { LanguageModelV2CallOptions, LanguageModelV2FunctionTool, LanguageModelV2ProviderDefinedTool, } from '@ai-sdk/provider'; import { dynamicTool, jsonSchema, ModelMessage, tool, } from '@ai-sdk/provider-utils'; import { mockId } from '@ai-sdk/provider-utils/test'; import { z } from 'zod/v4'; import { Output } from '.'; import { MockLanguageModelV2 } from '../test/mock-language-model-v2'; import { MockTracer } from '../test/mock-tracer'; import { generateText } from './generate-text'; import { GenerateTextResult } from './generate-text-result'; import { StepResult } from './step-result'; import { stepCountIs } from './stop-condition'; const dummyResponseValues = { finishReason: 'stop' as const, usage: { inputTokens: 3, outputTokens: 10, totalTokens: 13, reasoningTokens: undefined, cachedInputTokens: undefined, }, warnings: [], }; const modelWithSources = new MockLanguageModelV2({ doGenerate: { ...dummyResponseValues, content: [ { type: 'text', text: 'Hello, world!' }, { type: 'source', sourceType: 'url', id: '123', url: 'https://example.com', title: 'Example', providerMetadata: { provider: { custom: 'value' } }, }, { type: 'source', sourceType: 'url', id: '456', url: 'https://example.com/2', title: 'Example 2', providerMetadata: { provider: { custom: 'value2' } }, }, ], }, }); const modelWithFiles = new MockLanguageModelV2({ doGenerate: { ...dummyResponseValues, content: [ { type: 'text', text: 'Hello, world!' }, { type: 'file', data: new Uint8Array([1, 2, 3]), mediaType: 'image/png', }, { type: 'file', data: 'QkFVRw==', mediaType: 'image/jpeg', }, ], }, }); const modelWithReasoning = new MockLanguageModelV2({ doGenerate: { ...dummyResponseValues, content: [ { type: 'reasoning', text: 'I will open the conversation with witty banter.', providerMetadata: { testProvider: { signature: 'signature', }, }, }, { type: 'reasoning', text: '', providerMetadata: { testProvider: { redactedData: 'redacted-reasoning-data', }, }, }, { type: 'text', text: 'Hello, world!' }, ], }, }); describe('generateText', () => { describe('result.content', () => { it('should generate content', async () => { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: { ...dummyResponseValues, content: [ { type: 'text', text: 'Hello, world!' }, { type: 'source', sourceType: 'url', id: '123', url: 'https://example.com', title: 'Example', providerMetadata: { provider: { custom: 'value' } }, }, { type: 'file', data: new Uint8Array([1, 2, 3]), mediaType: 'image/png', }, { type: 'reasoning', text: 'I will open the conversation with witty banter.', }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'text', text: 'More text' }, ], }, }), prompt: 'prompt', tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async args => { expect(args).toStrictEqual({ value: 'value' }); return 'result1'; }, }, }, }); expect(result.content).toMatchInlineSnapshot(` [ { "text": "Hello, world!", "type": "text", }, { "id": "123", "providerMetadata": { "provider": { "custom": "value", }, }, "sourceType": "url", "title": "Example", "type": "source", "url": "https://example.com", }, { "file": DefaultGeneratedFile { "base64Data": "AQID", "mediaType": "image/png", "uint8ArrayData": Uint8Array [ 1, 2, 3, ], }, "type": "file", }, { "text": "I will open the conversation with witty banter.", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "text": "More text", "type": "text", }, { "dynamic": false, "input": { "value": "value", }, "output": "result1", "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ] `); }); }); describe('result.text', () => { it('should generate text', async () => { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: { ...dummyResponseValues, content: [{ type: 'text', text: 'Hello, world!' }], }, }), prompt: 'prompt', }); expect(modelWithSources.doGenerateCalls).toMatchSnapshot(); expect(result.text).toStrictEqual('Hello, world!'); }); }); describe('result.reasoningText', () => { it('should contain reasoning string from model response', async () => { const result = await generateText({ model: modelWithReasoning, prompt: 'prompt', }); expect(result.reasoningText).toStrictEqual( 'I will open the conversation with witty banter.', ); }); }); describe('result.sources', () => { it('should contain sources', async () => { const result = await generateText({ model: modelWithSources, prompt: 'prompt', }); expect(result.sources).toMatchSnapshot(); }); }); describe('result.files', () => { it('should contain files', async () => { const result = await generateText({ model: modelWithFiles, prompt: 'prompt', }); expect(result.files).toMatchSnapshot(); }); }); describe('result.steps', () => { it('should add the reasoning from the model response to the step result', async () => { const result = await generateText({ model: modelWithReasoning, prompt: 'prompt', _internal: { generateId: mockId({ prefix: 'id' }), currentDate: () => new Date(0), }, }); expect(result.steps).toMatchSnapshot(); }); it('should contain sources', async () => { const result = await generateText({ model: modelWithSources, prompt: 'prompt', _internal: { generateId: mockId({ prefix: 'id' }), currentDate: () => new Date(0), }, }); expect(result.steps).toMatchSnapshot(); }); it('should contain files', async () => { const result = await generateText({ model: modelWithFiles, prompt: 'prompt', _internal: { generateId: mockId({ prefix: 'id' }), currentDate: () => new Date(0), }, }); expect(result.steps).toMatchSnapshot(); }); }); describe('result.toolCalls', () => { it('should contain tool calls', async () => { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async ({ prompt, tools, toolChoice }) => { expect(tools).toStrictEqual([ { type: 'function', name: 'tool1', description: undefined, inputSchema: { $schema: 'http://json-schema.org/draft-07/schema#', additionalProperties: false, properties: { value: { type: 'string' } }, required: ['value'], type: 'object', }, providerOptions: undefined, }, { type: 'function', name: 'tool2', description: undefined, inputSchema: { $schema: 'http://json-schema.org/draft-07/schema#', additionalProperties: false, properties: { somethingElse: { type: 'string' } }, required: ['somethingElse'], type: 'object', }, providerOptions: undefined, }, ]); expect(toolChoice).toStrictEqual({ type: 'required' }); expect(prompt).toStrictEqual([ { role: 'user', content: [{ type: 'text', text: 'test-input' }], providerOptions: undefined, }, ]); return { ...dummyResponseValues, content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, ], }; }, }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), }, // 2nd tool to show typing: tool2: { inputSchema: z.object({ somethingElse: z.string() }), }, }, toolChoice: 'required', prompt: 'test-input', }); // test type inference if ( result.toolCalls[0].toolName === 'tool1' && !result.toolCalls[0].dynamic ) { assertType<string>(result.toolCalls[0].input.value); } expect(result.toolCalls).toMatchInlineSnapshot(` [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ] `); }); }); describe('result.toolResults', () => { it('should contain tool results', async () => { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async ({ prompt, tools, toolChoice }) => { expect(tools).toStrictEqual([ { type: 'function', name: 'tool1', description: undefined, inputSchema: { $schema: 'http://json-schema.org/draft-07/schema#', additionalProperties: false, properties: { value: { type: 'string' } }, required: ['value'], type: 'object', }, providerOptions: undefined, }, ]); expect(toolChoice).toStrictEqual({ type: 'auto' }); expect(prompt).toStrictEqual([ { role: 'user', content: [{ type: 'text', text: 'test-input' }], providerOptions: undefined, }, ]); return { ...dummyResponseValues, content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, ], }; }, }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async args => { expect(args).toStrictEqual({ value: 'value' }); return 'result1'; }, }, }, prompt: 'test-input', }); // test type inference if ( result.toolResults[0].toolName === 'tool1' && !result.toolResults[0].dynamic ) { assertType<string>(result.toolResults[0].output); } expect(result.toolResults).toMatchInlineSnapshot(` [ { "dynamic": false, "input": { "value": "value", }, "output": "result1", "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ] `); }); }); describe('result.providerMetadata', () => { it('should contain provider metadata', async () => { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [], providerMetadata: { exampleProvider: { a: 10, b: 20, }, }, }), }), prompt: 'test-input', }); expect(result.providerMetadata).toStrictEqual({ exampleProvider: { a: 10, b: 20, }, }); }); }); describe('result.response.messages', () => { it('should contain assistant response message when there are no tool calls', async () => { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [{ type: 'text', text: 'Hello, world!' }], }), }), prompt: 'test-input', }); expect(result.response.messages).toMatchSnapshot(); }); it('should contain assistant response message and tool message when there are tool calls with results', async () => { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [ { type: 'text', text: 'Hello, world!' }, { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, ], }), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async (args, options) => { expect(args).toStrictEqual({ value: 'value' }); expect(options.messages).toStrictEqual([ { role: 'user', content: 'test-input' }, ]); return 'result1'; }, }, }, prompt: 'test-input', }); expect(result.response.messages).toMatchSnapshot(); }); it('should contain reasoning', async () => { const result = await generateText({ model: modelWithReasoning, prompt: 'test-input', }); expect(result.response.messages).toMatchSnapshot(); }); }); describe('result.request', () => { it('should contain request body', async () => { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async ({}) => ({ ...dummyResponseValues, content: [{ type: 'text', text: 'Hello, world!' }], request: { body: 'test body', }, }), }), prompt: 'prompt', }); expect(result.request).toStrictEqual({ body: 'test body', }); }); }); describe('result.response', () => { it('should contain response body and headers', async () => { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async ({}) => ({ ...dummyResponseValues, content: [{ type: 'text', text: 'Hello, world!' }], response: { id: 'test-id-from-model', timestamp: new Date(10000), modelId: 'test-response-model-id', headers: { 'custom-response-header': 'response-header-value', }, body: 'test body', }, }), }), prompt: 'prompt', }); expect(result.steps[0].response).toMatchSnapshot(); expect(result.response).toMatchSnapshot(); }); }); describe('options.stopWhen', () => { describe('2 steps: initial, tool-result', () => { let result: GenerateTextResult<any, any>; let onStepFinishResults: StepResult<any>[]; beforeEach(async () => { onStepFinishResults = []; let responseCount = 0; result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async ({ prompt, tools, toolChoice }) => { switch (responseCount++) { case 0: expect(tools).toStrictEqual([ { type: 'function', name: 'tool1', description: undefined, inputSchema: { $schema: 'http://json-schema.org/draft-07/schema#', additionalProperties: false, properties: { value: { type: 'string' } }, required: ['value'], type: 'object', }, providerOptions: undefined, }, ]); expect(toolChoice).toStrictEqual({ type: 'auto' }); expect(prompt).toStrictEqual([ { role: 'user', content: [{ type: 'text', text: 'test-input' }], providerOptions: undefined, }, ]); return { ...dummyResponseValues, content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, ], finishReason: 'tool-calls', usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15, reasoningTokens: undefined, cachedInputTokens: undefined, }, response: { id: 'test-id-1-from-model', timestamp: new Date(0), modelId: 'test-response-model-id', }, }; case 1: return { ...dummyResponseValues, content: [{ type: 'text', text: 'Hello, world!' }], response: { id: 'test-id-2-from-model', timestamp: new Date(10000), modelId: 'test-response-model-id', headers: { 'custom-response-header': 'response-header-value', }, }, }; default: throw new Error( `Unexpected response count: ${responseCount}`, ); } }, }), tools: { tool1: tool({ inputSchema: z.object({ value: z.string() }), execute: async (args, options) => { expect(args).toStrictEqual({ value: 'value' }); expect(options.messages).toStrictEqual([ { role: 'user', content: 'test-input' }, ]); return 'result1'; }, }), }, prompt: 'test-input', stopWhen: stepCountIs(3), onStepFinish: async event => { onStepFinishResults.push(event); }, }); }); it('result.text should return text from last step', async () => { assert.deepStrictEqual(result.text, 'Hello, world!'); }); it('result.toolCalls should return empty tool calls from last step', async () => { assert.deepStrictEqual(result.toolCalls, []); }); it('result.toolResults should return empty tool results from last step', async () => { assert.deepStrictEqual(result.toolResults, []); }); it('result.response.messages should contain response messages from all steps', () => { expect(result.response.messages).toMatchSnapshot(); }); it('result.totalUsage should sum token usage', () => { expect(result.totalUsage).toMatchInlineSnapshot(` { "cachedInputTokens": undefined, "inputTokens": 13, "outputTokens": 15, "reasoningTokens": undefined, "totalTokens": 28, } `); }); it('result.usage should contain token usage from final step', async () => { expect(result.usage).toMatchInlineSnapshot(` { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, } `); }); it('result.steps should contain all steps', () => { expect(result.steps).toMatchSnapshot(); }); it('onStepFinish should be called for each step', () => { expect(onStepFinishResults).toMatchSnapshot(); }); }); describe('2 steps: initial, tool-result with prepareStep', () => { let result: GenerateTextResult<any, any>; let onStepFinishResults: StepResult<any>[]; let doGenerateCalls: Array<LanguageModelV2CallOptions>; let prepareStepCalls: Array<{ stepNumber: number; steps: Array<StepResult<any>>; messages: Array<ModelMessage>; }>; beforeEach(async () => { onStepFinishResults = []; doGenerateCalls = []; prepareStepCalls = []; let responseCount = 0; const trueModel = new MockLanguageModelV2({ doGenerate: async ({ prompt, tools, toolChoice }) => { doGenerateCalls.push({ prompt, tools, toolChoice }); switch (responseCount++) { case 0: return { ...dummyResponseValues, content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, ], toolResults: [ { toolCallId: 'call-1', toolName: 'tool1', input: { value: 'value' }, output: 'result1', }, ], finishReason: 'tool-calls', usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15, reasoningTokens: undefined, cachedInputTokens: undefined, }, response: { id: 'test-id-1-from-model', timestamp: new Date(0), modelId: 'test-response-model-id', }, }; case 1: return { ...dummyResponseValues, content: [{ type: 'text', text: 'Hello, world!' }], response: { id: 'test-id-2-from-model', timestamp: new Date(10000), modelId: 'test-response-model-id', headers: { 'custom-response-header': 'response-header-value', }, }, }; default: throw new Error(`Unexpected response count: ${responseCount}`); } }, }); result = await generateText({ model: modelWithFiles, tools: { tool1: tool({ inputSchema: z.object({ value: z.string() }), execute: async (args, options) => { expect(args).toStrictEqual({ value: 'value' }); expect(options.messages).toStrictEqual([ { role: 'user', content: 'test-input' }, ]); return 'result1'; }, }), }, prompt: 'test-input', stopWhen: stepCountIs(3), onStepFinish: async event => { onStepFinishResults.push(event); }, prepareStep: async ({ model, stepNumber, steps, messages }) => { prepareStepCalls.push({ stepNumber, steps, messages }); if (stepNumber === 0) { expect(steps).toStrictEqual([]); return { model: trueModel, toolChoice: { type: 'tool', toolName: 'tool1' as const, }, system: 'system-message-0', messages: [ { role: 'user', content: 'new input from prepareStep', }, ], }; } if (stepNumber === 1) { expect(steps.length).toStrictEqual(1); return { model: trueModel, activeTools: [], system: 'system-message-1', }; } }, }); }); it('should contain all prepareStep calls', async () => { expect(prepareStepCalls).toMatchInlineSnapshot(` [ { "messages": [ { "content": "test-input", "role": "user", }, ], "stepNumber": 0, "steps": [ DefaultStepResult { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "dynamic": false, "input": { "value": "value", }, "output": "result1", "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "tool-calls", "providerMetadata": undefined, "request": {}, "response": { "body": undefined, "headers": undefined, "id": "test-id-1-from-model", "messages": [ { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "test-response-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 10, "outputTokens": 5, "reasoningTokens": undefined, "totalTokens": 15, }, "warnings": [], }, DefaultStepResult { "content": [ { "text": "Hello, world!", "type": "text", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "body": undefined, "headers": { "custom-response-header": "response-header-value", }, "id": "test-id-2-from-model", "messages": [ { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, ], "modelId": "test-response-model-id", "timestamp": 1970-01-01T00:00:10.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, ], }, { "messages": [ { "content": "test-input", "role": "user", }, { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "stepNumber": 1, "steps": [ DefaultStepResult { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "dynamic": false, "input": { "value": "value", }, "output": "result1", "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "tool-calls", "providerMetadata": undefined, "request": {}, "response": { "body": undefined, "headers": undefined, "id": "test-id-1-from-model", "messages": [ { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "test-response-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 10, "outputTokens": 5, "reasoningTokens": undefined, "totalTokens": 15, }, "warnings": [], }, DefaultStepResult { "content": [ { "text": "Hello, world!", "type": "text", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "body": undefined, "headers": { "custom-response-header": "response-header-value", }, "id": "test-id-2-from-model", "messages": [ { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, ], "modelId": "test-response-model-id", "timestamp": 1970-01-01T00:00:10.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, ], }, ] `); }); it('doGenerate should be called with the correct arguments', () => { expect(doGenerateCalls).toMatchInlineSnapshot(` [ { "prompt": [ { "content": "system-message-0", "role": "system", }, { "content": [ { "text": "new input from prepareStep", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, ], "toolChoice": { "toolName": "tool1", "type": "tool", }, "tools": [ { "description": undefined, "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "value": { "type": "string", }, }, "required": [ "value", ], "type": "object", }, "name": "tool1", "providerOptions": undefined, "type": "function", }, ], }, { "prompt": [ { "content": "system-message-1", "role": "system", }, { "content": [ { "text": "test-input", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "providerOptions": undefined, "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "providerOptions": undefined, "role": "tool", }, ], "toolChoice": { "type": "auto", }, "tools": [], }, ] `); }); it('result.text should return text from last step', async () => { expect(result.text).toStrictEqual('Hello, world!'); }); it('result.toolCalls should return empty tool calls from last step', async () => { expect(result.toolCalls).toStrictEqual([]); }); it('result.toolResults should return empty tool results from last step', async () => { expect(result.toolResults).toStrictEqual([]); }); it('result.response.messages should contain response messages from all steps', () => { expect(result.response.messages).toMatchSnapshot(); }); it('result.totalUsage should sum token usage', () => { expect(result.totalUsage).toMatchInlineSnapshot(` { "cachedInputTokens": undefined, "inputTokens": 13, "outputTokens": 15, "reasoningTokens": undefined, "totalTokens": 28, } `); }); it('result.usage should contain token usage from final step', async () => { expect(result.usage).toMatchInlineSnapshot(` { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, } `); }); it('result.steps should contain all steps', () => { expect(result.steps).toMatchSnapshot(); }); it('onStepFinish should be called for each step', () => { expect(onStepFinishResults).toMatchSnapshot(); }); it('content should contain content from the last step', () => { expect(result.content).toMatchInlineSnapshot(` [ { "text": "Hello, world!", "type": "text", }, ] `); }); }); describe('2 stop conditions', () => { let result: GenerateTextResult<any, any>; let stopConditionCalls: Array<{ number: number; steps: StepResult<any>[]; }>; beforeEach(async () => { stopConditionCalls = []; let responseCount = 0; result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => { switch (responseCount++) { case 0: return { ...dummyResponseValues, content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, ], finishReason: 'tool-calls', usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15, reasoningTokens: undefined, cachedInputTokens: undefined, }, response: { id: 'test-id-1-from-model', timestamp: new Date(0), modelId: 'test-response-model-id', }, }; default: throw new Error( `Unexpected response count: ${responseCount}`, ); } }, }), tools: { tool1: tool({ inputSchema: z.object({ value: z.string() }), execute: async (input, options) => { expect(input).toStrictEqual({ value: 'value' }); expect(options.messages).toStrictEqual([ { role: 'user', content: 'test-input' }, ]); return 'result1'; }, }), }, prompt: 'test-input', stopWhen: [ ({ steps }) => { stopConditionCalls.push({ number: 0, steps }); return false; }, ({ steps }) => { stopConditionCalls.push({ number: 1, steps }); return true; }, ], }); }); it('result.steps should contain a single step', () => { expect(result.steps.length).toStrictEqual(1); }); it('stopConditionCalls should be called for each stop condition', () => { expect(stopConditionCalls).toMatchInlineSnapshot(` [ { "number": 0, "steps": [ DefaultStepResult { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "dynamic": false, "input": { "value": "value", }, "output": "result1", "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "tool-calls", "providerMetadata": undefined, "request": {}, "response": { "body": undefined, "headers": undefined, "id": "test-id-1-from-model", "messages": [ { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "test-response-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 10, "outputTokens": 5, "reasoningTokens": undefined, "totalTokens": 15, }, "warnings": [], }, ], }, { "number": 1, "steps": [ DefaultStepResult { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "dynamic": false, "input": { "value": "value", }, "output": "result1", "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "tool-calls", "providerMetadata": undefined, "request": {}, "response": { "body": undefined, "headers": undefined, "id": "test-id-1-from-model", "messages": [ { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "test-response-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 10, "outputTokens": 5, "reasoningTokens": undefined, "totalTokens": 15, }, "warnings": [], }, ], }, ] `); }); }); }); describe('options.headers', () => { it('should pass headers to model', async () => { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async ({ headers }) => { assert.deepStrictEqual(headers, { 'custom-request-header': 'request-header-value', }); return { ...dummyResponseValues, content: [{ type: 'text', text: 'Hello, world!' }], }; }, }), prompt: 'test-input', headers: { 'custom-request-header': 'request-header-value' }, }); assert.deepStrictEqual(result.text, 'Hello, world!'); }); }); describe('options.providerOptions', () => { it('should pass provider options to model', async () => { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async ({ providerOptions }) => { expect(providerOptions).toStrictEqual({ aProvider: { someKey: 'someValue' }, }); return { ...dummyResponseValues, content: [{ type: 'text', text: 'provider metadata test' }], }; }, }), prompt: 'test-input', providerOptions: { aProvider: { someKey: 'someValue' }, }, }); expect(result.text).toStrictEqual('provider metadata test'); }); }); describe('options.abortSignal', () => { it('should forward abort signal to tool execution', async () => { const abortController = new AbortController(); const toolExecuteMock = vi.fn().mockResolvedValue('tool result'); const generateTextPromise = generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, ], }), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: toolExecuteMock, }, }, prompt: 'test-input', abortSignal: abortController.signal, }); // Abort the operation abortController.abort(); await generateTextPromise; expect(toolExecuteMock).toHaveBeenCalledWith( { value: 'value' }, { abortSignal: abortController.signal, toolCallId: 'call-1', messages: expect.any(Array), }, ); }); }); describe('options.activeTools', () => { it('should filter available tools to only the ones in activeTools', async () => { let tools: | (LanguageModelV2FunctionTool | LanguageModelV2ProviderDefinedTool)[] | undefined; await generateText({ model: new MockLanguageModelV2({ doGenerate: async ({ tools: toolsArg }) => { tools = toolsArg; return { ...dummyResponseValues, content: [{ type: 'text', text: 'Hello, world!' }], }; }, }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async () => 'result1', }, tool2: { inputSchema: z.object({ value: z.string() }), execute: async () => 'result2', }, }, prompt: 'test-input', activeTools: ['tool1'], }); expect(tools).toMatchInlineSnapshot(` [ { "description": undefined, "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "value": { "type": "string", }, }, "required": [ "value", ], "type": "object", }, "name": "tool1", "providerOptions": undefined, "type": "function", }, ] `); }); }); describe('telemetry', () => { let tracer: MockTracer; beforeEach(() => { tracer = new MockTracer(); }); it('should not record any telemetry data when not explicitly enabled', async () => { await generateText({ model: new MockLanguageModelV2({ doGenerate: async ({}) => ({ ...dummyResponseValues, content: [{ type: 'text', text: 'Hello, world!' }], }), }), prompt: 'prompt', experimental_telemetry: { tracer }, }); expect(tracer.jsonSpans).toMatchSnapshot(); }); it('should record telemetry data when enabled', async () => { await generateText({ model: new MockLanguageModelV2({ doGenerate: async ({}) => ({ ...dummyResponseValues, content: [{ type: 'text', text: 'Hello, world!' }], response: { id: 'test-id-from-model', timestamp: new Date(10000), modelId: 'test-response-model-id', }, providerMetadata: { testProvider: { testKey: 'testValue', }, }, }), }), prompt: 'prompt', topK: 0.1, topP: 0.2, frequencyPenalty: 0.3, presencePenalty: 0.4, temperature: 0.5, stopSequences: ['stop'], headers: { header1: 'value1', header2: 'value2', }, experimental_telemetry: { isEnabled: true, functionId: 'test-function-id', metadata: { test1: 'value1', test2: false, }, tracer, }, }); expect(tracer.jsonSpans).toMatchSnapshot(); }); it('should record successful tool call', async () => { await generateText({ model: new MockLanguageModelV2({ doGenerate: async ({}) => ({ ...dummyResponseValues, content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, ], }), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async () => 'result1', }, }, prompt: 'test-input', experimental_telemetry: { isEnabled: true, tracer, }, _internal: { generateId: () => 'test-id', currentDate: () => new Date(0), }, }); expect(tracer.jsonSpans).toMatchInlineSnapshot(` [ { "attributes": { "ai.model.id": "mock-model-id", "ai.model.provider": "mock-provider", "ai.operationId": "ai.generateText", "ai.prompt": "{"prompt":"test-input"}", "ai.response.finishReason": "stop", "ai.response.toolCalls": "[{"toolCallId":"call-1","toolName":"tool1","input":"{ \\"value\\": \\"value\\" }"}]", "ai.settings.maxRetries": 2, "ai.usage.completionTokens": 10, "ai.usage.promptTokens": 3, "operation.name": "ai.generateText", }, "events": [], "name": "ai.generateText", }, { "attributes": { "ai.model.id": "mock-model-id", "ai.model.provider": "mock-provider", "ai.operationId": "ai.generateText.doGenerate", "ai.prompt.messages": "[{"role":"user","content":[{"type":"text","text":"test-input"}]}]", "ai.prompt.toolChoice": "{"type":"auto"}", "ai.prompt.tools": [ "{"type":"function","name":"tool1","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"value":{"type":"string"}},"required":["value"],"additionalProperties":false}}", ], "ai.response.finishReason": "stop", "ai.response.id": "test-id", "ai.response.model": "mock-model-id", "ai.response.timestamp": "1970-01-01T00:00:00.000Z", "ai.response.toolCalls": "[{"toolCallId":"call-1","toolName":"tool1","input":"{ \\"value\\": \\"value\\" }"}]", "ai.settings.maxRetries": 2, "ai.usage.completionTokens": 10, "ai.usage.promptTokens": 3, "gen_ai.request.model": "mock-model-id", "gen_ai.response.finish_reasons": [ "stop", ], "gen_ai.response.id": "test-id", "gen_ai.response.model": "mock-model-id", "gen_ai.system": "mock-provider", "gen_ai.usage.input_tokens": 3, "gen_ai.usage.output_tokens": 10, "operation.name": "ai.generateText.doGenerate", }, "events": [], "name": "ai.generateText.doGenerate", }, { "attributes": { "ai.operationId": "ai.toolCall", "ai.toolCall.args": "{"value":"value"}", "ai.toolCall.id": "call-1", "ai.toolCall.name": "tool1", "ai.toolCall.result": ""result1"", "operation.name": "ai.toolCall", }, "events": [], "name": "ai.toolCall", }, ] `); }); it('should record error on tool call', async () => { await generateText({ model: new MockLanguageModelV2({ doGenerate: async ({}) => ({ ...dummyResponseValues, content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, ], }), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async () => { throw new Error('Tool execution failed'); }, }, }, prompt: 'test-input', experimental_telemetry: { isEnabled: true, tracer, }, _internal: { generateId: () => 'test-id', currentDate: () => new Date(0), }, }); expect(tracer.jsonSpans).toHaveLength(3); // Check that we have the expected spans expect(tracer.jsonSpans[0].name).toBe('ai.generateText'); expect(tracer.jsonSpans[1].name).toBe('ai.generateText.doGenerate'); expect(tracer.jsonSpans[2].name).toBe('ai.toolCall'); // Check that the tool call span has error status const toolCallSpan = tracer.jsonSpans[2]; expect(toolCallSpan.status).toEqual({ code: 2, message: 'Tool execution failed', }); expect(toolCallSpan.events).toHaveLength(1); const exceptionEvent = toolCallSpan.events[0]; expect(exceptionEvent.name).toBe('exception'); expect(exceptionEvent.attributes).toMatchObject({ 'exception.message': 'Tool execution failed', 'exception.name': 'Error', }); expect(exceptionEvent.attributes?.['exception.stack']).toContain( 'Tool execution failed', ); expect(exceptionEvent.time).toEqual([0, 0]); }); it('should not record telemetry inputs / outputs when disabled', async () => { await generateText({ model: new MockLanguageModelV2({ doGenerate: async ({}) => ({ ...dummyResponseValues, content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, ], }), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async () => 'result1', }, }, prompt: 'test-input', experimental_telemetry: { isEnabled: true, recordInputs: false, recordOutputs: false, tracer, }, _internal: { generateId: () => 'test-id', currentDate: () => new Date(0), }, }); expect(tracer.jsonSpans).toMatchSnapshot(); }); }); describe('tool callbacks', () => { it('should invoke callbacks in the correct order', async () => { const recordedCalls: unknown[] = []; await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => { return { ...dummyResponseValues, content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'test-tool', input: `{ "value": "value" }`, }, ], }; }, }), tools: { 'test-tool': tool({ inputSchema: jsonSchema<{ value: string }>({ type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, }), onInputAvailable: options => { recordedCalls.push({ type: 'onInputAvailable', options }); }, onInputStart: options => { recordedCalls.push({ type: 'onInputStart', options }); }, onInputDelta: options => { recordedCalls.push({ type: 'onInputDelta', options }); }, }), }, toolChoice: 'required', prompt: 'test-input', }); expect(recordedCalls).toMatchInlineSnapshot(` [ { "options": { "abortSignal": undefined, "experimental_context": undefined, "input": { "value": "value", }, "messages": [ { "content": "test-input", "role": "user", }, ], "toolCallId": "call-1", }, "type": "onInputAvailable", }, ] `); }); }); describe('tools with custom schema', () => { it('should contain tool calls', async () => { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async ({ prompt, tools, toolChoice }) => { expect(tools).toStrictEqual([ { type: 'function', name: 'tool1', description: undefined, inputSchema: { additionalProperties: false, properties: { value: { type: 'string' } }, required: ['value'], type: 'object', }, providerOptions: undefined, }, { type: 'function', name: 'tool2', description: undefined, inputSchema: { additionalProperties: false, properties: { somethingElse: { type: 'string' } }, required: ['somethingElse'], type: 'object', }, providerOptions: undefined, }, ]); expect(toolChoice).toStrictEqual({ type: 'required' }); expect(prompt).toStrictEqual([ { role: 'user', content: [{ type: 'text', text: 'test-input' }], providerOptions: undefined, }, ]); return { ...dummyResponseValues, content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, ], }; }, }), tools: { tool1: { inputSchema: jsonSchema<{ value: string }>({ type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, }), }, // 2nd tool to show typing: tool2: { inputSchema: jsonSchema<{ somethingElse: string }>({ type: 'object', properties: { somethingElse: { type: 'string' } }, required: ['somethingElse'], additionalProperties: false, }), }, }, toolChoice: 'required', prompt: 'test-input', _internal: { generateId: () => 'test-id', currentDate: () => new Date(0), }, }); // test type inference if ( result.toolCalls[0].toolName === 'tool1' && !result.toolCalls[0].dynamic ) { assertType<string>(result.toolCalls[0].input.value); } expect(result.toolCalls).toMatchInlineSnapshot(` [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ] `); }); }); describe('provider-executed tools', () => { describe('single provider-executed tool call and result', () => { let result: GenerateTextResult<any, any>; beforeEach(async () => { result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [ { type: 'tool-call', toolCallId: 'call-1', toolName: 'web_search', input: `{ "value": "value" }`, providerExecuted: true, }, { type: 'tool-result', toolCallId: 'call-1', toolName: 'web_search', result: `{ "value": "result1" }`, }, { type: 'tool-call', toolCallId: 'call-2', toolName: 'web_search', input: `{ "value": "value" }`, providerExecuted: true, }, { type: 'tool-result', toolCallId: 'call-2', toolName: 'web_search', result: 'ERROR', isError: true, providerExecuted: true, }, ], }), }), tools: { web_search: { type: 'provider-defined', id: 'test.web_search', name: 'web_search', inputSchema: z.object({ value: z.string() }), outputSchema: z.object({ value: z.string() }), args: {}, }, }, prompt: 'test-input', stopWhen: stepCountIs(4), }); }); it('should include provider-executed tool calls and results in the content', async () => { expect(result.content).toMatchInlineSnapshot(` [ { "input": { "value": "value", }, "providerExecuted": true, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "web_search", "type": "tool-call", }, { "dynamic": undefined, "input": { "value": "value", }, "output": "{ "value": "result1" }", "providerExecuted": true, "toolCallId": "call-1", "toolName": "web_search", "type": "tool-result", }, { "input": { "value": "value", }, "providerExecuted": true, "providerMetadata": undefined, "toolCallId": "call-2", "toolName": "web_search", "type": "tool-call", }, { "dynamic": undefined, "error": "ERROR", "input": { "value": "value", }, "providerExecuted": true, "toolCallId": "call-2", "toolName": "web_search", "type": "tool-error", }, ] `); }); it('should only execute a single step', async () => { expect(result.steps.length).toBe(1); }); }); }); describe('options.messages', () => { it('should support models that use "this" context in supportedUrls', async () => { let supportedUrlsCalled = false; class MockLanguageModelWithImageSupport extends MockLanguageModelV2 { constructor() { super({ supportedUrls() { supportedUrlsCalled = true; // Reference 'this' to verify context return this.modelId === 'mock-model-id' ? ({ 'image/*': [/^https:\/\/.*$/] } as Record< string, RegExp[] >) : {}; }, doGenerate: async () => ({ ...dummyResponseValues, content: [{ type: 'text', text: 'Hello, world!' }], }), }); } } const model = new MockLanguageModelWithImageSupport(); const result = await generateText({ model, messages: [ { role: 'user', content: [{ type: 'image', image: 'https://example.com/test.jpg' }], }, ], }); expect(result.text).toStrictEqual('Hello, world!'); expect(supportedUrlsCalled).toBe(true); }); }); describe('options.output', () => { describe('no output', () => { it('should throw error when accessing output', async () => { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [{ type: 'text', text: `Hello, world!` }], }), }), prompt: 'prompt', }); expect(() => { result.experimental_output; }).toThrow('No output specified'); }); }); describe('text output', () => { it('should forward text as output', async () => { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [{ type: 'text', text: `Hello, world!` }], }), }), prompt: 'prompt', experimental_output: Output.text(), }); expect(result.experimental_output).toStrictEqual('Hello, world!'); }); it('should set responseFormat to text and not change the prompt', async () => { let callOptions: LanguageModelV2CallOptions; await generateText({ model: new MockLanguageModelV2({ doGenerate: async args => { callOptions = args; return { ...dummyResponseValues, content: [{ type: 'text', text: `Hello, world!` }], }; }, }), prompt: 'prompt', experimental_output: Output.text(), }); expect(callOptions!).toMatchInlineSnapshot(` { "abortSignal": undefined, "frequencyPenalty": undefined, "headers": undefined, "maxOutputTokens": undefined, "presencePenalty": undefined, "prompt": [ { "content": [ { "text": "prompt", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, ], "providerOptions": undefined, "responseFormat": { "type": "text", }, "seed": undefined, "stopSequences": undefined, "temperature": undefined, "toolChoice": undefined, "tools": undefined, "topK": undefined, "topP": undefined, } `); }); }); describe('object output', () => { it('should parse the output', async () => { const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [{ type: 'text', text: `{ "value": "test-value" }` }], }), }), prompt: 'prompt', experimental_output: Output.object({ schema: z.object({ value: z.string() }), }), }); expect(result.experimental_output).toEqual({ value: 'test-value' }); }); it('should set responseFormat to json and send schema as part of the responseFormat', async () => { let callOptions: LanguageModelV2CallOptions; await generateText({ model: new MockLanguageModelV2({ doGenerate: async args => { callOptions = args; return { ...dummyResponseValues, content: [{ type: 'text', text: `{ "value": "test-value" }` }], }; }, }), prompt: 'prompt', experimental_output: Output.object({ schema: z.object({ value: z.string() }), }), }); expect(callOptions!).toMatchInlineSnapshot(` { "abortSignal": undefined, "frequencyPenalty": undefined, "headers": undefined, "maxOutputTokens": undefined, "presencePenalty": undefined, "prompt": [ { "content": [ { "text": "prompt", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, ], "providerOptions": undefined, "responseFormat": { "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "value": { "type": "string", }, }, "required": [ "value", ], "type": "object", }, "type": "json", }, "seed": undefined, "stopSequences": undefined, "temperature": undefined, "toolChoice": undefined, "tools": undefined, "topK": undefined, "topP": undefined, } `); }); }); }); describe('tool execution errors', () => { let result: GenerateTextResult<any, any>; beforeEach(async () => { result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, ], }), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async () => { throw new Error('test error'); }, }, }, prompt: 'test-input', }); }); it('should add tool error part to the content', async () => { expect(result.content).toMatchInlineSnapshot(` [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "dynamic": false, "error": [Error: test error], "input": { "value": "value", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-error", }, ] `); }); it('should include error result in response messages', async () => { expect(result.response.messages).toMatchInlineSnapshot(` [ { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "error-text", "value": "test error", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ] `); }); }); describe('provider-executed tools', () => { it('should not call execute for provider-executed tool calls', async () => { let toolExecuted = false; const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'providerTool', input: `{ "value": "test" }`, providerExecuted: true, }, { type: 'tool-result', toolCallId: 'call-1', toolName: 'providerTool', providerExecuted: true, result: { example: 'example' }, }, ], finishReason: 'stop', }), }), tools: { providerTool: { inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => { toolExecuted = true; return `${value}-should-not-execute`; }, }, }, prompt: 'test-input', }); // tool should not be executed by client expect(toolExecuted).toBe(false); // tool call should still be included in content expect(result.content).toMatchInlineSnapshot(` [ { "input": { "value": "test", }, "providerExecuted": true, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "providerTool", "type": "tool-call", }, { "dynamic": undefined, "input": { "value": "test", }, "output": { "example": "example", }, "providerExecuted": true, "toolCallId": "call-1", "toolName": "providerTool", "type": "tool-result", }, ] `); // tool results should include the result from the provider expect(result.toolResults).toMatchInlineSnapshot(` [ { "dynamic": undefined, "input": { "value": "test", }, "output": { "example": "example", }, "providerExecuted": true, "toolCallId": "call-1", "toolName": "providerTool", "type": "tool-result", }, ] `); }); }); describe('dynamic tools', () => { it('should execute dynamic tools', async () => { let toolExecuted = false; const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'dynamicTool', input: `{ "value": "test" }`, }, ], finishReason: 'tool-calls', }), }), tools: { dynamicTool: dynamicTool({ inputSchema: z.object({ value: z.string() }), execute: async () => { toolExecuted = true; return { value: 'test-result' }; }, }), }, prompt: 'test-input', }); // tool should be executed by client expect(toolExecuted).toBe(true); // tool call should be included in content expect(result.content).toMatchInlineSnapshot(` [ { "dynamic": true, "input": { "value": "test", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "dynamicTool", "type": "tool-call", }, { "dynamic": true, "input": { "value": "test", }, "output": { "value": "test-result", }, "toolCallId": "call-1", "toolName": "dynamicTool", "type": "tool-result", }, ] `); }); }); describe('tool execution context', () => { it('should send context to tool execution', async () => { let recordedContext: unknown | undefined; const result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ ...dummyResponseValues, content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 't1', input: `{ "value": "test" }`, }, ], finishReason: 'tool-calls', }), }), tools: { t1: tool({ inputSchema: z.object({ value: z.string() }), execute: async ({ value }, { experimental_context }) => { recordedContext = experimental_context; return { value: 'test-result' }; }, }), }, experimental_context: { context: 'test', }, prompt: 'test-input', }); // tool should be executed by client expect(recordedContext).toStrictEqual({ context: 'test', }); }); }); describe('invalid tool calls', () => { describe('single invalid tool call', () => { let result: GenerateTextResult<any, any>; beforeEach(async () => { result = await generateText({ model: new MockLanguageModelV2({ doGenerate: async () => ({ warnings: [], usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30, }, finishReason: 'tool-calls', content: [ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'cityAttractions', // wrong tool call arguments (city vs cities): input: `{ "cities": "San Francisco" }`, }, ], }), }), tools: { cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, prompt: 'What are the tourist attractions in San Francisco?', }); }); it('should add tool error part to the content', async () => { expect(result.content).toMatchInlineSnapshot(` [ { "dynamic": true, "error": [AI_InvalidToolInputError: Invalid input for tool cityAttractions: Type validation failed: Value: {"cities":"San Francisco"}. Error message: [ { "expected": "string", "code": "invalid_type", "path": [ "city" ], "message": "Invalid input: expected string, received undefined" } ]], "input": "{ "cities": "San Francisco" }", "invalid": true, "toolCallId": "call-1", "toolName": "cityAttractions", "type": "tool-call", }, { "dynamic": true, "error": "Invalid input for tool cityAttractions: Type validation failed: Value: {"cities":"San Francisco"}. Error message: [ { "expected": "string", "code": "invalid_type", "path": [ "city" ], "message": "Invalid input: expected string, received undefined" } ]", "input": "{ "cities": "San Francisco" }", "toolCallId": "call-1", "toolName": "cityAttractions", "type": "tool-error", }, ] `); }); it('should include error result in response messages', async () => { expect(result.response.messages).toMatchInlineSnapshot(` [ { "content": [ { "input": "{ "cities": "San Francisco" }", "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "cityAttractions", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "error-text", "value": "Invalid input for tool cityAttractions: Type validation failed: Value: {"cities":"San Francisco"}. Error message: [ { "expected": "string", "code": "invalid_type", "path": [ "city" ], "message": "Invalid input: expected string, received undefined" } ]", }, "toolCallId": "call-1", "toolName": "cityAttractions", "type": "tool-result", }, ], "role": "tool", }, ] `); }); }); }); }); --- File: /ai/packages/ai/src/generate-text/generate-text.ts --- import { LanguageModelV2, LanguageModelV2Content, LanguageModelV2ToolCall, } from '@ai-sdk/provider'; import { createIdGenerator, getErrorMessage, IdGenerator, ProviderOptions, } from '@ai-sdk/provider-utils'; import { Tracer } from '@opentelemetry/api'; import { NoOutputSpecifiedError } from '../error/no-output-specified-error'; import { resolveLanguageModel } from '../model/resolve-model'; import { ModelMessage } from '../prompt'; import { CallSettings } from '../prompt/call-settings'; import { convertToLanguageModelPrompt } from '../prompt/convert-to-language-model-prompt'; import { prepareCallSettings } from '../prompt/prepare-call-settings'; import { prepareToolsAndToolChoice } from '../prompt/prepare-tools-and-tool-choice'; import { Prompt } from '../prompt/prompt'; import { standardizePrompt } from '../prompt/standardize-prompt'; import { wrapGatewayError } from '../prompt/wrap-gateway-error'; import { assembleOperationName } from '../telemetry/assemble-operation-name'; import { getBaseTelemetryAttributes } from '../telemetry/get-base-telemetry-attributes'; import { getTracer } from '../telemetry/get-tracer'; import { recordErrorOnSpan, recordSpan } from '../telemetry/record-span'; import { selectTelemetryAttributes } from '../telemetry/select-telemetry-attributes'; import { stringifyForTelemetry } from '../telemetry/stringify-for-telemetry'; import { TelemetrySettings } from '../telemetry/telemetry-settings'; import { LanguageModel, ToolChoice } from '../types'; import { addLanguageModelUsage, LanguageModelUsage } from '../types/usage'; import { asArray } from '../util/as-array'; import { prepareRetries } from '../util/prepare-retries'; import { ContentPart } from './content-part'; import { extractContentText } from './extract-content-text'; import { GenerateTextResult } from './generate-text-result'; import { DefaultGeneratedFile } from './generated-file'; import { Output } from './output'; import { parseToolCall } from './parse-tool-call'; import { PrepareStepFunction } from './prepare-step'; import { ResponseMessage } from './response-message'; import { DefaultStepResult, StepResult } from './step-result'; import { isStopConditionMet, stepCountIs, StopCondition, } from './stop-condition'; import { toResponseMessages } from './to-response-messages'; import { TypedToolCall } from './tool-call'; import { ToolCallRepairFunction } from './tool-call-repair-function'; import { TypedToolError } from './tool-error'; import { ToolOutput } from './tool-output'; import { TypedToolResult } from './tool-result'; import { ToolSet } from './tool-set'; const originalGenerateId = createIdGenerator({ prefix: 'aitxt', size: 24, }); /** Callback that is set using the `onStepFinish` option. @param stepResult - The result of the step. */ export type GenerateTextOnStepFinishCallback<TOOLS extends ToolSet> = ( stepResult: StepResult<TOOLS>, ) => Promise<void> | void; /** Generate a text and call tools for a given prompt using a language model. This function does not stream the output. If you want to stream the output, use `streamText` instead. @param model - The language model to use. @param tools - Tools that are accessible to and can be called by the model. The model needs to support calling tools. @param toolChoice - The tool choice strategy. Default: 'auto'. @param system - A system message that will be part of the prompt. @param prompt - A simple text prompt. You can either use `prompt` or `messages` but not both. @param messages - A list of messages. You can either use `prompt` or `messages` but not both. @param maxOutputTokens - Maximum number of tokens to generate. @param temperature - Temperature setting. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both. @param topP - Nucleus sampling. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both. @param topK - Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature. @param presencePenalty - Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The value is passed through to the provider. The range depends on the provider and model. @param frequencyPenalty - Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The value is passed through to the provider. The range depends on the provider and model. @param stopSequences - Stop sequences. If set, the model will stop generating text when one of the stop sequences is generated. @param seed - The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results. @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2. @param abortSignal - An optional abort signal that can be used to cancel the call. @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. @param experimental_generateMessageId - Generate a unique ID for each message. @param onStepFinish - Callback that is called when each step (LLM call) is finished, including intermediate steps. @returns A result object that contains the generated text, the results of the tool calls, and additional information. */ export async function generateText< TOOLS extends ToolSet, OUTPUT = never, OUTPUT_PARTIAL = never, >({ model: modelArg, tools, toolChoice, system, prompt, messages, maxRetries: maxRetriesArg, abortSignal, headers, stopWhen = stepCountIs(1), experimental_output: output, experimental_telemetry: telemetry, providerOptions, experimental_activeTools, activeTools = experimental_activeTools, experimental_prepareStep, prepareStep = experimental_prepareStep, experimental_repairToolCall: repairToolCall, experimental_context, _internal: { generateId = originalGenerateId, currentDate = () => new Date(), } = {}, onStepFinish, ...settings }: CallSettings & Prompt & { /** The language model to use. */ model: LanguageModel; /** The tools that the model can call. The model needs to support calling tools. */ tools?: TOOLS; /** The tool choice strategy. Default: 'auto'. */ toolChoice?: ToolChoice<NoInfer<TOOLS>>; /** Condition for stopping the generation when there are tool results in the last step. When the condition is an array, any of the conditions can be met to stop the generation. @default stepCountIs(1) */ stopWhen?: | StopCondition<NoInfer<TOOLS>> | Array<StopCondition<NoInfer<TOOLS>>>; /** Optional telemetry configuration (experimental). */ experimental_telemetry?: TelemetrySettings; /** Additional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; /** * @deprecated Use `activeTools` instead. */ experimental_activeTools?: Array<keyof NoInfer<TOOLS>>; /** Limits the tools that are available for the model to call without changing the tool call and result types in the result. */ activeTools?: Array<keyof NoInfer<TOOLS>>; /** Optional specification for parsing structured outputs from the LLM response. */ experimental_output?: Output<OUTPUT, OUTPUT_PARTIAL>; /** * @deprecated Use `prepareStep` instead. */ experimental_prepareStep?: PrepareStepFunction<NoInfer<TOOLS>>; /** Optional function that you can use to provide different settings for a step. */ prepareStep?: PrepareStepFunction<NoInfer<TOOLS>>; /** A function that attempts to repair a tool call that failed to parse. */ experimental_repairToolCall?: ToolCallRepairFunction<NoInfer<TOOLS>>; /** Callback that is called when each step (LLM call) is finished, including intermediate steps. */ onStepFinish?: GenerateTextOnStepFinishCallback<NoInfer<TOOLS>>; /** * Context that is passed into tool execution. * * Experimental (can break in patch releases). * * @default undefined */ experimental_context?: unknown; /** * Internal. For test use only. May change without notice. */ _internal?: { generateId?: IdGenerator; currentDate?: () => Date; }; }): Promise<GenerateTextResult<TOOLS, OUTPUT>> { const model = resolveLanguageModel(modelArg); const stopConditions = asArray(stopWhen); const { maxRetries, retry } = prepareRetries({ maxRetries: maxRetriesArg, abortSignal, }); const callSettings = prepareCallSettings(settings); const baseTelemetryAttributes = getBaseTelemetryAttributes({ model, telemetry, headers, settings: { ...callSettings, maxRetries }, }); const initialPrompt = await standardizePrompt({ system, prompt, messages, }); const tracer = getTracer(telemetry); try { return await recordSpan({ name: 'ai.generateText', attributes: selectTelemetryAttributes({ telemetry, attributes: { ...assembleOperationName({ operationId: 'ai.generateText', telemetry, }), ...baseTelemetryAttributes, // model: 'ai.model.provider': model.provider, 'ai.model.id': model.modelId, // specific settings that only make sense on the outer level: 'ai.prompt': { input: () => JSON.stringify({ system, prompt, messages }), }, }, }), tracer, fn: async span => { const callSettings = prepareCallSettings(settings); let currentModelResponse: Awaited< ReturnType<LanguageModelV2['doGenerate']> > & { response: { id: string; timestamp: Date; modelId: string } }; let clientToolCalls: Array<TypedToolCall<TOOLS>> = []; let clientToolOutputs: Array<ToolOutput<TOOLS>> = []; const responseMessages: Array<ResponseMessage> = []; const steps: GenerateTextResult<TOOLS, OUTPUT>['steps'] = []; do { const stepInputMessages = [ ...initialPrompt.messages, ...responseMessages, ]; const prepareStepResult = await prepareStep?.({ model, steps, stepNumber: steps.length, messages: stepInputMessages, }); const promptMessages = await convertToLanguageModelPrompt({ prompt: { system: prepareStepResult?.system ?? initialPrompt.system, messages: prepareStepResult?.messages ?? stepInputMessages, }, supportedUrls: await model.supportedUrls, }); const stepModel = resolveLanguageModel( prepareStepResult?.model ?? model, ); const { toolChoice: stepToolChoice, tools: stepTools } = prepareToolsAndToolChoice({ tools, toolChoice: prepareStepResult?.toolChoice ?? toolChoice, activeTools: prepareStepResult?.activeTools ?? activeTools, }); currentModelResponse = await retry(() => recordSpan({ name: 'ai.generateText.doGenerate', attributes: selectTelemetryAttributes({ telemetry, attributes: { ...assembleOperationName({ operationId: 'ai.generateText.doGenerate', telemetry, }), ...baseTelemetryAttributes, // model: 'ai.model.provider': stepModel.provider, 'ai.model.id': stepModel.modelId, // prompt: 'ai.prompt.messages': { input: () => stringifyForTelemetry(promptMessages), }, 'ai.prompt.tools': { // convert the language model level tools: input: () => stepTools?.map(tool => JSON.stringify(tool)), }, 'ai.prompt.toolChoice': { input: () => stepToolChoice != null ? JSON.stringify(stepToolChoice) : undefined, }, // standardized gen-ai llm span attributes: 'gen_ai.system': stepModel.provider, 'gen_ai.request.model': stepModel.modelId, 'gen_ai.request.frequency_penalty': settings.frequencyPenalty, 'gen_ai.request.max_tokens': settings.maxOutputTokens, 'gen_ai.request.presence_penalty': settings.presencePenalty, 'gen_ai.request.stop_sequences': settings.stopSequences, 'gen_ai.request.temperature': settings.temperature ?? undefined, 'gen_ai.request.top_k': settings.topK, 'gen_ai.request.top_p': settings.topP, }, }), tracer, fn: async span => { const result = await stepModel.doGenerate({ ...callSettings, tools: stepTools, toolChoice: stepToolChoice, responseFormat: output?.responseFormat, prompt: promptMessages, providerOptions, abortSignal, headers, }); // Fill in default values: const responseData = { id: result.response?.id ?? generateId(), timestamp: result.response?.timestamp ?? currentDate(), modelId: result.response?.modelId ?? stepModel.modelId, headers: result.response?.headers, body: result.response?.body, }; // Add response information to the span: span.setAttributes( selectTelemetryAttributes({ telemetry, attributes: { 'ai.response.finishReason': result.finishReason, 'ai.response.text': { output: () => extractContentText(result.content), }, 'ai.response.toolCalls': { output: () => { const toolCalls = asToolCalls(result.content); return toolCalls == null ? undefined : JSON.stringify(toolCalls); }, }, 'ai.response.id': responseData.id, 'ai.response.model': responseData.modelId, 'ai.response.timestamp': responseData.timestamp.toISOString(), 'ai.response.providerMetadata': JSON.stringify( result.providerMetadata, ), // TODO rename telemetry attributes to inputTokens and outputTokens 'ai.usage.promptTokens': result.usage.inputTokens, 'ai.usage.completionTokens': result.usage.outputTokens, // standardized gen-ai llm span attributes: 'gen_ai.response.finish_reasons': [result.finishReason], 'gen_ai.response.id': responseData.id, 'gen_ai.response.model': responseData.modelId, 'gen_ai.usage.input_tokens': result.usage.inputTokens, 'gen_ai.usage.output_tokens': result.usage.outputTokens, }, }), ); return { ...result, response: responseData }; }, }), ); // parse tool calls: const stepToolCalls: TypedToolCall<TOOLS>[] = await Promise.all( currentModelResponse.content .filter( (part): part is LanguageModelV2ToolCall => part.type === 'tool-call', ) .map(toolCall => parseToolCall({ toolCall, tools, repairToolCall, system, messages: stepInputMessages, }), ), ); // notify the tools that the tool calls are available: for (const toolCall of stepToolCalls) { if (toolCall.invalid) { continue; // ignore invalid tool calls } const tool = tools![toolCall.toolName]; if (tool?.onInputAvailable != null) { await tool.onInputAvailable({ input: toolCall.input, toolCallId: toolCall.toolCallId, messages: stepInputMessages, abortSignal, experimental_context, }); } } // insert error tool outputs for invalid tool calls: // TODO AI SDK 6: invalid inputs should not require output parts const invalidToolCalls = stepToolCalls.filter( toolCall => toolCall.invalid && toolCall.dynamic, ); clientToolOutputs = []; for (const toolCall of invalidToolCalls) { clientToolOutputs.push({ type: 'tool-error', toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, input: toolCall.input, error: getErrorMessage(toolCall.error!), dynamic: true, }); } // execute client tool calls: clientToolCalls = stepToolCalls.filter( toolCall => !toolCall.providerExecuted, ); if (tools != null) { clientToolOutputs.push( ...(await executeTools({ toolCalls: clientToolCalls.filter( toolCall => !toolCall.invalid, ), tools, tracer, telemetry, messages: stepInputMessages, abortSignal, experimental_context, })), ); } // content: const stepContent = asContent({ content: currentModelResponse.content, toolCalls: stepToolCalls, toolOutputs: clientToolOutputs, }); // append to messages for potential next step: responseMessages.push( ...toResponseMessages({ content: stepContent, tools, }), ); // Add step information (after response messages are updated): const currentStepResult: StepResult<TOOLS> = new DefaultStepResult({ content: stepContent, finishReason: currentModelResponse.finishReason, usage: currentModelResponse.usage, warnings: currentModelResponse.warnings, providerMetadata: currentModelResponse.providerMetadata, request: currentModelResponse.request ?? {}, response: { ...currentModelResponse.response, // deep clone msgs to avoid mutating past messages in multi-step: messages: structuredClone(responseMessages), }, }); steps.push(currentStepResult); await onStepFinish?.(currentStepResult); } while ( // there are tool calls: clientToolCalls.length > 0 && // all current tool calls have outputs (incl. execution errors): clientToolOutputs.length === clientToolCalls.length && // continue until a stop condition is met: !(await isStopConditionMet({ stopConditions, steps })) ); // Add response information to the span: span.setAttributes( selectTelemetryAttributes({ telemetry, attributes: { 'ai.response.finishReason': currentModelResponse.finishReason, 'ai.response.text': { output: () => extractContentText(currentModelResponse.content), }, 'ai.response.toolCalls': { output: () => { const toolCalls = asToolCalls(currentModelResponse.content); return toolCalls == null ? undefined : JSON.stringify(toolCalls); }, }, 'ai.response.providerMetadata': JSON.stringify( currentModelResponse.providerMetadata, ), // TODO rename telemetry attributes to inputTokens and outputTokens 'ai.usage.promptTokens': currentModelResponse.usage.inputTokens, 'ai.usage.completionTokens': currentModelResponse.usage.outputTokens, }, }), ); const lastStep = steps[steps.length - 1]; return new DefaultGenerateTextResult({ steps, resolvedOutput: await output?.parseOutput( { text: lastStep.text }, { response: lastStep.response, usage: lastStep.usage, finishReason: lastStep.finishReason, }, ), }); }, }); } catch (error) { throw wrapGatewayError(error); } } async function executeTools<TOOLS extends ToolSet>({ toolCalls, tools, tracer, telemetry, messages, abortSignal, experimental_context, }: { toolCalls: Array<TypedToolCall<TOOLS>>; tools: TOOLS; tracer: Tracer; telemetry: TelemetrySettings | undefined; messages: ModelMessage[]; abortSignal: AbortSignal | undefined; experimental_context: unknown; }): Promise<Array<ToolOutput<TOOLS>>> { const toolOutputs = await Promise.all( toolCalls.map(async ({ toolCallId, toolName, input }) => { const tool = tools[toolName]; if (tool?.execute == null) { return undefined; } return recordSpan({ name: 'ai.toolCall', attributes: selectTelemetryAttributes({ telemetry, attributes: { ...assembleOperationName({ operationId: 'ai.toolCall', telemetry, }), 'ai.toolCall.name': toolName, 'ai.toolCall.id': toolCallId, 'ai.toolCall.args': { output: () => JSON.stringify(input), }, }, }), tracer, fn: async span => { try { const output = await tool.execute!(input, { toolCallId, messages, abortSignal, experimental_context, }); try { span.setAttributes( selectTelemetryAttributes({ telemetry, attributes: { 'ai.toolCall.result': { output: () => JSON.stringify(output), }, }, }), ); } catch (ignored) { // JSON stringify might fail if the result is not serializable, // in which case we just ignore it. In the future we might want to // add an optional serialize method to the tool interface and warn // if the result is not serializable. } return { type: 'tool-result', toolCallId, toolName, input, output, dynamic: tool.type === 'dynamic', } as TypedToolResult<TOOLS>; } catch (error) { recordErrorOnSpan(span, error); return { type: 'tool-error', toolCallId, toolName, input, error, dynamic: tool.type === 'dynamic', } as TypedToolError<TOOLS>; } }, }); }), ); return toolOutputs.filter( (output): output is NonNullable<typeof output> => output != null, ); } class DefaultGenerateTextResult<TOOLS extends ToolSet, OUTPUT> implements GenerateTextResult<TOOLS, OUTPUT> { readonly steps: GenerateTextResult<TOOLS, OUTPUT>['steps']; private readonly resolvedOutput: OUTPUT; constructor(options: { steps: GenerateTextResult<TOOLS, OUTPUT>['steps']; resolvedOutput: OUTPUT; }) { this.steps = options.steps; this.resolvedOutput = options.resolvedOutput; } private get finalStep() { return this.steps[this.steps.length - 1]; } get content() { return this.finalStep.content; } get text() { return this.finalStep.text; } get files() { return this.finalStep.files; } get reasoningText() { return this.finalStep.reasoningText; } get reasoning() { return this.finalStep.reasoning; } get toolCalls() { return this.finalStep.toolCalls; } get staticToolCalls() { return this.finalStep.staticToolCalls; } get dynamicToolCalls() { return this.finalStep.dynamicToolCalls; } get toolResults() { return this.finalStep.toolResults; } get staticToolResults() { return this.finalStep.staticToolResults; } get dynamicToolResults() { return this.finalStep.dynamicToolResults; } get sources() { return this.finalStep.sources; } get finishReason() { return this.finalStep.finishReason; } get warnings() { return this.finalStep.warnings; } get providerMetadata() { return this.finalStep.providerMetadata; } get response() { return this.finalStep.response; } get request() { return this.finalStep.request; } get usage() { return this.finalStep.usage; } get totalUsage() { return this.steps.reduce( (totalUsage, step) => { return addLanguageModelUsage(totalUsage, step.usage); }, { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, reasoningTokens: undefined, cachedInputTokens: undefined, } as LanguageModelUsage, ); } get experimental_output() { if (this.resolvedOutput == null) { throw new NoOutputSpecifiedError(); } return this.resolvedOutput; } } function asToolCalls(content: Array<LanguageModelV2Content>) { const parts = content.filter( (part): part is LanguageModelV2ToolCall => part.type === 'tool-call', ); if (parts.length === 0) { return undefined; } return parts.map(toolCall => ({ toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, input: toolCall.input, })); } function asContent<TOOLS extends ToolSet>({ content, toolCalls, toolOutputs, }: { content: Array<LanguageModelV2Content>; toolCalls: Array<TypedToolCall<TOOLS>>; toolOutputs: Array<ToolOutput<TOOLS>>; }): Array<ContentPart<TOOLS>> { return [ ...content.map(part => { switch (part.type) { case 'text': case 'reasoning': case 'source': return part; case 'file': { return { type: 'file' as const, file: new DefaultGeneratedFile(part), }; } case 'tool-call': { return toolCalls.find( toolCall => toolCall.toolCallId === part.toolCallId, )!; } case 'tool-result': { const toolCall = toolCalls.find( toolCall => toolCall.toolCallId === part.toolCallId, )!; if (toolCall == null) { throw new Error(`Tool call ${part.toolCallId} not found.`); } if (part.isError) { return { type: 'tool-error' as const, toolCallId: part.toolCallId, toolName: part.toolName as keyof TOOLS & string, input: toolCall.input, error: part.result, providerExecuted: true, dynamic: toolCall.dynamic, } as TypedToolError<TOOLS>; } return { type: 'tool-result' as const, toolCallId: part.toolCallId, toolName: part.toolName as keyof TOOLS & string, input: toolCall.input, output: part.result, providerExecuted: true, dynamic: toolCall.dynamic, } as TypedToolResult<TOOLS>; } } }), ...toolOutputs, ]; } --- File: /ai/packages/ai/src/generate-text/generated-file.ts --- import { convertBase64ToUint8Array, convertUint8ArrayToBase64, } from '@ai-sdk/provider-utils'; /** * A generated file. */ export interface GeneratedFile { /** File as a base64 encoded string. */ readonly base64: string; /** File as a Uint8Array. */ readonly uint8Array: Uint8Array; /** The IANA media type of the file. @see https://www.iana.org/assignments/media-types/media-types.xhtml */ readonly mediaType: string; } export class DefaultGeneratedFile implements GeneratedFile { private base64Data: string | undefined; private uint8ArrayData: Uint8Array | undefined; readonly mediaType: string; constructor({ data, mediaType, }: { data: string | Uint8Array; mediaType: string; }) { const isUint8Array = data instanceof Uint8Array; this.base64Data = isUint8Array ? undefined : data; this.uint8ArrayData = isUint8Array ? data : undefined; this.mediaType = mediaType; } // lazy conversion with caching to avoid unnecessary conversion overhead: get base64() { if (this.base64Data == null) { this.base64Data = convertUint8ArrayToBase64(this.uint8ArrayData!); } return this.base64Data; } // lazy conversion with caching to avoid unnecessary conversion overhead: get uint8Array() { if (this.uint8ArrayData == null) { this.uint8ArrayData = convertBase64ToUint8Array(this.base64Data!); } return this.uint8ArrayData; } } export class DefaultGeneratedFileWithType extends DefaultGeneratedFile { readonly type = 'file'; constructor(options: { data: string | Uint8Array; mediaType: string }) { super(options); } } --- File: /ai/packages/ai/src/generate-text/index.ts --- export { generateText } from './generate-text'; export type { GenerateTextOnStepFinishCallback } from './generate-text'; export type { GenerateTextResult } from './generate-text-result'; export type { GeneratedFile as Experimental_GeneratedImage, // Image for backwards compatibility, TODO remove in v5 GeneratedFile, } from './generated-file'; export * as Output from './output'; export type { PrepareStepFunction, PrepareStepResult } from './prepare-step'; export { smoothStream, type ChunkDetector } from './smooth-stream'; export type { StepResult } from './step-result'; export { hasToolCall, stepCountIs, type StopCondition } from './stop-condition'; export { streamText } from './stream-text'; export type { StreamTextOnChunkCallback, StreamTextOnErrorCallback, StreamTextOnFinishCallback, StreamTextOnStepFinishCallback, StreamTextTransform, } from './stream-text'; export type { StreamTextResult, TextStreamPart, UIMessageStreamOptions, } from './stream-text-result'; export type { DynamicToolCall, StaticToolCall, TypedToolCall, } from './tool-call'; export type { ToolCallRepairFunction } from './tool-call-repair-function'; export type { DynamicToolError, StaticToolError, TypedToolError, } from './tool-error'; export type { DynamicToolResult, StaticToolResult, TypedToolResult, } from './tool-result'; export type { ToolSet } from './tool-set'; --- File: /ai/packages/ai/src/generate-text/output.test.ts --- import { fail } from 'assert'; import { z } from 'zod/v4'; import { verifyNoObjectGeneratedError } from '../error/no-object-generated-error'; import { object } from './output'; import { FinishReason } from '../types'; const context = { response: { id: '123', timestamp: new Date(), modelId: '456', }, usage: { inputTokens: 1, outputTokens: 2, totalTokens: 3, reasoningTokens: undefined, cachedInputTokens: undefined, }, finishReason: 'length' as FinishReason, }; describe('Output.object', () => { const output = object({ schema: z.object({ content: z.string() }) }); it('should parse the output of the model', async () => { const result = await output.parseOutput( { text: `{ "content": "test" }` }, context, ); expect(result).toStrictEqual({ content: 'test' }); }); it('should throw NoObjectGeneratedError when parsing fails', async () => { try { await output.parseOutput({ text: '{ broken json' }, context); fail('must throw error'); } catch (error) { verifyNoObjectGeneratedError(error, { message: 'No object generated: could not parse the response.', response: context.response, usage: context.usage, finishReason: context.finishReason, }); } }); it('should throw NoObjectGeneratedError when schema validation fails', async () => { try { await output.parseOutput({ text: `{ "content": 123 }` }, context); fail('must throw error'); } catch (error) { verifyNoObjectGeneratedError(error, { message: 'No object generated: response did not match schema.', response: context.response, usage: context.usage, finishReason: context.finishReason, }); } }); }); --- File: /ai/packages/ai/src/generate-text/output.ts --- import { LanguageModelV2CallOptions } from '@ai-sdk/provider'; import { asSchema, safeParseJSON, safeValidateTypes, Schema, } from '@ai-sdk/provider-utils'; import * as z3 from 'zod/v3'; import * as z4 from 'zod/v4'; import { NoObjectGeneratedError } from '../error/no-object-generated-error'; import { DeepPartial } from '../util/deep-partial'; import { parsePartialJson } from '../util/parse-partial-json'; import { FinishReason } from '../types/language-model'; import { LanguageModelResponseMetadata } from '../types/language-model-response-metadata'; import { LanguageModelUsage } from '../types/usage'; export interface Output<OUTPUT, PARTIAL> { readonly type: 'object' | 'text'; responseFormat: LanguageModelV2CallOptions['responseFormat']; parsePartial(options: { text: string; }): Promise<{ partial: PARTIAL } | undefined>; parseOutput( options: { text: string }, context: { response: LanguageModelResponseMetadata; usage: LanguageModelUsage; finishReason: FinishReason; }, ): Promise<OUTPUT>; } export const text = (): Output<string, string> => ({ type: 'text', responseFormat: { type: 'text' }, async parsePartial({ text }: { text: string }) { return { partial: text }; }, async parseOutput({ text }: { text: string }) { return text; }, }); export const object = <OUTPUT>({ schema: inputSchema, }: { schema: | z4.core.$ZodType<OUTPUT, any> | z3.Schema<OUTPUT, z3.ZodTypeDef, any> | Schema<OUTPUT>; }): Output<OUTPUT, DeepPartial<OUTPUT>> => { const schema = asSchema(inputSchema); return { type: 'object', responseFormat: { type: 'json', schema: schema.jsonSchema, }, async parsePartial({ text }: { text: string }) { const result = await parsePartialJson(text); switch (result.state) { case 'failed-parse': case 'undefined-input': return undefined; case 'repaired-parse': case 'successful-parse': return { // Note: currently no validation of partial results: partial: result.value as DeepPartial<OUTPUT>, }; default: { const _exhaustiveCheck: never = result.state; throw new Error(`Unsupported parse state: ${_exhaustiveCheck}`); } } }, async parseOutput( { text }: { text: string }, context: { response: LanguageModelResponseMetadata; usage: LanguageModelUsage; finishReason: FinishReason; }, ) { const parseResult = await safeParseJSON({ text }); if (!parseResult.success) { throw new NoObjectGeneratedError({ message: 'No object generated: could not parse the response.', cause: parseResult.error, text, response: context.response, usage: context.usage, finishReason: context.finishReason, }); } const validationResult = await safeValidateTypes({ value: parseResult.value, schema, }); if (!validationResult.success) { throw new NoObjectGeneratedError({ message: 'No object generated: response did not match schema.', cause: validationResult.error, text, response: context.response, usage: context.usage, finishReason: context.finishReason, }); } return validationResult.value; }, }; }; --- File: /ai/packages/ai/src/generate-text/parse-tool-call.test.ts --- import { dynamicTool, tool } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { InvalidToolInputError } from '../error/invalid-tool-input-error'; import { NoSuchToolError } from '../error/no-such-tool-error'; import { ToolCallRepairError } from '../error/tool-call-repair-error'; import { parseToolCall } from './parse-tool-call'; describe('parseToolCall', () => { it('should successfully parse a valid tool call', async () => { const result = await parseToolCall({ toolCall: { type: 'tool-call', toolName: 'testTool', toolCallId: '123', input: '{"param1": "test", "param2": 42}', }, tools: { testTool: tool({ inputSchema: z.object({ param1: z.string(), param2: z.number(), }), }), } as const, repairToolCall: undefined, messages: [], system: undefined, }); expect(result).toMatchInlineSnapshot(` { "input": { "param1": "test", "param2": 42, }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "123", "toolName": "testTool", "type": "tool-call", } `); }); it('should successfully parse a valid tool call with provider metadata', async () => { const result = await parseToolCall({ toolCall: { type: 'tool-call', toolName: 'testTool', toolCallId: '123', input: '{"param1": "test", "param2": 42}', providerMetadata: { testProvider: { signature: 'sig', }, }, }, tools: { testTool: tool({ inputSchema: z.object({ param1: z.string(), param2: z.number(), }), }), } as const, repairToolCall: undefined, messages: [], system: undefined, }); expect(result).toMatchInlineSnapshot(` { "input": { "param1": "test", "param2": 42, }, "providerExecuted": undefined, "providerMetadata": { "testProvider": { "signature": "sig", }, }, "toolCallId": "123", "toolName": "testTool", "type": "tool-call", } `); }); it('should successfully process empty tool calls for tools that have no inputSchema', async () => { const result = await parseToolCall({ toolCall: { type: 'tool-call', toolName: 'testTool', toolCallId: '123', input: '', }, tools: { testTool: tool({ inputSchema: z.object({}), }), } as const, repairToolCall: undefined, messages: [], system: undefined, }); expect(result).toMatchInlineSnapshot(` { "input": {}, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "123", "toolName": "testTool", "type": "tool-call", } `); }); it('should successfully process empty object tool calls for tools that have no inputSchema', async () => { const result = await parseToolCall({ toolCall: { type: 'tool-call', toolName: 'testTool', toolCallId: '123', input: '{}', }, tools: { testTool: tool({ inputSchema: z.object({}), }), } as const, repairToolCall: undefined, messages: [], system: undefined, }); expect(result).toMatchInlineSnapshot(` { "input": {}, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "123", "toolName": "testTool", "type": "tool-call", } `); }); it('should throw NoSuchToolError when tools is null', async () => { const result = await parseToolCall({ toolCall: { type: 'tool-call', toolName: 'testTool', toolCallId: '123', input: '{}', }, tools: undefined, repairToolCall: undefined, messages: [], system: undefined, }); expect(result).toMatchInlineSnapshot(` { "dynamic": true, "error": [AI_NoSuchToolError: Model tried to call unavailable tool 'testTool'. No tools are available.], "input": "{}", "invalid": true, "toolCallId": "123", "toolName": "testTool", "type": "tool-call", } `); }); it('should throw NoSuchToolError when tool is not found', async () => { const result = await parseToolCall({ toolCall: { type: 'tool-call', toolName: 'nonExistentTool', toolCallId: '123', input: '{}', }, tools: { testTool: tool({ inputSchema: z.object({ param1: z.string(), param2: z.number(), }), }), } as const, repairToolCall: undefined, messages: [], system: undefined, }); expect(result).toMatchInlineSnapshot(` { "dynamic": true, "error": [AI_NoSuchToolError: Model tried to call unavailable tool 'nonExistentTool'. Available tools: testTool.], "input": "{}", "invalid": true, "toolCallId": "123", "toolName": "nonExistentTool", "type": "tool-call", } `); }); it('should throw InvalidToolArgumentsError when args are invalid', async () => { const result = await parseToolCall({ toolCall: { type: 'tool-call', toolName: 'testTool', toolCallId: '123', input: '{"param1": "test"}', // Missing required param2 }, tools: { testTool: tool({ inputSchema: z.object({ param1: z.string(), param2: z.number(), }), }), } as const, repairToolCall: undefined, messages: [], system: undefined, }); expect(result).toMatchInlineSnapshot(` { "dynamic": true, "error": [AI_InvalidToolInputError: Invalid input for tool testTool: Type validation failed: Value: {"param1":"test"}. Error message: [ { "expected": "number", "code": "invalid_type", "path": [ "param2" ], "message": "Invalid input: expected number, received undefined" } ]], "input": "{"param1": "test"}", "invalid": true, "toolCallId": "123", "toolName": "testTool", "type": "tool-call", } `); }); describe('tool call repair', () => { it('should invoke repairTool when provided and use its result', async () => { const repairToolCall = vi.fn().mockResolvedValue({ toolCallType: 'function', toolName: 'testTool', toolCallId: '123', input: '{"param1": "test", "param2": 42}', }); const result = await parseToolCall({ toolCall: { type: 'tool-call', toolName: 'testTool', toolCallId: '123', input: 'invalid json', // This will trigger repair }, tools: { testTool: tool({ inputSchema: z.object({ param1: z.string(), param2: z.number(), }), }), } as const, repairToolCall, messages: [{ role: 'user', content: 'test message' }], system: 'test system', }); // Verify repair function was called expect(repairToolCall).toHaveBeenCalledTimes(1); expect(repairToolCall).toHaveBeenCalledWith({ toolCall: expect.objectContaining({ toolName: 'testTool', input: 'invalid json', }), tools: expect.any(Object), inputSchema: expect.any(Function), messages: [{ role: 'user', content: 'test message' }], system: 'test system', error: expect.any(InvalidToolInputError), }); // Verify the repaired result was used expect(result).toMatchInlineSnapshot(` { "input": { "param1": "test", "param2": 42, }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "123", "toolName": "testTool", "type": "tool-call", } `); }); it('should re-throw error if tool call repair returns null', async () => { const repairToolCall = vi.fn().mockResolvedValue(null); const result = await parseToolCall({ toolCall: { type: 'tool-call', toolName: 'testTool', toolCallId: '123', input: 'invalid json', }, tools: { testTool: tool({ inputSchema: z.object({ param1: z.string(), param2: z.number(), }), }), } as const, repairToolCall, messages: [], system: undefined, }); expect(result).toMatchInlineSnapshot(` { "dynamic": true, "error": [AI_InvalidToolInputError: Invalid input for tool testTool: JSON parsing failed: Text: invalid json. Error message: Unexpected token 'i', "invalid json" is not valid JSON], "input": "invalid json", "invalid": true, "toolCallId": "123", "toolName": "testTool", "type": "tool-call", } `); }); it('should throw ToolCallRepairError if repairToolCall throws', async () => { const repairToolCall = vi.fn().mockRejectedValue(new Error('test error')); const result = await parseToolCall({ toolCall: { type: 'tool-call', toolName: 'testTool', toolCallId: '123', input: 'invalid json', }, tools: { testTool: tool({ inputSchema: z.object({ param1: z.string(), param2: z.number(), }), }), } as const, repairToolCall, messages: [], system: undefined, }); expect(result).toMatchInlineSnapshot(` { "dynamic": true, "error": [AI_ToolCallRepairError: Error repairing tool call: test error], "input": "invalid json", "invalid": true, "toolCallId": "123", "toolName": "testTool", "type": "tool-call", } `); }); }); it('should set dynamic to true for dynamic tools', async () => { const result = await parseToolCall({ toolCall: { type: 'tool-call', toolName: 'testTool', toolCallId: '123', input: '{"param1": "test", "param2": 42}', }, tools: { testTool: dynamicTool({ inputSchema: z.object({ param1: z.string(), param2: z.number(), }), execute: async () => 'result', }), } as const, repairToolCall: undefined, messages: [], system: undefined, }); expect(result).toMatchInlineSnapshot(` { "dynamic": true, "input": { "param1": "test", "param2": 42, }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "123", "toolName": "testTool", "type": "tool-call", } `); }); }); --- File: /ai/packages/ai/src/generate-text/parse-tool-call.ts --- import { LanguageModelV2ToolCall } from '@ai-sdk/provider'; import { asSchema, ModelMessage, safeParseJSON, safeValidateTypes, } from '@ai-sdk/provider-utils'; import { InvalidToolInputError } from '../error/invalid-tool-input-error'; import { NoSuchToolError } from '../error/no-such-tool-error'; import { ToolCallRepairError } from '../error/tool-call-repair-error'; import { TypedToolCall } from './tool-call'; import { ToolCallRepairFunction } from './tool-call-repair-function'; import { ToolSet } from './tool-set'; export async function parseToolCall<TOOLS extends ToolSet>({ toolCall, tools, repairToolCall, system, messages, }: { toolCall: LanguageModelV2ToolCall; tools: TOOLS | undefined; repairToolCall: ToolCallRepairFunction<TOOLS> | undefined; system: string | undefined; messages: ModelMessage[]; }): Promise<TypedToolCall<TOOLS>> { try { if (tools == null) { throw new NoSuchToolError({ toolName: toolCall.toolName }); } try { return await doParseToolCall({ toolCall, tools }); } catch (error) { if ( repairToolCall == null || !( NoSuchToolError.isInstance(error) || InvalidToolInputError.isInstance(error) ) ) { throw error; } let repairedToolCall: LanguageModelV2ToolCall | null = null; try { repairedToolCall = await repairToolCall({ toolCall, tools, inputSchema: ({ toolName }) => { const { inputSchema } = tools[toolName]; return asSchema(inputSchema).jsonSchema; }, system, messages, error, }); } catch (repairError) { throw new ToolCallRepairError({ cause: repairError, originalError: error, }); } // no repaired tool call returned if (repairedToolCall == null) { throw error; } return await doParseToolCall({ toolCall: repairedToolCall, tools }); } } catch (error) { // TODO AI SDK 6: special invalid tool call parts return { type: 'tool-call', toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, input: toolCall.input, dynamic: true, invalid: true, error, }; } } async function doParseToolCall<TOOLS extends ToolSet>({ toolCall, tools, }: { toolCall: LanguageModelV2ToolCall; tools: TOOLS; }): Promise<TypedToolCall<TOOLS>> { const toolName = toolCall.toolName as keyof TOOLS & string; const tool = tools[toolName]; if (tool == null) { throw new NoSuchToolError({ toolName: toolCall.toolName, availableTools: Object.keys(tools), }); } const schema = asSchema(tool.inputSchema); // when the tool call has no arguments, we try passing an empty object to the schema // (many LLMs generate empty strings for tool calls with no arguments) const parseResult = toolCall.input.trim() === '' ? await safeValidateTypes({ value: {}, schema }) : await safeParseJSON({ text: toolCall.input, schema }); if (parseResult.success === false) { throw new InvalidToolInputError({ toolName, toolInput: toolCall.input, cause: parseResult.error, }); } return tool.type === 'dynamic' ? { type: 'tool-call', toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, input: parseResult.value, providerExecuted: toolCall.providerExecuted, providerMetadata: toolCall.providerMetadata, dynamic: true, } : { type: 'tool-call', toolCallId: toolCall.toolCallId, toolName, input: parseResult.value, providerExecuted: toolCall.providerExecuted, providerMetadata: toolCall.providerMetadata, }; } --- File: /ai/packages/ai/src/generate-text/prepare-step.ts --- import { ModelMessage, Tool } from '@ai-sdk/provider-utils'; import { LanguageModel, ToolChoice } from '../types/language-model'; import { StepResult } from './step-result'; /** Function that you can use to provide different settings for a step. @param options - The options for the step. @param options.steps - The steps that have been executed so far. @param options.stepNumber - The number of the step that is being executed. @param options.model - The model that is being used. @returns An object that contains the settings for the step. If you return undefined (or for undefined settings), the settings from the outer level will be used. */ export type PrepareStepFunction< TOOLS extends Record<string, Tool> = Record<string, Tool>, > = (options: { steps: Array<StepResult<NoInfer<TOOLS>>>; stepNumber: number; model: LanguageModel; messages: Array<ModelMessage>; }) => PromiseLike<PrepareStepResult<TOOLS>> | PrepareStepResult<TOOLS>; export type PrepareStepResult< TOOLS extends Record<string, Tool> = Record<string, Tool>, > = | { model?: LanguageModel; toolChoice?: ToolChoice<NoInfer<TOOLS>>; activeTools?: Array<keyof NoInfer<TOOLS>>; system?: string; messages?: Array<ModelMessage>; } | undefined; --- File: /ai/packages/ai/src/generate-text/reasoning.ts --- import { ReasoningPart } from '@ai-sdk/provider-utils'; export function asReasoningText( reasoningParts: Array<ReasoningPart>, ): string | undefined { const reasoningText = reasoningParts.map(part => part.text).join(''); return reasoningText.length > 0 ? reasoningText : undefined; } --- File: /ai/packages/ai/src/generate-text/response-message.ts --- import { AssistantModelMessage, ToolModelMessage, } from '@ai-sdk/provider-utils'; /** A message that was generated during the generation process. It can be either an assistant message or a tool message. */ export type ResponseMessage = AssistantModelMessage | ToolModelMessage; --- File: /ai/packages/ai/src/generate-text/run-tools-transformation.test.ts --- import { LanguageModelV2StreamPart } from '@ai-sdk/provider'; import { delay } from '@ai-sdk/provider-utils'; import { convertArrayToReadableStream, convertReadableStreamToArray, } from '@ai-sdk/provider-utils/test'; import { z } from 'zod/v4'; import { NoSuchToolError } from '../error/no-such-tool-error'; import { MockTracer } from '../test/mock-tracer'; import { runToolsTransformation } from './run-tools-transformation'; const testUsage = { inputTokens: 3, outputTokens: 10, totalTokens: 13, reasoningTokens: undefined, cachedInputTokens: undefined, }; describe('runToolsTransformation', () => { it('should forward text deltas correctly', async () => { const inputStream: ReadableStream<LanguageModelV2StreamPart> = convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'text' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]); const transformedStream = runToolsTransformation({ tools: undefined, generatorStream: inputStream, tracer: new MockTracer(), telemetry: undefined, messages: [], system: undefined, abortSignal: undefined, repairToolCall: undefined, experimental_context: undefined, }); const result = await convertReadableStreamToArray(transformedStream); expect(result).toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, { "delta": "text", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, ] `); }); it('should handle async tool execution', async () => { const inputStream: ReadableStream<LanguageModelV2StreamPart> = convertArrayToReadableStream([ { type: 'tool-call', toolCallId: 'call-1', toolName: 'syncTool', input: `{ "value": "test" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]); const transformedStream = runToolsTransformation({ tools: { syncTool: { inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => `${value}-sync-result`, }, }, generatorStream: inputStream, tracer: new MockTracer(), telemetry: undefined, messages: [], system: undefined, abortSignal: undefined, repairToolCall: undefined, experimental_context: undefined, }); expect(await convertReadableStreamToArray(transformedStream)) .toMatchInlineSnapshot(` [ { "input": { "value": "test", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "syncTool", "type": "tool-call", }, { "input": { "value": "test", }, "output": "test-sync-result", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "syncTool", "type": "tool-result", }, { "finishReason": "stop", "providerMetadata": undefined, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, ] `); }); it('should handle sync tool execution', async () => { const inputStream: ReadableStream<LanguageModelV2StreamPart> = convertArrayToReadableStream([ { type: 'tool-call', toolCallId: 'call-1', toolName: 'syncTool', input: `{ "value": "test" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]); const transformedStream = runToolsTransformation({ tools: { syncTool: { inputSchema: z.object({ value: z.string() }), execute: ({ value }) => `${value}-sync-result`, }, }, generatorStream: inputStream, tracer: new MockTracer(), telemetry: undefined, messages: [], system: undefined, abortSignal: undefined, repairToolCall: undefined, experimental_context: undefined, }); expect(await convertReadableStreamToArray(transformedStream)) .toMatchInlineSnapshot(` [ { "input": { "value": "test", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "syncTool", "type": "tool-call", }, { "input": { "value": "test", }, "output": "test-sync-result", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "syncTool", "type": "tool-result", }, { "finishReason": "stop", "providerMetadata": undefined, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, ] `); }); it('should hold off on sending finish until the delayed tool result is received', async () => { const inputStream: ReadableStream<LanguageModelV2StreamPart> = convertArrayToReadableStream([ { type: 'tool-call', toolCallId: 'call-1', toolName: 'delayedTool', input: `{ "value": "test" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]); const transformedStream = runToolsTransformation({ tools: { delayedTool: { inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => { await delay(0); // Simulate delayed execution return `${value}-delayed-result`; }, }, }, generatorStream: inputStream, tracer: new MockTracer(), telemetry: undefined, messages: [], system: undefined, abortSignal: undefined, repairToolCall: undefined, experimental_context: undefined, }); const result = await convertReadableStreamToArray(transformedStream); expect(result).toMatchInlineSnapshot(` [ { "input": { "value": "test", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "delayedTool", "type": "tool-call", }, { "input": { "value": "test", }, "output": "test-delayed-result", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "delayedTool", "type": "tool-result", }, { "finishReason": "stop", "providerMetadata": undefined, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, ] `); }); it('should try to repair tool call when the tool name is not found', async () => { const inputStream: ReadableStream<LanguageModelV2StreamPart> = convertArrayToReadableStream([ { type: 'tool-call', toolCallId: 'call-1', toolName: 'unknownTool', input: `{ "value": "test" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]); const transformedStream = runToolsTransformation({ generatorStream: inputStream, tracer: new MockTracer(), telemetry: undefined, messages: [], system: undefined, abortSignal: undefined, tools: { correctTool: { inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => `${value}-result`, }, }, repairToolCall: async ({ toolCall, tools, inputSchema, error }) => { expect(NoSuchToolError.isInstance(error)).toBe(true); expect(toolCall).toStrictEqual({ type: 'tool-call', toolCallId: 'call-1', toolName: 'unknownTool', input: `{ "value": "test" }`, }); return { ...toolCall, toolName: 'correctTool' }; }, experimental_context: undefined, }); expect(await convertReadableStreamToArray(transformedStream)) .toMatchInlineSnapshot(` [ { "input": { "value": "test", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "correctTool", "type": "tool-call", }, { "input": { "value": "test", }, "output": "test-result", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "correctTool", "type": "tool-result", }, { "finishReason": "stop", "providerMetadata": undefined, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, ] `); }); it('should not call execute for provider-executed tool calls', async () => { let toolExecuted = false; const inputStream: ReadableStream<LanguageModelV2StreamPart> = convertArrayToReadableStream([ { type: 'tool-call', toolCallId: 'call-1', toolName: 'providerTool', input: `{ "value": "test" }`, providerExecuted: true, }, { type: 'tool-result', toolCallId: 'call-1', toolName: 'providerTool', providerExecuted: true, result: { example: 'example' }, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]); const transformedStream = runToolsTransformation({ tools: { providerTool: { inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => { toolExecuted = true; return `${value}-should-not-execute`; }, }, }, generatorStream: inputStream, tracer: new MockTracer(), telemetry: undefined, messages: [], system: undefined, abortSignal: undefined, repairToolCall: undefined, experimental_context: undefined, }); await convertReadableStreamToArray(transformedStream); expect(toolExecuted).toBe(false); }); }); --- File: /ai/packages/ai/src/generate-text/run-tools-transformation.ts --- import { LanguageModelV2CallWarning, LanguageModelV2StreamPart, } from '@ai-sdk/provider'; import { generateId, getErrorMessage, ModelMessage, } from '@ai-sdk/provider-utils'; import { Tracer } from '@opentelemetry/api'; import { assembleOperationName } from '../telemetry/assemble-operation-name'; import { recordErrorOnSpan, recordSpan } from '../telemetry/record-span'; import { selectTelemetryAttributes } from '../telemetry/select-telemetry-attributes'; import { TelemetrySettings } from '../telemetry/telemetry-settings'; import { FinishReason, LanguageModelUsage, ProviderMetadata } from '../types'; import { Source } from '../types/language-model'; import { DefaultGeneratedFileWithType, GeneratedFile } from './generated-file'; import { parseToolCall } from './parse-tool-call'; import { TypedToolCall } from './tool-call'; import { ToolCallRepairFunction } from './tool-call-repair-function'; import { TypedToolError } from './tool-error'; import { TypedToolResult } from './tool-result'; import { ToolSet } from './tool-set'; export type SingleRequestTextStreamPart<TOOLS extends ToolSet> = // Text blocks: | { type: 'text-start'; providerMetadata?: ProviderMetadata; id: string; } | { type: 'text-delta'; id: string; providerMetadata?: ProviderMetadata; delta: string; } | { type: 'text-end'; providerMetadata?: ProviderMetadata; id: string; } // Reasoning blocks: | { type: 'reasoning-start'; providerMetadata?: ProviderMetadata; id: string; } | { type: 'reasoning-delta'; id: string; providerMetadata?: ProviderMetadata; delta: string; } | { type: 'reasoning-end'; id: string; providerMetadata?: ProviderMetadata; } // Tool calls: | { type: 'tool-input-start'; id: string; toolName: string; providerMetadata?: ProviderMetadata; } | { type: 'tool-input-delta'; id: string; delta: string; providerMetadata?: ProviderMetadata; } | { type: 'tool-input-end'; id: string; providerMetadata?: ProviderMetadata; } | ({ type: 'source' } & Source) | { type: 'file'; file: GeneratedFile } // different because of GeneratedFile object | ({ type: 'tool-call' } & TypedToolCall<TOOLS>) | ({ type: 'tool-result' } & TypedToolResult<TOOLS>) | ({ type: 'tool-error' } & TypedToolError<TOOLS>) | { type: 'file'; file: GeneratedFile } // different because of GeneratedFile object | { type: 'stream-start'; warnings: LanguageModelV2CallWarning[] } | { type: 'response-metadata'; id?: string; timestamp?: Date; modelId?: string; } | { type: 'finish'; finishReason: FinishReason; usage: LanguageModelUsage; providerMetadata?: ProviderMetadata; } | { type: 'error'; error: unknown } | { type: 'raw'; rawValue: unknown }; export function runToolsTransformation<TOOLS extends ToolSet>({ tools, generatorStream, tracer, telemetry, system, messages, abortSignal, repairToolCall, experimental_context, }: { tools: TOOLS | undefined; generatorStream: ReadableStream<LanguageModelV2StreamPart>; tracer: Tracer; telemetry: TelemetrySettings | undefined; system: string | undefined; messages: ModelMessage[]; abortSignal: AbortSignal | undefined; repairToolCall: ToolCallRepairFunction<TOOLS> | undefined; experimental_context: unknown; }): ReadableStream<SingleRequestTextStreamPart<TOOLS>> { // tool results stream let toolResultsStreamController: ReadableStreamDefaultController< SingleRequestTextStreamPart<TOOLS> > | null = null; const toolResultsStream = new ReadableStream< SingleRequestTextStreamPart<TOOLS> >({ start(controller) { toolResultsStreamController = controller; }, }); // keep track of outstanding tool results for stream closing: const outstandingToolResults = new Set<string>(); // keep track of tool inputs for provider-side tool results const toolInputs = new Map<string, unknown>(); let canClose = false; let finishChunk: | (SingleRequestTextStreamPart<TOOLS> & { type: 'finish' }) | undefined = undefined; function attemptClose() { // close the tool results controller if no more outstanding tool calls if (canClose && outstandingToolResults.size === 0) { // we delay sending the finish chunk until all tool results (incl. delayed ones) // are received to ensure that the frontend receives tool results before a message // finish event arrives. if (finishChunk != null) { toolResultsStreamController!.enqueue(finishChunk); } toolResultsStreamController!.close(); } } // forward stream const forwardStream = new TransformStream< LanguageModelV2StreamPart, SingleRequestTextStreamPart<TOOLS> >({ async transform( chunk: LanguageModelV2StreamPart, controller: TransformStreamDefaultController< SingleRequestTextStreamPart<TOOLS> >, ) { const chunkType = chunk.type; switch (chunkType) { // forward: case 'stream-start': case 'text-start': case 'text-delta': case 'text-end': case 'reasoning-start': case 'reasoning-delta': case 'reasoning-end': case 'tool-input-start': case 'tool-input-delta': case 'tool-input-end': case 'source': case 'response-metadata': case 'error': case 'raw': { controller.enqueue(chunk); break; } case 'file': { controller.enqueue({ type: 'file', file: new DefaultGeneratedFileWithType({ data: chunk.data, mediaType: chunk.mediaType, }), }); break; } case 'finish': { finishChunk = { type: 'finish', finishReason: chunk.finishReason, usage: chunk.usage, providerMetadata: chunk.providerMetadata, }; break; } // process tool call: case 'tool-call': { try { const toolCall = await parseToolCall({ toolCall: chunk, tools, repairToolCall, system, messages, }); controller.enqueue(toolCall); // handle invalid tool calls: if (toolCall.invalid) { toolResultsStreamController!.enqueue({ type: 'tool-error', toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, input: toolCall.input, error: getErrorMessage(toolCall.error!), dynamic: true, }); break; } const tool = tools![toolCall.toolName]; toolInputs.set(toolCall.toolCallId, toolCall.input); if (tool.onInputAvailable != null) { await tool.onInputAvailable({ input: toolCall.input, toolCallId: toolCall.toolCallId, messages, abortSignal, experimental_context, }); } // Only execute tools that are not provider-executed: if (tool.execute != null && toolCall.providerExecuted !== true) { const toolExecutionId = generateId(); // use our own id to guarantee uniqueness outstandingToolResults.add(toolExecutionId); // Note: we don't await the tool execution here (by leaving out 'await' on recordSpan), // because we want to process the next chunk as soon as possible. // This is important for the case where the tool execution takes a long time. recordSpan({ name: 'ai.toolCall', attributes: selectTelemetryAttributes({ telemetry, attributes: { ...assembleOperationName({ operationId: 'ai.toolCall', telemetry, }), 'ai.toolCall.name': toolCall.toolName, 'ai.toolCall.id': toolCall.toolCallId, 'ai.toolCall.args': { output: () => JSON.stringify(toolCall.input), }, }, }), tracer, fn: async span => { let output: unknown; try { output = await tool.execute!(toolCall.input, { toolCallId: toolCall.toolCallId, messages, abortSignal, experimental_context, }); } catch (error) { recordErrorOnSpan(span, error); toolResultsStreamController!.enqueue({ ...toolCall, type: 'tool-error', error, } satisfies TypedToolError<TOOLS>); outstandingToolResults.delete(toolExecutionId); attemptClose(); return; } toolResultsStreamController!.enqueue({ ...toolCall, type: 'tool-result', output, } satisfies TypedToolResult<TOOLS>); outstandingToolResults.delete(toolExecutionId); attemptClose(); // record telemetry try { span.setAttributes( selectTelemetryAttributes({ telemetry, attributes: { 'ai.toolCall.result': { output: () => JSON.stringify(output), }, }, }), ); } catch (ignored) { // JSON stringify might fail if the result is not serializable, // in which case we just ignore it. In the future we might want to // add an optional serialize method to the tool interface and warn // if the result is not serializable. } }, }); } } catch (error) { toolResultsStreamController!.enqueue({ type: 'error', error }); } break; } case 'tool-result': { const toolName = chunk.toolName as keyof TOOLS & string; if (chunk.isError) { toolResultsStreamController!.enqueue({ type: 'tool-error', toolCallId: chunk.toolCallId, toolName, input: toolInputs.get(chunk.toolCallId), providerExecuted: chunk.providerExecuted, error: chunk.result, } as TypedToolError<TOOLS>); } else { controller.enqueue({ type: 'tool-result', toolCallId: chunk.toolCallId, toolName, input: toolInputs.get(chunk.toolCallId), output: chunk.result, providerExecuted: chunk.providerExecuted, } as TypedToolResult<TOOLS>); } break; } default: { const _exhaustiveCheck: never = chunkType; throw new Error(`Unhandled chunk type: ${_exhaustiveCheck}`); } } }, flush() { canClose = true; attemptClose(); }, }); // combine the generator stream and the tool results stream return new ReadableStream<SingleRequestTextStreamPart<TOOLS>>({ async start(controller) { // need to wait for both pipes so there are no dangling promises that // can cause uncaught promise rejections when the stream is aborted return Promise.all([ generatorStream.pipeThrough(forwardStream).pipeTo( new WritableStream({ write(chunk) { controller.enqueue(chunk); }, close() { // the generator stream controller is automatically closed when it's consumed }, }), ), toolResultsStream.pipeTo( new WritableStream({ write(chunk) { controller.enqueue(chunk); }, close() { controller.close(); }, }), ), ]); }, }); } --- File: /ai/packages/ai/src/generate-text/smooth-stream.test.ts --- import { convertArrayToReadableStream } from '@ai-sdk/provider-utils/test'; import { smoothStream } from './smooth-stream'; import { TextStreamPart } from './stream-text-result'; import { ToolSet } from './tool-set'; describe('smoothStream', () => { let events: any[] = []; beforeEach(() => { events = []; }); async function consumeStream(stream: ReadableStream<any>) { const reader = stream.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; events.push(value); } } function delay(delayInMs: number | null) { events.push(`delay ${delayInMs}`); return Promise.resolve(); } describe('throws error if chunking option is invalid', async () => { it('throws error if chunking strategy is invalid', async () => { expect(() => { smoothStream({ chunking: 'foo' as any, }); }).toThrowError(); }); it('throws error if chunking option is null', async () => { expect(() => { smoothStream({ chunking: null as any, }); }).toThrowError(); }); }); describe('word chunking', () => { it('should combine partial words', async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { text: 'Hello', type: 'text-delta', id: '1' }, { text: ', ', type: 'text-delta', id: '1' }, { text: 'world!', type: 'text-delta', id: '1' }, { type: 'text-end', id: '1' }, ]).pipeThrough( smoothStream({ delayInMs: 10, _internal: { delay }, })({ tools: {} }), ); await consumeStream(stream); expect(events).toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, "delay 10", { "id": "1", "text": "Hello, ", "type": "text-delta", }, { "id": "1", "text": "world!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, ] `); }); it('should split larger text chunks', async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { text: 'Hello, World! This is an example text.', type: 'text-delta', id: '1', }, { type: 'text-end', id: '1' }, ]).pipeThrough( smoothStream({ delayInMs: 10, _internal: { delay }, })({ tools: {} }), ); await consumeStream(stream); expect(events).toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, "delay 10", { "id": "1", "text": "Hello, ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "World! ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "This ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "is ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "an ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "example ", "type": "text-delta", }, { "id": "1", "text": "text.", "type": "text-delta", }, { "id": "1", "type": "text-end", }, ] `); }); it('should keep longer whitespace sequences together', async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { text: 'First line', type: 'text-delta', id: '1' }, { text: ' \n\n', type: 'text-delta', id: '1' }, { text: ' ', type: 'text-delta', id: '1' }, { text: ' Multiple spaces', type: 'text-delta', id: '1' }, { text: '\n Indented', type: 'text-delta', id: '1' }, { type: 'text-end', id: '1' }, ]).pipeThrough( smoothStream({ delayInMs: 10, _internal: { delay }, })({ tools: {} }), ); await consumeStream(stream); expect(events).toEqual([ { id: '1', type: 'text-start' }, 'delay 10', { id: '1', text: 'First ', type: 'text-delta', }, 'delay 10', { id: '1', text: 'line \n\n', type: 'text-delta', }, 'delay 10', { // note: leading whitespace is included here // because it is part of the new chunk: id: '1', text: ' Multiple ', type: 'text-delta', }, 'delay 10', { id: '1', text: 'spaces\n ', type: 'text-delta', }, { id: '1', text: 'Indented', type: 'text-delta', }, { id: '1', type: 'text-end' }, ]); }); it('should send remaining text buffer before tool call starts', async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { text: 'I will check the', type: 'text-delta', id: '1' }, { text: ' weather in Lon', type: 'text-delta', id: '1' }, { text: 'don.', type: 'text-delta', id: '1' }, { type: 'tool-call', toolCallId: '1', toolName: 'weather', input: { city: 'London' }, }, { type: 'text-end', id: '1' }, ]).pipeThrough( smoothStream({ delayInMs: 10, _internal: { delay }, })({ tools: {} }), ); await consumeStream(stream); expect(events).toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, "delay 10", { "id": "1", "text": "I ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "will ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "check ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "the ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "weather ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "in ", "type": "text-delta", }, { "id": "1", "text": "London.", "type": "text-delta", }, { "input": { "city": "London", }, "toolCallId": "1", "toolName": "weather", "type": "tool-call", }, { "id": "1", "type": "text-end", }, ] `); }); it('should send remaining text buffer before tool call starts and tool call streaming is enabled', async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', text: 'I will check the' }, { type: 'text-delta', id: '1', text: ' weather in Lon' }, { type: 'text-delta', id: '1', text: 'don.' }, { type: 'tool-input-start', toolName: 'weather', id: '2', }, { type: 'tool-input-delta', id: '2', delta: '{ city: "London" }' }, { type: 'tool-input-end', id: '2' }, { type: 'tool-call', toolCallId: '1', toolName: 'weather', input: { city: 'London' }, }, { type: 'text-end', id: '1' }, ]).pipeThrough( smoothStream({ delayInMs: 10, _internal: { delay }, })({ tools: {} }), ); await consumeStream(stream); expect(events).toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, "delay 10", { "id": "1", "text": "I ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "will ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "check ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "the ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "weather ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "in ", "type": "text-delta", }, { "id": "1", "text": "London.", "type": "text-delta", }, { "id": "2", "toolName": "weather", "type": "tool-input-start", }, { "delta": "{ city: "London" }", "id": "2", "type": "tool-input-delta", }, { "id": "2", "type": "tool-input-end", }, { "input": { "city": "London", }, "toolCallId": "1", "toolName": "weather", "type": "tool-call", }, { "id": "1", "type": "text-end", }, ] `); }); it(`doesn't return chunks with just spaces`, async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', text: ' ' }, { type: 'text-delta', id: '1', text: ' ' }, { type: 'text-delta', id: '1', text: ' ' }, { type: 'text-delta', id: '1', text: 'foo' }, { type: 'text-end', id: '1' }, ]).pipeThrough( smoothStream({ delayInMs: 10, _internal: { delay }, })({ tools: {} }), ); await consumeStream(stream); expect(events).toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, { "id": "1", "text": " foo", "type": "text-delta", }, { "id": "1", "type": "text-end", }, ] `); }); }); describe('line chunking', () => { it('should split text by lines when using line chunking mode', async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { text: 'First line\nSecond line\nThird line with more text\n', type: 'text-delta', id: '1', }, { text: 'Partial line', type: 'text-delta', id: '1' }, { text: ' continues\nFinal line\n', type: 'text-delta', id: '1' }, { type: 'text-end', id: '1' }, ]).pipeThrough( smoothStream({ delayInMs: 10, chunking: 'line', _internal: { delay }, })({ tools: {} }), ); await consumeStream(stream); expect(events).toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, "delay 10", { "id": "1", "text": "First line ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "Second line ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "Third line with more text ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "Partial line continues ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "Final line ", "type": "text-delta", }, { "id": "1", "type": "text-end", }, ] `); }); it('should handle text without line endings in line chunking mode', async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { text: 'Text without', type: 'text-delta', id: '1' }, { text: ' any line', type: 'text-delta', id: '1' }, { text: ' breaks', type: 'text-delta', id: '1' }, { type: 'text-end', id: '1' }, ]).pipeThrough( smoothStream({ chunking: 'line', _internal: { delay }, })({ tools: {} }), ); await consumeStream(stream); expect(events).toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, { "id": "1", "text": "Text without any line breaks", "type": "text-delta", }, { "id": "1", "type": "text-end", }, ] `); }); }); describe('custom chunking', () => { it(`should return correct result for regexes that don't match from the exact start onwards`, async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { text: 'Hello_, world!', type: 'text-delta', id: '1' }, { type: 'text-end', id: '1' }, ]).pipeThrough( smoothStream({ chunking: /_/, delayInMs: 10, _internal: { delay }, })({ tools: {} }), ); await consumeStream(stream); expect(events).toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, "delay 10", { "id": "1", "text": "Hello_", "type": "text-delta", }, { "id": "1", "text": ", world!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, ] `); }); it('should support custom chunking regexps (character-level)', async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { text: 'Hello, world!', type: 'text-delta', id: '1' }, { type: 'text-end', id: '1' }, ]).pipeThrough( smoothStream({ chunking: /./, delayInMs: 10, _internal: { delay }, })({ tools: {} }), ); await consumeStream(stream); expect(events).toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, "delay 10", { "id": "1", "text": "H", "type": "text-delta", }, "delay 10", { "id": "1", "text": "e", "type": "text-delta", }, "delay 10", { "id": "1", "text": "l", "type": "text-delta", }, "delay 10", { "id": "1", "text": "l", "type": "text-delta", }, "delay 10", { "id": "1", "text": "o", "type": "text-delta", }, "delay 10", { "id": "1", "text": ",", "type": "text-delta", }, "delay 10", { "id": "1", "text": " ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "w", "type": "text-delta", }, "delay 10", { "id": "1", "text": "o", "type": "text-delta", }, "delay 10", { "id": "1", "text": "r", "type": "text-delta", }, "delay 10", { "id": "1", "text": "l", "type": "text-delta", }, "delay 10", { "id": "1", "text": "d", "type": "text-delta", }, "delay 10", { "id": "1", "text": "!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, ] `); }); }); describe('custom callback chunking', () => { it('should support custom chunking callback', async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { text: 'He_llo, ', type: 'text-delta', id: '1' }, { text: 'w_orld!', type: 'text-delta', id: '1' }, { type: 'text-end', id: '1' }, ]).pipeThrough( smoothStream({ chunking: buffer => /[^_]*_/.exec(buffer)?.[0], _internal: { delay }, })({ tools: {} }), ); await consumeStream(stream); expect(events).toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, "delay 10", { "id": "1", "text": "He_", "type": "text-delta", }, "delay 10", { "id": "1", "text": "llo, w_", "type": "text-delta", }, { "id": "1", "text": "orld!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, ] `); }); describe('throws errors if the chunking function invalid matches', async () => { it('throws empty match error', async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { text: 'Hello, world!', type: 'text-delta', id: '1' }, { type: 'text-end', id: '1' }, ]).pipeThrough( smoothStream({ chunking: () => '', _internal: { delay } })({ tools: {}, }), ); await expect( consumeStream(stream), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Error: Chunking function must return a non-empty string.]`, ); }); it('throws match prefix error', async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { text: 'Hello, world!', type: 'text-delta', id: '1' }, { type: 'text-end', id: '1' }, ]).pipeThrough( smoothStream({ chunking: () => 'world', _internal: { delay } })({ tools: {}, }), ); await expect( consumeStream(stream), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Error: Chunking function must return a match that is a prefix of the buffer. Received: "world" expected to start with "Hello, world!"]`, ); }); }); }); describe('delay', () => { it('should default to 10ms', async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { text: 'Hello, world!', type: 'text-delta', id: '1' }, { type: 'text-end', id: '1' }, ]).pipeThrough( smoothStream({ _internal: { delay }, })({ tools: {} }), ); await consumeStream(stream); expect(events).toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, "delay 10", { "id": "1", "text": "Hello, ", "type": "text-delta", }, { "id": "1", "text": "world!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, ] `); }); it('should support different number of milliseconds delay', async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { text: 'Hello, world!', type: 'text-delta', id: '1' }, { type: 'text-end', id: '1' }, ]).pipeThrough( smoothStream({ delayInMs: 20, _internal: { delay }, })({ tools: {} }), ); await consumeStream(stream); expect(events).toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, "delay 20", { "id": "1", "text": "Hello, ", "type": "text-delta", }, { "id": "1", "text": "world!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, ] `); }); it('should support null delay', async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { text: 'Hello, world!', type: 'text-delta', id: '1' }, { type: 'text-end', id: '1' }, ]).pipeThrough( smoothStream({ delayInMs: null, _internal: { delay }, })({ tools: {} }), ); await consumeStream(stream); expect(events).toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, "delay null", { "id": "1", "text": "Hello, ", "type": "text-delta", }, { "id": "1", "text": "world!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, ] `); }); }); describe('text part id changes', () => { it('should change the id when the text part id changes', async () => { const stream = convertArrayToReadableStream<TextStreamPart<ToolSet>>([ { type: 'text-start', id: '1' }, { type: 'text-start', id: '2' }, { text: 'I will check the', type: 'text-delta', id: '1' }, { text: ' weather in Lon', type: 'text-delta', id: '1' }, { text: 'don.', type: 'text-delta', id: '1' }, { text: 'I will check the', type: 'text-delta', id: '2' }, { text: ' weather in Lon', type: 'text-delta', id: '2' }, { text: 'don.', type: 'text-delta', id: '2' }, { type: 'text-end', id: '1' }, { type: 'text-end', id: '2' }, ]).pipeThrough( smoothStream({ _internal: { delay }, })({ tools: {} }), ); await consumeStream(stream); expect(events).toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, { "id": "2", "type": "text-start", }, "delay 10", { "id": "1", "text": "I ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "will ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "check ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "the ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "weather ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "in ", "type": "text-delta", }, "delay 10", { "id": "1", "text": "London.", "type": "text-delta", }, "delay 10", { "id": "2", "text": "I ", "type": "text-delta", }, "delay 10", { "id": "2", "text": "will ", "type": "text-delta", }, { "id": "2", "text": "check ", "type": "text-delta", }, "delay 10", { "id": "2", "text": "the ", "type": "text-delta", }, "delay 10", { "id": "2", "text": "weather ", "type": "text-delta", }, "delay 10", { "id": "2", "text": "in ", "type": "text-delta", }, { "id": "2", "text": "London.", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "id": "2", "type": "text-end", }, ] `); }); }); }); --- File: /ai/packages/ai/src/generate-text/smooth-stream.ts --- import { delay as originalDelay } from '@ai-sdk/provider-utils'; import { TextStreamPart } from './stream-text-result'; import { ToolSet } from './tool-set'; import { InvalidArgumentError } from '@ai-sdk/provider'; const CHUNKING_REGEXPS = { word: /\S+\s+/m, line: /\n+/m, }; /** * Detects the first chunk in a buffer. * * @param buffer - The buffer to detect the first chunk in. * * @returns The first detected chunk, or `undefined` if no chunk was detected. */ export type ChunkDetector = (buffer: string) => string | undefined | null; /** * Smooths text streaming output. * * @param delayInMs - The delay in milliseconds between each chunk. Defaults to 10ms. Can be set to `null` to skip the delay. * @param chunking - Controls how the text is chunked for streaming. Use "word" to stream word by word (default), "line" to stream line by line, or provide a custom RegExp pattern for custom chunking. * * @returns A transform stream that smooths text streaming output. */ export function smoothStream<TOOLS extends ToolSet>({ delayInMs = 10, chunking = 'word', _internal: { delay = originalDelay } = {}, }: { delayInMs?: number | null; chunking?: 'word' | 'line' | RegExp | ChunkDetector; /** * Internal. For test use only. May change without notice. */ _internal?: { delay?: (delayInMs: number | null) => Promise<void>; }; } = {}): (options: { tools: TOOLS; }) => TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>> { let detectChunk: ChunkDetector; if (typeof chunking === 'function') { detectChunk = buffer => { const match = chunking(buffer); if (match == null) { return null; } if (!match.length) { throw new Error(`Chunking function must return a non-empty string.`); } if (!buffer.startsWith(match)) { throw new Error( `Chunking function must return a match that is a prefix of the buffer. Received: "${match}" expected to start with "${buffer}"`, ); } return match; }; } else { const chunkingRegex = typeof chunking === 'string' ? CHUNKING_REGEXPS[chunking] : chunking; if (chunkingRegex == null) { throw new InvalidArgumentError({ argument: 'chunking', message: `Chunking must be "word" or "line" or a RegExp. Received: ${chunking}`, }); } detectChunk = buffer => { const match = chunkingRegex.exec(buffer); if (!match) { return null; } return buffer.slice(0, match.index) + match?.[0]; }; } return () => { let buffer = ''; let id = ''; return new TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>({ async transform(chunk, controller) { if (chunk.type !== 'text-delta') { if (buffer.length > 0) { controller.enqueue({ type: 'text-delta', text: buffer, id }); buffer = ''; } controller.enqueue(chunk); return; } if (chunk.id !== id && buffer.length > 0) { controller.enqueue({ type: 'text-delta', text: buffer, id }); buffer = ''; } buffer += chunk.text; id = chunk.id; let match; while ((match = detectChunk(buffer)) != null) { controller.enqueue({ type: 'text-delta', text: match, id }); buffer = buffer.slice(match.length); await delay(delayInMs); } }, }); }; } --- File: /ai/packages/ai/src/generate-text/step-result.ts --- import { ReasoningPart } from '@ai-sdk/provider-utils'; import { CallWarning, FinishReason, LanguageModelRequestMetadata, LanguageModelResponseMetadata, ProviderMetadata, } from '../types'; import { Source } from '../types/language-model'; import { LanguageModelUsage } from '../types/usage'; import { ContentPart } from './content-part'; import { GeneratedFile } from './generated-file'; import { ResponseMessage } from './response-message'; import { DynamicToolCall, StaticToolCall, TypedToolCall } from './tool-call'; import { DynamicToolResult, StaticToolResult, TypedToolResult, } from './tool-result'; import { ToolSet } from './tool-set'; /** * The result of a single step in the generation process. */ export type StepResult<TOOLS extends ToolSet> = { /** The content that was generated in the last step. */ readonly content: Array<ContentPart<TOOLS>>; /** The generated text. */ readonly text: string; /** The reasoning that was generated during the generation. */ readonly reasoning: Array<ReasoningPart>; /** The reasoning text that was generated during the generation. */ readonly reasoningText: string | undefined; /** The files that were generated during the generation. */ readonly files: Array<GeneratedFile>; /** The sources that were used to generate the text. */ readonly sources: Array<Source>; /** The tool calls that were made during the generation. */ readonly toolCalls: Array<TypedToolCall<TOOLS>>; /** The static tool calls that were made in the last step. */ readonly staticToolCalls: Array<StaticToolCall<TOOLS>>; /** The dynamic tool calls that were made in the last step. */ readonly dynamicToolCalls: Array<DynamicToolCall>; /** The results of the tool calls. */ readonly toolResults: Array<TypedToolResult<TOOLS>>; /** The static tool results that were made in the last step. */ readonly staticToolResults: Array<StaticToolResult<TOOLS>>; /** The dynamic tool results that were made in the last step. */ readonly dynamicToolResults: Array<DynamicToolResult>; /** The reason why the generation finished. */ readonly finishReason: FinishReason; /** The token usage of the generated text. */ readonly usage: LanguageModelUsage; /** Warnings from the model provider (e.g. unsupported settings). */ readonly warnings: CallWarning[] | undefined; /** Additional request information. */ readonly request: LanguageModelRequestMetadata; /** Additional response information. */ readonly response: LanguageModelResponseMetadata & { /** The response messages that were generated during the call. Response messages can be either assistant messages or tool messages. They contain a generated id. */ readonly messages: Array<ResponseMessage>; /** Response body (available only for providers that use HTTP requests). */ body?: unknown; }; /** Additional provider-specific metadata. They are passed through from the provider to the AI SDK and enable provider-specific results that can be fully encapsulated in the provider. */ readonly providerMetadata: ProviderMetadata | undefined; }; export class DefaultStepResult<TOOLS extends ToolSet> implements StepResult<TOOLS> { readonly content: StepResult<TOOLS>['content']; readonly finishReason: StepResult<TOOLS>['finishReason']; readonly usage: StepResult<TOOLS>['usage']; readonly warnings: StepResult<TOOLS>['warnings']; readonly request: StepResult<TOOLS>['request']; readonly response: StepResult<TOOLS>['response']; readonly providerMetadata: StepResult<TOOLS>['providerMetadata']; constructor({ content, finishReason, usage, warnings, request, response, providerMetadata, }: { content: StepResult<TOOLS>['content']; finishReason: StepResult<TOOLS>['finishReason']; usage: StepResult<TOOLS>['usage']; warnings: StepResult<TOOLS>['warnings']; request: StepResult<TOOLS>['request']; response: StepResult<TOOLS>['response']; providerMetadata: StepResult<TOOLS>['providerMetadata']; }) { this.content = content; this.finishReason = finishReason; this.usage = usage; this.warnings = warnings; this.request = request; this.response = response; this.providerMetadata = providerMetadata; } get text() { return this.content .filter(part => part.type === 'text') .map(part => part.text) .join(''); } get reasoning() { return this.content.filter(part => part.type === 'reasoning'); } get reasoningText() { return this.reasoning.length === 0 ? undefined : this.reasoning.map(part => part.text).join(''); } get files() { return this.content .filter(part => part.type === 'file') .map(part => part.file); } get sources() { return this.content.filter(part => part.type === 'source'); } get toolCalls() { return this.content.filter(part => part.type === 'tool-call'); } get staticToolCalls() { return this.toolCalls.filter( (toolCall): toolCall is StaticToolCall<TOOLS> => toolCall.dynamic === false, ); } get dynamicToolCalls() { return this.toolCalls.filter( (toolCall): toolCall is DynamicToolCall => toolCall.dynamic === true, ); } get toolResults() { return this.content.filter(part => part.type === 'tool-result'); } get staticToolResults() { return this.toolResults.filter( (toolResult): toolResult is StaticToolResult<TOOLS> => toolResult.dynamic === false, ); } get dynamicToolResults() { return this.toolResults.filter( (toolResult): toolResult is DynamicToolResult => toolResult.dynamic === true, ); } } --- File: /ai/packages/ai/src/generate-text/stop-condition.ts --- import { StepResult } from './step-result'; import { ToolSet } from './tool-set'; export type StopCondition<TOOLS extends ToolSet> = (options: { steps: Array<StepResult<TOOLS>>; }) => PromiseLike<boolean> | boolean; export function stepCountIs(stepCount: number): StopCondition<any> { return ({ steps }) => steps.length === stepCount; } export function hasToolCall(toolName: string): StopCondition<any> { return ({ steps }) => steps[steps.length - 1]?.toolCalls?.some( toolCall => toolCall.toolName === toolName, ) ?? false; } export async function isStopConditionMet<TOOLS extends ToolSet>({ stopConditions, steps, }: { stopConditions: Array<StopCondition<TOOLS>>; steps: Array<StepResult<TOOLS>>; }): Promise<boolean> { return ( await Promise.all(stopConditions.map(condition => condition({ steps }))) ).some(result => result); } --- File: /ai/packages/ai/src/generate-text/stream-text-result.ts --- import { IdGenerator, ReasoningPart } from '@ai-sdk/provider-utils'; import { ServerResponse } from 'node:http'; import { InferUIMessageChunk } from '../ui-message-stream/ui-message-chunks'; import { UIMessageStreamResponseInit } from '../ui-message-stream/ui-message-stream-response-init'; import { InferUIMessageMetadata, UIMessage } from '../ui/ui-messages'; import { AsyncIterableStream } from '../util/async-iterable-stream'; import { ErrorHandler } from '../util/error-handler'; import { CallWarning, FinishReason, LanguageModelRequestMetadata, ProviderMetadata, } from '../types'; import { Source } from '../types/language-model'; import { LanguageModelResponseMetadata } from '../types/language-model-response-metadata'; import { LanguageModelUsage } from '../types/usage'; import { UIMessageStreamOnFinishCallback } from '../ui-message-stream/ui-message-stream-on-finish-callback'; import { ContentPart } from './content-part'; import { GeneratedFile } from './generated-file'; import { ResponseMessage } from './response-message'; import { StepResult } from './step-result'; import { DynamicToolCall, StaticToolCall, TypedToolCall } from './tool-call'; import { TypedToolError } from './tool-error'; import { DynamicToolResult, StaticToolResult, TypedToolResult, } from './tool-result'; import { ToolSet } from './tool-set'; export type UIMessageStreamOptions<UI_MESSAGE extends UIMessage> = { /** * The original messages. If they are provided, persistence mode is assumed, * and a message ID is provided for the response message. */ originalMessages?: UI_MESSAGE[]; /** * Generate a message ID for the response message. * * If not provided, no message ID will be set for the response message (unless * the original messages are provided and the last message is an assistant message). */ generateMessageId?: IdGenerator; onFinish?: UIMessageStreamOnFinishCallback<UI_MESSAGE>; /** * Extracts message metadata that will be send to the client. * * Called on `start` and `finish` events. */ messageMetadata?: (options: { part: TextStreamPart<ToolSet>; }) => InferUIMessageMetadata<UI_MESSAGE> | undefined; /** * Send reasoning parts to the client. * Default to true. */ sendReasoning?: boolean; /** * Send source parts to the client. * Default to false. */ sendSources?: boolean; /** * Send the finish event to the client. * Set to false if you are using additional streamText calls * that send additional data. * Default to true. */ sendFinish?: boolean; /** * Send the message start event to the client. * Set to false if you are using additional streamText calls * and the message start event has already been sent. * Default to true. */ sendStart?: boolean; /** * Process an error, e.g. to log it. Default to `() => 'An error occurred.'`. * * @return error message to include in the data stream. */ onError?: (error: unknown) => string; }; export type ConsumeStreamOptions = { onError?: ErrorHandler; }; /** A result object for accessing different stream types and additional information. */ export interface StreamTextResult<TOOLS extends ToolSet, PARTIAL_OUTPUT> { /** The content that was generated in the last step. Resolved when the response is finished. */ readonly content: Promise<Array<ContentPart<TOOLS>>>; /** The full text that has been generated by the last step. Resolved when the response is finished. */ readonly text: Promise<string>; /** The full reasoning that the model has generated. Resolved when the response is finished. */ readonly reasoning: Promise<Array<ReasoningPart>>; /** The reasoning that has been generated by the last step. Resolved when the response is finished. */ readonly reasoningText: Promise<string | undefined>; /** Files that have been generated by the model in the last step. Resolved when the response is finished. */ readonly files: Promise<GeneratedFile[]>; /** Sources that have been used as references in the last step. Resolved when the response is finished. */ readonly sources: Promise<Source[]>; /** The tool calls that have been executed in the last step. Resolved when the response is finished. */ readonly toolCalls: Promise<TypedToolCall<TOOLS>[]>; /** The static tool calls that have been executed in the last step. Resolved when the response is finished. */ readonly staticToolCalls: Promise<StaticToolCall<TOOLS>[]>; /** The dynamic tool calls that have been executed in the last step. Resolved when the response is finished. */ readonly dynamicToolCalls: Promise<DynamicToolCall[]>; /** The static tool results that have been generated in the last step. Resolved when the response is finished. */ readonly staticToolResults: Promise<StaticToolResult<TOOLS>[]>; /** The dynamic tool results that have been generated in the last step. Resolved when the response is finished. */ readonly dynamicToolResults: Promise<DynamicToolResult[]>; /** The tool results that have been generated in the last step. Resolved when the all tool executions are finished. */ readonly toolResults: Promise<TypedToolResult<TOOLS>[]>; /** The reason why the generation finished. Taken from the last step. Resolved when the response is finished. */ readonly finishReason: Promise<FinishReason>; /** The token usage of the last step. Resolved when the response is finished. */ readonly usage: Promise<LanguageModelUsage>; /** The total token usage of the generated response. When there are multiple steps, the usage is the sum of all step usages. Resolved when the response is finished. */ readonly totalUsage: Promise<LanguageModelUsage>; /** Warnings from the model provider (e.g. unsupported settings) for the first step. */ readonly warnings: Promise<CallWarning[] | undefined>; /** Details for all steps. You can use this to get information about intermediate steps, such as the tool calls or the response headers. */ readonly steps: Promise<Array<StepResult<TOOLS>>>; /** Additional request information from the last step. */ readonly request: Promise<LanguageModelRequestMetadata>; /** Additional response information from the last step. */ readonly response: Promise< LanguageModelResponseMetadata & { /** The response messages that were generated during the call. It consists of an assistant message, potentially containing tool calls. When there are tool results, there is an additional tool message with the tool results that are available. If there are tools that do not have execute functions, they are not included in the tool results and need to be added separately. */ messages: Array<ResponseMessage>; } >; /** Additional provider-specific metadata from the last step. Metadata is passed through from the provider to the AI SDK and enables provider-specific results that can be fully encapsulated in the provider. */ readonly providerMetadata: Promise<ProviderMetadata | undefined>; /** A text stream that returns only the generated text deltas. You can use it as either an AsyncIterable or a ReadableStream. When an error occurs, the stream will throw the error. */ readonly textStream: AsyncIterableStream<string>; /** A stream with all events, including text deltas, tool calls, tool results, and errors. You can use it as either an AsyncIterable or a ReadableStream. Only errors that stop the stream, such as network errors, are thrown. */ readonly fullStream: AsyncIterableStream<TextStreamPart<TOOLS>>; /** A stream of partial outputs. It uses the `experimental_output` specification. */ readonly experimental_partialOutputStream: AsyncIterableStream<PARTIAL_OUTPUT>; /** Consumes the stream without processing the parts. This is useful to force the stream to finish. It effectively removes the backpressure and allows the stream to finish, triggering the `onFinish` callback and the promise resolution. If an error occurs, it is passed to the optional `onError` callback. */ consumeStream(options?: ConsumeStreamOptions): Promise<void>; /** Converts the result to a UI message stream. @param options.getErrorMessage an optional function that converts an error to an error message. @param options.sendUsage whether to send the usage information to the client. Defaults to true. @param options.sendReasoning whether to send the reasoning information to the client. Defaults to false. @param options.sendSources whether to send the sources information to the client. Defaults to false. @param options.experimental_sendFinish whether to send the finish information to the client. Defaults to true. @param options.experimental_sendStart whether to send the start information to the client. Defaults to true. @return A UI message stream. */ toUIMessageStream<UI_MESSAGE extends UIMessage>( options?: UIMessageStreamOptions<UI_MESSAGE>, ): AsyncIterableStream<InferUIMessageChunk<UI_MESSAGE>>; /** Writes UI message stream output to a Node.js response-like object. @param response A Node.js response-like object (ServerResponse). @param options.status The status code. @param options.statusText The status text. @param options.headers The headers. @param options.getErrorMessage An optional function that converts an error to an error message. @param options.sendUsage Whether to send the usage information to the client. Defaults to true. @param options.sendReasoning Whether to send the reasoning information to the client. Defaults to false. */ pipeUIMessageStreamToResponse<UI_MESSAGE extends UIMessage>( response: ServerResponse, options?: UIMessageStreamResponseInit & UIMessageStreamOptions<UI_MESSAGE>, ): void; /** Writes text delta output to a Node.js response-like object. It sets a `Content-Type` header to `text/plain; charset=utf-8` and writes each text delta as a separate chunk. @param response A Node.js response-like object (ServerResponse). @param init Optional headers, status code, and status text. */ pipeTextStreamToResponse(response: ServerResponse, init?: ResponseInit): void; /** Converts the result to a streamed response object with a stream data part stream. @param options.status The status code. @param options.statusText The status text. @param options.headers The headers. @param options.getErrorMessage An optional function that converts an error to an error message. @param options.sendUsage Whether to send the usage information to the client. Defaults to true. @param options.sendReasoning Whether to send the reasoning information to the client. Defaults to false. @return A response object. */ toUIMessageStreamResponse<UI_MESSAGE extends UIMessage>( options?: UIMessageStreamResponseInit & UIMessageStreamOptions<UI_MESSAGE>, ): Response; /** Creates a simple text stream response. Each text delta is encoded as UTF-8 and sent as a separate chunk. Non-text-delta events are ignored. @param init Optional headers, status code, and status text. */ toTextStreamResponse(init?: ResponseInit): Response; } export type TextStreamPart<TOOLS extends ToolSet> = | { type: 'text-start'; id: string; providerMetadata?: ProviderMetadata; } | { type: 'text-end'; id: string; providerMetadata?: ProviderMetadata; } | { type: 'text-delta'; id: string; providerMetadata?: ProviderMetadata; text: string; } | { type: 'reasoning-start'; id: string; providerMetadata?: ProviderMetadata; } | { type: 'reasoning-end'; id: string; providerMetadata?: ProviderMetadata; } | { type: 'reasoning-delta'; providerMetadata?: ProviderMetadata; id: string; text: string; } | { type: 'tool-input-start'; id: string; toolName: string; providerMetadata?: ProviderMetadata; providerExecuted?: boolean; dynamic?: boolean; } | { type: 'tool-input-end'; id: string; providerMetadata?: ProviderMetadata; } | { type: 'tool-input-delta'; id: string; delta: string; providerMetadata?: ProviderMetadata; } | ({ type: 'source' } & Source) | { type: 'file'; file: GeneratedFile } // different because of GeneratedFile object | ({ type: 'tool-call' } & TypedToolCall<TOOLS>) | ({ type: 'tool-result' } & TypedToolResult<TOOLS>) | ({ type: 'tool-error' } & TypedToolError<TOOLS>) | { type: 'start-step'; request: LanguageModelRequestMetadata; warnings: CallWarning[]; } | { type: 'finish-step'; response: LanguageModelResponseMetadata; usage: LanguageModelUsage; finishReason: FinishReason; providerMetadata: ProviderMetadata | undefined; } | { type: 'start'; } | { type: 'finish'; finishReason: FinishReason; totalUsage: LanguageModelUsage; } | { type: 'abort'; } | { type: 'error'; error: unknown; } | { type: 'raw'; rawValue: unknown; }; --- File: /ai/packages/ai/src/generate-text/stream-text.test.ts --- import { LanguageModelV2, LanguageModelV2CallOptions, LanguageModelV2CallWarning, LanguageModelV2FunctionTool, LanguageModelV2ProviderDefinedTool, LanguageModelV2StreamPart, SharedV2ProviderMetadata, } from '@ai-sdk/provider'; import { delay, dynamicTool, jsonSchema, ModelMessage, tool, Tool, } from '@ai-sdk/provider-utils'; import { convertArrayToReadableStream, convertAsyncIterableToArray, convertReadableStreamToArray, convertResponseStreamToArray, mockId, } from '@ai-sdk/provider-utils/test'; import assert from 'node:assert'; import { z } from 'zod/v4'; import { MockLanguageModelV2 } from '../test/mock-language-model-v2'; import { createMockServerResponse } from '../test/mock-server-response'; import { MockTracer } from '../test/mock-tracer'; import { mockValues } from '../test/mock-values'; import { object, text } from './output'; import { StepResult } from './step-result'; import { stepCountIs } from './stop-condition'; import { streamText } from './stream-text'; import { StreamTextResult, TextStreamPart } from './stream-text-result'; import { ToolSet } from './tool-set'; const defaultSettings = () => ({ prompt: 'prompt', experimental_generateMessageId: mockId({ prefix: 'msg' }), _internal: { generateId: mockId({ prefix: 'id' }), currentDate: () => new Date(0), }, onError: () => {}, }) as const; const testUsage = { inputTokens: 3, outputTokens: 10, totalTokens: 13, reasoningTokens: undefined, cachedInputTokens: undefined, }; const testUsage2 = { inputTokens: 3, outputTokens: 10, totalTokens: 23, reasoningTokens: 10, cachedInputTokens: 3, }; function createTestModel({ warnings = [], stream = convertArrayToReadableStream([ { type: 'stream-start', warnings, }, { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: `world!` }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, providerMetadata: { testProvider: { testKey: 'testValue' }, }, }, ]), request = undefined, response = undefined, }: { stream?: ReadableStream<LanguageModelV2StreamPart>; request?: { body: string }; response?: { headers: Record<string, string> }; warnings?: LanguageModelV2CallWarning[]; } = {}): LanguageModelV2 { return new MockLanguageModelV2({ doStream: async () => ({ stream, request, response, warnings }), }); } const modelWithSources = new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'source', sourceType: 'url', id: '123', url: 'https://example.com', title: 'Example', providerMetadata: { provider: { custom: 'value' } }, }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello!' }, { type: 'text-end', id: '1' }, { type: 'source', sourceType: 'url', id: '456', url: 'https://example.com/2', title: 'Example 2', providerMetadata: { provider: { custom: 'value2' } }, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }); const modelWithDocumentSources = new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'source', sourceType: 'document', id: 'doc-123', mediaType: 'application/pdf', title: 'Document Example', filename: 'example.pdf', providerMetadata: { provider: { custom: 'doc-value' } }, }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello from document!' }, { type: 'text-end', id: '1' }, { type: 'source', sourceType: 'document', id: 'doc-456', mediaType: 'text/plain', title: 'Text Document', providerMetadata: { provider: { custom: 'doc-value2' } }, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }); const modelWithFiles = new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'file', data: 'Hello World', mediaType: 'text/plain', }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello!' }, { type: 'text-end', id: '1' }, { type: 'file', data: 'QkFVRw==', mediaType: 'image/jpeg', }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }); const modelWithReasoning = new MockLanguageModelV2({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'reasoning-start', id: '1' }, { type: 'reasoning-delta', id: '1', delta: 'I will open the conversation', }, { type: 'reasoning-delta', id: '1', delta: ' with witty banter.', }, { type: 'reasoning-delta', id: '1', delta: '', providerMetadata: { testProvider: { signature: '1234567890' }, } as SharedV2ProviderMetadata, }, { type: 'reasoning-end', id: '1' }, { type: 'reasoning-start', id: '2', providerMetadata: { testProvider: { redactedData: 'redacted-reasoning-data' }, }, }, { type: 'reasoning-end', id: '2' }, { type: 'reasoning-start', id: '3' }, { type: 'reasoning-delta', id: '3', delta: ' Once the user has relaxed,', }, { type: 'reasoning-delta', id: '3', delta: ' I will pry for valuable information.', }, { type: 'reasoning-end', id: '3', providerMetadata: { testProvider: { signature: '1234567890' }, } as SharedV2ProviderMetadata, }, { type: 'reasoning-start', id: '4', providerMetadata: { testProvider: { signature: '1234567890' }, } as SharedV2ProviderMetadata, }, { type: 'reasoning-delta', id: '4', delta: ' I need to think about', }, { type: 'reasoning-delta', id: '4', delta: ' this problem carefully.', }, { type: 'reasoning-start', id: '5', providerMetadata: { testProvider: { signature: '1234567890' }, } as SharedV2ProviderMetadata, }, { type: 'reasoning-delta', id: '5', delta: ' The best solution', }, { type: 'reasoning-delta', id: '5', delta: ' requires careful', }, { type: 'reasoning-delta', id: '5', delta: ' consideration of all factors.', }, { type: 'reasoning-end', id: '4', providerMetadata: { testProvider: { signature: '0987654321' }, } as SharedV2ProviderMetadata, }, { type: 'reasoning-end', id: '5', providerMetadata: { testProvider: { signature: '0987654321' }, } as SharedV2ProviderMetadata, }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hi' }, { type: 'text-delta', id: '1', delta: ' there!' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), }); describe('streamText', () => { describe('result.textStream', () => { it('should send text deltas', async () => { const result = streamText({ model: new MockLanguageModelV2({ doStream: async ({ prompt }) => { expect(prompt).toStrictEqual([ { role: 'user', content: [{ type: 'text', text: 'test-input' }], providerOptions: undefined, }, ]); return { stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: `world!` }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }), prompt: 'test-input', }); expect( await convertAsyncIterableToArray(result.textStream), ).toStrictEqual(['Hello', ', ', 'world!']); }); it('should filter out empty text deltas', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: '' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: '' }, { type: 'text-delta', id: '1', delta: 'world!' }, { type: 'text-delta', id: '1', delta: '' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), prompt: 'test-input', }); expect( await convertAsyncIterableToArray(result.textStream), ).toMatchSnapshot(); }); it('should not include reasoning content in textStream', async () => { const result = streamText({ model: modelWithReasoning, ...defaultSettings(), }); expect( await convertAsyncIterableToArray(result.textStream), ).toMatchSnapshot(); }); it('should swallow error to prevent server crash', async () => { const result = streamText({ model: new MockLanguageModelV2({ doStream: async () => { throw new Error('test error'); }, }), prompt: 'test-input', onError: () => {}, }); expect( await convertAsyncIterableToArray(result.textStream), ).toMatchSnapshot(); }); }); describe('result.fullStream', () => { it('should send text deltas', async () => { const result = streamText({ model: new MockLanguageModelV2({ doStream: async ({ prompt }) => { expect(prompt).toStrictEqual([ { role: 'user', content: [{ type: 'text', text: 'test-input' }], providerOptions: undefined, }, ]); return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'response-id', modelId: 'response-model-id', timestamp: new Date(5000), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: `world!` }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }), prompt: 'test-input', }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hello", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": ", ", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": "world!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "response-id", "modelId": "response-model-id", "timestamp": 1970-01-01T00:00:05.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); it('should send reasoning deltas', async () => { const result = streamText({ model: modelWithReasoning, ...defaultSettings(), }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "reasoning-start", }, { "id": "1", "providerMetadata": undefined, "text": "I will open the conversation", "type": "reasoning-delta", }, { "id": "1", "providerMetadata": undefined, "text": " with witty banter.", "type": "reasoning-delta", }, { "id": "1", "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "text": "", "type": "reasoning-delta", }, { "id": "1", "type": "reasoning-end", }, { "id": "2", "providerMetadata": { "testProvider": { "redactedData": "redacted-reasoning-data", }, }, "type": "reasoning-start", }, { "id": "2", "type": "reasoning-end", }, { "id": "3", "type": "reasoning-start", }, { "id": "3", "providerMetadata": undefined, "text": " Once the user has relaxed,", "type": "reasoning-delta", }, { "id": "3", "providerMetadata": undefined, "text": " I will pry for valuable information.", "type": "reasoning-delta", }, { "id": "3", "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "type": "reasoning-end", }, { "id": "4", "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "type": "reasoning-start", }, { "id": "4", "providerMetadata": undefined, "text": " I need to think about", "type": "reasoning-delta", }, { "id": "4", "providerMetadata": undefined, "text": " this problem carefully.", "type": "reasoning-delta", }, { "id": "5", "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "type": "reasoning-start", }, { "id": "5", "providerMetadata": undefined, "text": " The best solution", "type": "reasoning-delta", }, { "id": "5", "providerMetadata": undefined, "text": " requires careful", "type": "reasoning-delta", }, { "id": "5", "providerMetadata": undefined, "text": " consideration of all factors.", "type": "reasoning-delta", }, { "id": "4", "providerMetadata": { "testProvider": { "signature": "0987654321", }, }, "type": "reasoning-end", }, { "id": "5", "providerMetadata": { "testProvider": { "signature": "0987654321", }, }, "type": "reasoning-end", }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hi", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": " there!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); it('should send sources', async () => { const result = streamText({ model: modelWithSources, ...defaultSettings(), }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "123", "providerMetadata": { "provider": { "custom": "value", }, }, "sourceType": "url", "title": "Example", "type": "source", "url": "https://example.com", }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hello!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "id": "456", "providerMetadata": { "provider": { "custom": "value2", }, }, "sourceType": "url", "title": "Example 2", "type": "source", "url": "https://example.com/2", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); it('should send files', async () => { const result = streamText({ model: modelWithFiles, ...defaultSettings(), }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "file": DefaultGeneratedFileWithType { "base64Data": "Hello World", "mediaType": "text/plain", "type": "file", "uint8ArrayData": undefined, }, "type": "file", }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hello!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "file": DefaultGeneratedFileWithType { "base64Data": "QkFVRw==", "mediaType": "image/jpeg", "type": "file", "uint8ArrayData": undefined, }, "type": "file", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); it('should use fallback response metadata when response metadata is not provided', async () => { const result = streamText({ model: new MockLanguageModelV2({ doStream: async ({ prompt }) => { expect(prompt).toStrictEqual([ { role: 'user', content: [{ type: 'text', text: 'test-input' }], providerOptions: undefined, }, ]); return { stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: `world!` }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }), prompt: 'test-input', _internal: { currentDate: mockValues(new Date(2000)), generateId: mockValues('id-2000'), }, }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hello", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": ", ", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": "world!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-2000", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:02.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); it('should send tool calls', async () => { const result = streamText({ model: new MockLanguageModelV2({ doStream: async ({ prompt, tools, toolChoice }) => { expect(tools).toStrictEqual([ { type: 'function', name: 'tool1', description: undefined, inputSchema: { $schema: 'http://json-schema.org/draft-07/schema#', additionalProperties: false, properties: { value: { type: 'string' } }, required: ['value'], type: 'object', }, providerOptions: undefined, }, ]); expect(toolChoice).toStrictEqual({ type: 'required' }); expect(prompt).toStrictEqual([ { role: 'user', content: [{ type: 'text', text: 'test-input' }], providerOptions: undefined, }, ]); return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, providerMetadata: { testProvider: { signature: 'sig', }, }, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }), tools: { tool1: tool({ inputSchema: z.object({ value: z.string() }), }), }, toolChoice: 'required', prompt: 'test-input', }); expect( await convertAsyncIterableToArray(result.fullStream), ).toMatchSnapshot(); }); it('should send tool call deltas', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-input-start', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', toolName: 'test-tool', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: '{"', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: 'value', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: '":"', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: 'Spark', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: 'le', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: ' Day', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: '"}', }, { type: 'tool-input-end', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', }, { type: 'tool-call', toolCallId: 'call_O17Uplv4lJvD6DVdIvFFeRMw', toolName: 'test-tool', input: '{"value":"Sparkle Day"}', }, { type: 'finish', finishReason: 'tool-calls', usage: testUsage2, }, ]), }), tools: { 'test-tool': tool({ inputSchema: z.object({ value: z.string() }), }), }, toolChoice: 'required', prompt: 'test-input', }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "dynamic": false, "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-input-start", }, { "delta": "{"", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "value", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "":"", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "Spark", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "le", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": " Day", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": ""}", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-end", }, { "input": { "value": "Sparkle Day", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "tool-calls", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": 3, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": 10, "totalTokens": 23, }, }, { "finishReason": "tool-calls", "totalUsage": { "cachedInputTokens": 3, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": 10, "totalTokens": 23, }, "type": "finish", }, ] `); }); it('should send tool results', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { tool1: tool({ inputSchema: z.object({ value: z.string() }), execute: async (input, options) => { expect(input).toStrictEqual({ value: 'value' }); expect(options.messages).toStrictEqual([ { role: 'user', content: 'test-input' }, ]); return `${input.value}-result`; }, }), }, prompt: 'test-input', }); expect( await convertAsyncIterableToArray(result.fullStream), ).toMatchSnapshot(); }); it('should send delayed asynchronous tool results', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => { await delay(50); // delay to show bug where step finish is sent before tool result return `${value}-result`; }, }, }, prompt: 'test-input', }); expect( await convertAsyncIterableToArray(result.fullStream), ).toMatchSnapshot(); }); it('should filter out empty text deltas', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: '' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: '' }, { type: 'text-delta', id: '1', delta: 'world!' }, { type: 'text-delta', id: '1', delta: '' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), prompt: 'test-input', }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hello", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": ", ", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": "world!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); }); describe('errors', () => { it('should forward error in doStream as error stream part', async () => { const result = streamText({ model: new MockLanguageModelV2({ doStream: async () => { throw new Error('test error'); }, }), prompt: 'test-input', onError: () => {}, }); expect( await convertAsyncIterableToArray(result.fullStream), ).toStrictEqual([ { type: 'start', }, { type: 'error', error: new Error('test error'), }, ]); }); it('should invoke onError callback when error is thrown', async () => { const onError = vi.fn(); const result = streamText({ model: new MockLanguageModelV2({ doStream: async () => { throw new Error('test error'); }, }), prompt: 'test-input', onError, }); await result.consumeStream(); expect(onError).toHaveBeenCalledWith({ error: new Error('test error'), }); }); it('should invoke onError callback when error is thrown in 2nd step', async () => { const onError = vi.fn(); let responseCount = 0; const result = streamText({ model: new MockLanguageModelV2({ doStream: async ({ prompt, tools, toolChoice }) => { if (responseCount++ === 0) { return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'tool-calls', usage: testUsage, }, ]), response: { headers: { call: '1' } }, }; } throw new Error('test error'); }, }), prompt: 'test-input', tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async () => 'result1', }, }, stopWhen: stepCountIs(3), onError, }); await result.consumeStream(); expect(onError).toHaveBeenCalledWith({ error: new Error('test error'), }); }); }); describe('result.pipeUIMessageStreamToResponse', async () => { it('should write data stream parts to a Node.js response-like object', async () => { const mockResponse = createMockServerResponse(); const result = streamText({ model: createTestModel(), prompt: 'test-input', _internal: { generateId: mockId({ prefix: 'id' }), }, }); result.pipeUIMessageStreamToResponse(mockResponse); await mockResponse.waitForEnd(); expect(mockResponse.statusCode).toBe(200); expect(mockResponse.headers).toMatchInlineSnapshot(` { "cache-control": "no-cache", "connection": "keep-alive", "content-type": "text/event-stream", "x-accel-buffering": "no", "x-vercel-ai-ui-message-stream": "v1", } `); expect(mockResponse.getDecodedChunks()).toMatchInlineSnapshot(` [ "data: {"type":"start"} ", "data: {"type":"start-step"} ", "data: {"type":"text-start","id":"1"} ", "data: {"type":"text-delta","id":"1","delta":"Hello"} ", "data: {"type":"text-delta","id":"1","delta":", "} ", "data: {"type":"text-delta","id":"1","delta":"world!"} ", "data: {"type":"text-end","id":"1"} ", "data: {"type":"finish-step"} ", "data: {"type":"finish"} ", "data: [DONE] ", ] `); }); it('should create a Response with a data stream and custom headers', async () => { const mockResponse = createMockServerResponse(); const result = streamText({ model: createTestModel(), prompt: 'test-input', _internal: { generateId: mockId({ prefix: 'id' }), }, }); result.pipeUIMessageStreamToResponse(mockResponse, { status: 201, statusText: 'foo', headers: { 'custom-header': 'custom-value', }, }); await mockResponse.waitForEnd(); expect(mockResponse.statusCode).toBe(201); expect(mockResponse.statusMessage).toBe('foo'); expect(mockResponse.headers).toMatchInlineSnapshot(` { "cache-control": "no-cache", "connection": "keep-alive", "content-type": "text/event-stream", "custom-header": "custom-value", "x-accel-buffering": "no", "x-vercel-ai-ui-message-stream": "v1", } `); expect(mockResponse.getDecodedChunks()).toMatchInlineSnapshot(` [ "data: {"type":"start"} ", "data: {"type":"start-step"} ", "data: {"type":"text-start","id":"1"} ", "data: {"type":"text-delta","id":"1","delta":"Hello"} ", "data: {"type":"text-delta","id":"1","delta":", "} ", "data: {"type":"text-delta","id":"1","delta":"world!"} ", "data: {"type":"text-end","id":"1"} ", "data: {"type":"finish-step"} ", "data: {"type":"finish"} ", "data: [DONE] ", ] `); }); it('should mask error messages by default', async () => { const mockResponse = createMockServerResponse(); const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'error', error: 'error' }, ]), }), prompt: 'test-input', _internal: { generateId: mockId({ prefix: 'id' }), }, onError: () => {}, }); result.pipeUIMessageStreamToResponse(mockResponse); await mockResponse.waitForEnd(); expect(mockResponse.getDecodedChunks()).toMatchSnapshot(); }); it('should support custom error messages', async () => { const mockResponse = createMockServerResponse(); const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'error', error: 'error' }, ]), }), prompt: 'test-input', _internal: { generateId: mockId({ prefix: 'id' }), }, onError: () => {}, }); result.pipeUIMessageStreamToResponse(mockResponse, { onError: error => `custom error message: ${error}`, }); await mockResponse.waitForEnd(); expect(mockResponse.getDecodedChunks()).toMatchSnapshot(); }); it('should omit message finish event (d:) when sendFinish is false', async () => { const mockResponse = createMockServerResponse(); const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello, World!' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), ...defaultSettings(), }); result.pipeUIMessageStreamToResponse(mockResponse, { sendFinish: false }); await mockResponse.waitForEnd(); expect(mockResponse.getDecodedChunks()).toMatchInlineSnapshot(` [ "data: {"type":"start"} ", "data: {"type":"start-step"} ", "data: {"type":"text-start","id":"1"} ", "data: {"type":"text-delta","id":"1","delta":"Hello, World!"} ", "data: {"type":"text-end","id":"1"} ", "data: {"type":"finish-step"} ", "data: [DONE] ", ] `); }); it('should write reasoning content to a Node.js response-like object', async () => { const mockResponse = createMockServerResponse(); const result = streamText({ model: modelWithReasoning, ...defaultSettings(), }); result.pipeUIMessageStreamToResponse(mockResponse, { sendReasoning: true, }); await mockResponse.waitForEnd(); expect(mockResponse.statusCode).toBe(200); expect(mockResponse.headers).toMatchInlineSnapshot(` { "cache-control": "no-cache", "connection": "keep-alive", "content-type": "text/event-stream", "x-accel-buffering": "no", "x-vercel-ai-ui-message-stream": "v1", } `); expect(mockResponse.getDecodedChunks()).toMatchInlineSnapshot(` [ "data: {"type":"start"} ", "data: {"type":"start-step"} ", "data: {"type":"reasoning-start","id":"1"} ", "data: {"type":"reasoning-delta","id":"1","delta":"I will open the conversation"} ", "data: {"type":"reasoning-delta","id":"1","delta":" with witty banter."} ", "data: {"type":"reasoning-delta","id":"1","delta":"","providerMetadata":{"testProvider":{"signature":"1234567890"}}} ", "data: {"type":"reasoning-end","id":"1"} ", "data: {"type":"reasoning-start","id":"2","providerMetadata":{"testProvider":{"redactedData":"redacted-reasoning-data"}}} ", "data: {"type":"reasoning-end","id":"2"} ", "data: {"type":"reasoning-start","id":"3"} ", "data: {"type":"reasoning-delta","id":"3","delta":" Once the user has relaxed,"} ", "data: {"type":"reasoning-delta","id":"3","delta":" I will pry for valuable information."} ", "data: {"type":"reasoning-end","id":"3","providerMetadata":{"testProvider":{"signature":"1234567890"}}} ", "data: {"type":"reasoning-start","id":"4","providerMetadata":{"testProvider":{"signature":"1234567890"}}} ", "data: {"type":"reasoning-delta","id":"4","delta":" I need to think about"} ", "data: {"type":"reasoning-delta","id":"4","delta":" this problem carefully."} ", "data: {"type":"reasoning-start","id":"5","providerMetadata":{"testProvider":{"signature":"1234567890"}}} ", "data: {"type":"reasoning-delta","id":"5","delta":" The best solution"} ", "data: {"type":"reasoning-delta","id":"5","delta":" requires careful"} ", "data: {"type":"reasoning-delta","id":"5","delta":" consideration of all factors."} ", "data: {"type":"reasoning-end","id":"4","providerMetadata":{"testProvider":{"signature":"0987654321"}}} ", "data: {"type":"reasoning-end","id":"5","providerMetadata":{"testProvider":{"signature":"0987654321"}}} ", "data: {"type":"text-start","id":"1"} ", "data: {"type":"text-delta","id":"1","delta":"Hi"} ", "data: {"type":"text-delta","id":"1","delta":" there!"} ", "data: {"type":"text-end","id":"1"} ", "data: {"type":"finish-step"} ", "data: {"type":"finish"} ", "data: [DONE] ", ] `); }); it('should write source content to a Node.js response-like object', async () => { const mockResponse = createMockServerResponse(); const result = streamText({ model: modelWithSources, ...defaultSettings(), }); result.pipeUIMessageStreamToResponse(mockResponse, { sendSources: true, }); await mockResponse.waitForEnd(); expect(mockResponse.statusCode).toBe(200); expect(mockResponse.headers).toMatchInlineSnapshot(` { "cache-control": "no-cache", "connection": "keep-alive", "content-type": "text/event-stream", "x-accel-buffering": "no", "x-vercel-ai-ui-message-stream": "v1", } `); expect(mockResponse.getDecodedChunks()).toMatchInlineSnapshot(` [ "data: {"type":"start"} ", "data: {"type":"start-step"} ", "data: {"type":"source-url","sourceId":"123","url":"https://example.com","title":"Example","providerMetadata":{"provider":{"custom":"value"}}} ", "data: {"type":"text-start","id":"1"} ", "data: {"type":"text-delta","id":"1","delta":"Hello!"} ", "data: {"type":"text-end","id":"1"} ", "data: {"type":"source-url","sourceId":"456","url":"https://example.com/2","title":"Example 2","providerMetadata":{"provider":{"custom":"value2"}}} ", "data: {"type":"finish-step"} ", "data: {"type":"finish"} ", "data: [DONE] ", ] `); }); it('should write file content to a Node.js response-like object', async () => { const mockResponse = createMockServerResponse(); const result = streamText({ model: modelWithFiles, ...defaultSettings(), }); result.pipeUIMessageStreamToResponse(mockResponse); await mockResponse.waitForEnd(); expect(mockResponse.statusCode).toBe(200); expect(mockResponse.headers).toMatchInlineSnapshot(` { "cache-control": "no-cache", "connection": "keep-alive", "content-type": "text/event-stream", "x-accel-buffering": "no", "x-vercel-ai-ui-message-stream": "v1", } `); expect(mockResponse.getDecodedChunks()).toMatchInlineSnapshot(` [ "data: {"type":"start"} ", "data: {"type":"start-step"} ", "data: {"type":"file","mediaType":"text/plain","url":"data:text/plain;base64,Hello World"} ", "data: {"type":"text-start","id":"1"} ", "data: {"type":"text-delta","id":"1","delta":"Hello!"} ", "data: {"type":"text-end","id":"1"} ", "data: {"type":"file","mediaType":"image/jpeg","url":"data:image/jpeg;base64,QkFVRw=="} ", "data: {"type":"finish-step"} ", "data: {"type":"finish"} ", "data: [DONE] ", ] `); }); }); describe('result.pipeTextStreamToResponse', async () => { it('should write text deltas to a Node.js response-like object', async () => { const mockResponse = createMockServerResponse(); const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: 'world!' }, { type: 'text-end', id: '1' }, ]), }), prompt: 'test-input', }); result.pipeTextStreamToResponse(mockResponse); await mockResponse.waitForEnd(); expect(mockResponse.statusCode).toBe(200); expect(mockResponse.headers).toMatchInlineSnapshot(` { "content-type": "text/plain; charset=utf-8", } `); expect(mockResponse.getDecodedChunks()).toEqual([ 'Hello', ', ', 'world!', ]); }); }); describe('result.toUIMessageStream', () => { it('should create a ui message stream', async () => { const result = streamText({ model: createTestModel(), ...defaultSettings(), }); const uiMessageStream = result.toUIMessageStream(); expect(await convertReadableStreamToArray(uiMessageStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "id": "1", "type": "text-start", }, { "delta": "Hello", "id": "1", "type": "text-delta", }, { "delta": ", ", "id": "1", "type": "text-delta", }, { "delta": "world!", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); it('should create a ui message stream with provider metadata', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'stream-start', warnings: [], }, { type: 'reasoning-start', id: 'r1', providerMetadata: { testProvider: { signature: 'r1' } }, }, { type: 'reasoning-delta', id: 'r1', delta: 'Hello', providerMetadata: { testProvider: { signature: 'r2' } }, }, { type: 'reasoning-delta', id: 'r1', delta: ', ', providerMetadata: { testProvider: { signature: 'r3' } }, }, { type: 'reasoning-end', id: 'r1', providerMetadata: { testProvider: { signature: 'r4' } }, }, { type: 'text-start', id: '1', providerMetadata: { testProvider: { signature: '1' } }, }, { type: 'text-delta', id: '1', delta: 'Hello', providerMetadata: { testProvider: { signature: '2' } }, }, { type: 'text-delta', id: '1', delta: ', ', providerMetadata: { testProvider: { signature: '3' } }, }, { type: 'text-delta', id: '1', delta: 'world!', providerMetadata: { testProvider: { signature: '4' } }, }, { type: 'text-end', id: '1', providerMetadata: { testProvider: { signature: '5' } }, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), ...defaultSettings(), }); const uiMessageStream = result.toUIMessageStream(); expect(await convertReadableStreamToArray(uiMessageStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "id": "r1", "providerMetadata": { "testProvider": { "signature": "r1", }, }, "type": "reasoning-start", }, { "delta": "Hello", "id": "r1", "providerMetadata": { "testProvider": { "signature": "r2", }, }, "type": "reasoning-delta", }, { "delta": ", ", "id": "r1", "providerMetadata": { "testProvider": { "signature": "r3", }, }, "type": "reasoning-delta", }, { "id": "r1", "providerMetadata": { "testProvider": { "signature": "r4", }, }, "type": "reasoning-end", }, { "id": "1", "providerMetadata": { "testProvider": { "signature": "1", }, }, "type": "text-start", }, { "delta": "Hello", "id": "1", "providerMetadata": { "testProvider": { "signature": "2", }, }, "type": "text-delta", }, { "delta": ", ", "id": "1", "providerMetadata": { "testProvider": { "signature": "3", }, }, "type": "text-delta", }, { "delta": "world!", "id": "1", "providerMetadata": { "testProvider": { "signature": "4", }, }, "type": "text-delta", }, { "id": "1", "providerMetadata": { "testProvider": { "signature": "5", }, }, "type": "text-end", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); it('should send tool call, tool call stream start, tool call deltas, and tool result stream parts', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'tool-input-start', id: 'call-1', toolName: 'tool1' }, { type: 'tool-input-delta', id: 'call-1', delta: '{ "value":' }, { type: 'tool-input-delta', id: 'call-1', delta: ' "value" }' }, { type: 'tool-input-end', id: 'call-1' }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => `${value}-result`, }, }, ...defaultSettings(), }); expect( await convertReadableStreamToArray(result.toUIMessageStream()), ).toMatchSnapshot(); }); it('should send message metadata as defined in the metadata function', async () => { const result = streamText({ model: createTestModel(), ...defaultSettings(), }); const uiMessageStream = result.toUIMessageStream({ messageMetadata: mockValues( { key1: 'value1' }, { key2: 'value2' }, { key3: 'value3' }, { key4: 'value4' }, { key5: 'value5' }, { key6: 'value6' }, { key7: 'value7' }, { key8: 'value8' }, ), }); expect(await convertReadableStreamToArray(uiMessageStream)) .toMatchInlineSnapshot(` [ { "messageMetadata": { "key1": "value1", }, "type": "start", }, { "type": "start-step", }, { "messageMetadata": { "key2": "value2", }, "type": "message-metadata", }, { "id": "1", "type": "text-start", }, { "messageMetadata": { "key3": "value3", }, "type": "message-metadata", }, { "delta": "Hello", "id": "1", "type": "text-delta", }, { "messageMetadata": { "key4": "value4", }, "type": "message-metadata", }, { "delta": ", ", "id": "1", "type": "text-delta", }, { "messageMetadata": { "key5": "value5", }, "type": "message-metadata", }, { "delta": "world!", "id": "1", "type": "text-delta", }, { "messageMetadata": { "key6": "value6", }, "type": "message-metadata", }, { "id": "1", "type": "text-end", }, { "messageMetadata": { "key7": "value7", }, "type": "message-metadata", }, { "type": "finish-step", }, { "messageMetadata": { "key8": "value8", }, "type": "message-metadata", }, { "messageMetadata": { "key8": "value8", }, "type": "finish", }, ] `); }); it('should mask error messages by default', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'error', error: 'error' }, ]), }), ...defaultSettings(), onError: () => {}, }); const uiMessageStream = result.toUIMessageStream(); expect( await convertReadableStreamToArray(uiMessageStream), ).toMatchSnapshot(); }); it('should support custom error messages', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'error', error: 'error' }, ]), }), ...defaultSettings(), onError: () => {}, }); const uiMessageStream = result.toUIMessageStream({ onError: error => `custom error message: ${error}`, }); expect( await convertReadableStreamToArray(uiMessageStream), ).toMatchSnapshot(); }); it('should omit message finish event when sendFinish is false', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'stream-start', warnings: [] }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello, World!' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), ...defaultSettings(), }); const uiMessageStream = result.toUIMessageStream({ sendFinish: false }); expect(await convertReadableStreamToArray(uiMessageStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "id": "1", "type": "text-start", }, { "delta": "Hello, World!", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "type": "finish-step", }, ] `); }); it('should omit message start event when sendStart is false', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'stream-start', warnings: [] }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello, World!' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), ...defaultSettings(), }); const uiMessageStream = result.toUIMessageStream({ sendStart: false }); expect(await convertReadableStreamToArray(uiMessageStream)) .toMatchInlineSnapshot(` [ { "type": "start-step", }, { "id": "1", "type": "text-start", }, { "delta": "Hello, World!", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); it('should send reasoning content when sendReasoning is true', async () => { const result = streamText({ model: modelWithReasoning, ...defaultSettings(), }); const uiMessageStream = result.toUIMessageStream({ sendReasoning: true }); expect(await convertReadableStreamToArray(uiMessageStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "id": "1", "type": "reasoning-start", }, { "delta": "I will open the conversation", "id": "1", "type": "reasoning-delta", }, { "delta": " with witty banter.", "id": "1", "type": "reasoning-delta", }, { "delta": "", "id": "1", "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "type": "reasoning-delta", }, { "id": "1", "type": "reasoning-end", }, { "id": "2", "providerMetadata": { "testProvider": { "redactedData": "redacted-reasoning-data", }, }, "type": "reasoning-start", }, { "id": "2", "type": "reasoning-end", }, { "id": "3", "type": "reasoning-start", }, { "delta": " Once the user has relaxed,", "id": "3", "type": "reasoning-delta", }, { "delta": " I will pry for valuable information.", "id": "3", "type": "reasoning-delta", }, { "id": "3", "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "type": "reasoning-end", }, { "id": "4", "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "type": "reasoning-start", }, { "delta": " I need to think about", "id": "4", "type": "reasoning-delta", }, { "delta": " this problem carefully.", "id": "4", "type": "reasoning-delta", }, { "id": "5", "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "type": "reasoning-start", }, { "delta": " The best solution", "id": "5", "type": "reasoning-delta", }, { "delta": " requires careful", "id": "5", "type": "reasoning-delta", }, { "delta": " consideration of all factors.", "id": "5", "type": "reasoning-delta", }, { "id": "4", "providerMetadata": { "testProvider": { "signature": "0987654321", }, }, "type": "reasoning-end", }, { "id": "5", "providerMetadata": { "testProvider": { "signature": "0987654321", }, }, "type": "reasoning-end", }, { "id": "1", "type": "text-start", }, { "delta": "Hi", "id": "1", "type": "text-delta", }, { "delta": " there!", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); it('should send source content when sendSources is true', async () => { const result = streamText({ model: modelWithSources, ...defaultSettings(), }); const uiMessageStream = result.toUIMessageStream({ sendSources: true }); expect(await convertReadableStreamToArray(uiMessageStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "providerMetadata": { "provider": { "custom": "value", }, }, "sourceId": "123", "title": "Example", "type": "source-url", "url": "https://example.com", }, { "id": "1", "type": "text-start", }, { "delta": "Hello!", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "providerMetadata": { "provider": { "custom": "value2", }, }, "sourceId": "456", "title": "Example 2", "type": "source-url", "url": "https://example.com/2", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); it('should send document source content when sendSources is true', async () => { const result = streamText({ model: modelWithDocumentSources, ...defaultSettings(), }); const uiMessageStream = result.toUIMessageStream({ sendSources: true }); expect(await convertReadableStreamToArray(uiMessageStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "filename": "example.pdf", "mediaType": "application/pdf", "providerMetadata": { "provider": { "custom": "doc-value", }, }, "sourceId": "doc-123", "title": "Document Example", "type": "source-document", }, { "id": "1", "type": "text-start", }, { "delta": "Hello from document!", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "filename": undefined, "mediaType": "text/plain", "providerMetadata": { "provider": { "custom": "doc-value2", }, }, "sourceId": "doc-456", "title": "Text Document", "type": "source-document", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); it('should send file content', async () => { const result = streamText({ model: modelWithFiles, ...defaultSettings(), }); const uiMessageStream = result.toUIMessageStream(); expect(await convertReadableStreamToArray(uiMessageStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "mediaType": "text/plain", "type": "file", "url": "data:text/plain;base64,Hello World", }, { "id": "1", "type": "text-start", }, { "delta": "Hello!", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "mediaType": "image/jpeg", "type": "file", "url": "data:image/jpeg;base64,QkFVRw==", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); it('should not generate a new message id when onFinish is provided and generateMessageId is not provided', async () => { const result = streamText({ model: createTestModel(), ...defaultSettings(), }); const uiMessageStream = result.toUIMessageStream({ onFinish: () => {}, // provided onFinish should trigger a new message id }); expect(await convertReadableStreamToArray(uiMessageStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "id": "1", "type": "text-start", }, { "delta": "Hello", "id": "1", "type": "text-delta", }, { "delta": ", ", "id": "1", "type": "text-delta", }, { "delta": "world!", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); it('should generate a new message id when generateMessageId is provided', async () => { const result = streamText({ model: createTestModel(), ...defaultSettings(), }); const uiMessageStream = result.toUIMessageStream({ generateMessageId: mockId({ prefix: 'message' }), }); expect(await convertReadableStreamToArray(uiMessageStream)) .toMatchInlineSnapshot(` [ { "messageId": "message-0", "type": "start", }, { "type": "start-step", }, { "id": "1", "type": "text-start", }, { "delta": "Hello", "id": "1", "type": "text-delta", }, { "delta": ", ", "id": "1", "type": "text-delta", }, { "delta": "world!", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); }); describe('result.toUIMessageStreamResponse', () => { it('should create a Response with a data stream', async () => { const result = streamText({ model: createTestModel(), prompt: 'test-input', _internal: { generateId: mockId({ prefix: 'id' }), }, }); const response = result.toUIMessageStreamResponse(); expect(response.status).toStrictEqual(200); expect(Object.fromEntries(response.headers.entries())) .toMatchInlineSnapshot(` { "cache-control": "no-cache", "connection": "keep-alive", "content-type": "text/event-stream", "x-accel-buffering": "no", "x-vercel-ai-ui-message-stream": "v1", } `); expect(await convertResponseStreamToArray(response)) .toMatchInlineSnapshot(` [ "data: {"type":"start"} ", "data: {"type":"start-step"} ", "data: {"type":"text-start","id":"1"} ", "data: {"type":"text-delta","id":"1","delta":"Hello"} ", "data: {"type":"text-delta","id":"1","delta":", "} ", "data: {"type":"text-delta","id":"1","delta":"world!"} ", "data: {"type":"text-end","id":"1"} ", "data: {"type":"finish-step"} ", "data: {"type":"finish"} ", "data: [DONE] ", ] `); }); it('should create a Response with a data stream and custom headers', async () => { const result = streamText({ model: createTestModel(), prompt: 'test-input', _internal: { generateId: mockId({ prefix: 'id' }), }, }); const response = result.toUIMessageStreamResponse({ status: 201, statusText: 'foo', headers: { 'custom-header': 'custom-value', }, }); expect(response.status).toStrictEqual(201); expect(response.statusText).toStrictEqual('foo'); expect(Object.fromEntries(response.headers.entries())) .toMatchInlineSnapshot(` { "cache-control": "no-cache", "connection": "keep-alive", "content-type": "text/event-stream", "custom-header": "custom-value", "x-accel-buffering": "no", "x-vercel-ai-ui-message-stream": "v1", } `); expect(await convertResponseStreamToArray(response)) .toMatchInlineSnapshot(` [ "data: {"type":"start"} ", "data: {"type":"start-step"} ", "data: {"type":"text-start","id":"1"} ", "data: {"type":"text-delta","id":"1","delta":"Hello"} ", "data: {"type":"text-delta","id":"1","delta":", "} ", "data: {"type":"text-delta","id":"1","delta":"world!"} ", "data: {"type":"text-end","id":"1"} ", "data: {"type":"finish-step"} ", "data: {"type":"finish"} ", "data: [DONE] ", ] `); }); it('should mask error messages by default', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'error', error: 'error' }, ]), }), prompt: 'test-input', _internal: { generateId: mockId({ prefix: 'id' }), }, onError: () => {}, }); const response = result.toUIMessageStreamResponse(); expect(await convertResponseStreamToArray(response)).toMatchSnapshot(); }); it('should support custom error messages', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'error', error: 'error' }, ]), }), prompt: 'test-input', _internal: { generateId: mockId({ prefix: 'id' }), }, onError: () => {}, }); const response = result.toUIMessageStreamResponse({ onError: error => `custom error message: ${error}`, }); expect(await convertResponseStreamToArray(response)).toMatchSnapshot(); }); }); describe('result.toTextStreamResponse', () => { it('should create a Response with a text stream', async () => { const result = streamText({ model: createTestModel(), prompt: 'test-input', }); const response = result.toTextStreamResponse(); expect(response.status).toStrictEqual(200); expect(Object.fromEntries(response.headers.entries())).toStrictEqual({ 'content-type': 'text/plain; charset=utf-8', }); expect(await convertResponseStreamToArray(response)).toStrictEqual([ 'Hello', ', ', 'world!', ]); }); }); describe('result.consumeStream', () => { it('should ignore AbortError during stream consumption', async () => { const result = streamText({ model: createTestModel({ stream: new ReadableStream({ start(controller) { controller.enqueue({ type: 'text-start', id: '1' }); controller.enqueue({ type: 'text-delta', id: '1', delta: 'Hello', }); queueMicrotask(() => { controller.error( Object.assign(new Error('Stream aborted'), { name: 'AbortError', }), ); }); }, }), }), prompt: 'test-input', }); await expect(result.consumeStream()).resolves.not.toThrow(); }); it('should ignore ResponseAborted error during stream consumption', async () => { const result = streamText({ model: createTestModel({ stream: new ReadableStream({ start(controller) { controller.enqueue({ type: 'text-start', id: '1' }); controller.enqueue({ type: 'text-delta', id: '1', delta: 'Hello', }); queueMicrotask(() => { controller.error( Object.assign(new Error('Response aborted'), { name: 'ResponseAborted', }), ); }); }, }), }), prompt: 'test-input', }); await expect(result.consumeStream()).resolves.not.toThrow(); }); it('should ignore any errors during stream consumption', async () => { const result = streamText({ model: createTestModel({ stream: new ReadableStream({ start(controller) { controller.enqueue({ type: 'text-start', id: '1' }); controller.enqueue({ type: 'text-delta', id: '1', delta: 'Hello', }); queueMicrotask(() => { controller.error(Object.assign(new Error('Some error'))); }); }, }), }), prompt: 'test-input', }); await expect(result.consumeStream()).resolves.not.toThrow(); }); it('should call the onError callback with the error', async () => { const onErrorCallback = vi.fn(); const result = streamText({ model: createTestModel({ stream: new ReadableStream({ start(controller) { controller.enqueue({ type: 'text-start', id: '1' }); controller.enqueue({ type: 'text-delta', id: '1', delta: 'Hello', }); queueMicrotask(() => { controller.error(Object.assign(new Error('Some error'))); }); }, }), }), prompt: 'test-input', }); await expect( result.consumeStream({ onError: onErrorCallback }), ).resolves.not.toThrow(); expect(onErrorCallback).toHaveBeenCalledWith(new Error('Some error')); }); }); describe('multiple stream consumption', () => { it('should support text stream, ai stream, full stream on single result object', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: 'world!' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), prompt: 'test-input', _internal: { generateId: mockId({ prefix: 'id' }), }, }); expect({ textStream: await convertAsyncIterableToArray(result.textStream), fullStream: await convertAsyncIterableToArray(result.fullStream), uiMessageStream: await convertReadableStreamToArray( result.toUIMessageStream(), ), }).toMatchInlineSnapshot(` { "fullStream": [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hello", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": ", ", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": "world!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ], "textStream": [ "Hello", ", ", "world!", ], "uiMessageStream": [ { "type": "start", }, { "type": "start-step", }, { "id": "1", "type": "text-start", }, { "delta": "Hello", "id": "1", "type": "text-delta", }, { "delta": ", ", "id": "1", "type": "text-delta", }, { "delta": "world!", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "type": "finish-step", }, { "type": "finish", }, ], } `); }); }); describe('result.warnings', () => { it('should resolve with warnings', async () => { const result = streamText({ model: createTestModel({ warnings: [{ type: 'other', message: 'test-warning' }], }), prompt: 'test-input', }); result.consumeStream(); expect(await result.warnings).toStrictEqual([ { type: 'other', message: 'test-warning' }, ]); }); }); describe('result.usage', () => { it('should resolve with token usage', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), prompt: 'test-input', }); result.consumeStream(); expect(await result.usage).toMatchInlineSnapshot(` { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, } `); }); }); describe('result.finishReason', () => { it('should resolve with finish reason', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), prompt: 'test-input', }); result.consumeStream(); expect(await result.finishReason).toStrictEqual('stop'); }); }); describe('result.providerMetadata', () => { it('should resolve with provider metadata', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, providerMetadata: { testProvider: { testKey: 'testValue' }, }, }, ]), }), prompt: 'test-input', }); result.consumeStream(); expect(await result.providerMetadata).toStrictEqual({ testProvider: { testKey: 'testValue' }, }); }); }); describe('result.response.messages', () => { it('should contain reasoning', async () => { const result = streamText({ model: modelWithReasoning, ...defaultSettings(), }); result.consumeStream(); expect((await result.response).messages).toMatchInlineSnapshot(` [ { "content": [ { "providerOptions": { "testProvider": { "signature": "1234567890", }, }, "text": "I will open the conversation with witty banter.", "type": "reasoning", }, { "providerOptions": { "testProvider": { "redactedData": "redacted-reasoning-data", }, }, "text": "", "type": "reasoning", }, { "providerOptions": { "testProvider": { "signature": "1234567890", }, }, "text": " Once the user has relaxed, I will pry for valuable information.", "type": "reasoning", }, { "providerOptions": { "testProvider": { "signature": "0987654321", }, }, "text": " I need to think about this problem carefully.", "type": "reasoning", }, { "providerOptions": { "testProvider": { "signature": "0987654321", }, }, "text": " The best solution requires careful consideration of all factors.", "type": "reasoning", }, { "providerOptions": undefined, "text": "Hi there!", "type": "text", }, ], "role": "assistant", }, ] `); }); }); describe('result.request', () => { it('should resolve with response information', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), request: { body: 'test body' }, }), prompt: 'test-input', }); result.consumeStream(); expect(await result.request).toStrictEqual({ body: 'test body', }); }); }); describe('result.response', () => { it('should resolve with response information', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), response: { headers: { call: '2' } }, }), ...defaultSettings(), }); result.consumeStream(); expect(await result.response).toMatchInlineSnapshot(` { "headers": { "call": "2", }, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "Hello", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, } `); }); }); describe('result.text', () => { it('should resolve with full text', async () => { const result = streamText({ model: createTestModel(), ...defaultSettings(), }); result.consumeStream(); expect(await result.text).toMatchSnapshot(); }); }); describe('result.reasoningText', () => { it('should contain reasoning text from model response', async () => { const result = streamText({ model: modelWithReasoning, ...defaultSettings(), }); result.consumeStream(); expect(await result.reasoningText).toMatchSnapshot(); }); }); describe('result.reasoning', () => { it('should contain reasoning from model response', async () => { const result = streamText({ model: modelWithReasoning, ...defaultSettings(), }); result.consumeStream(); expect(await result.reasoning).toMatchSnapshot(); }); }); describe('result.sources', () => { it('should contain sources', async () => { const result = streamText({ model: modelWithSources, ...defaultSettings(), }); result.consumeStream(); expect(await result.sources).toMatchSnapshot(); }); }); describe('result.files', () => { it('should contain files', async () => { const result = streamText({ model: modelWithFiles, ...defaultSettings(), }); result.consumeStream(); expect(await result.files).toMatchSnapshot(); }); }); describe('result.steps', () => { it('should add the reasoning from the model response to the step result', async () => { const result = streamText({ model: modelWithReasoning, ...defaultSettings(), }); result.consumeStream(); expect(await result.steps).toMatchInlineSnapshot(` [ DefaultStepResult { "content": [ { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "text": "I will open the conversation with witty banter.", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "redactedData": "redacted-reasoning-data", }, }, "text": "", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "text": " Once the user has relaxed, I will pry for valuable information.", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "signature": "0987654321", }, }, "text": " I need to think about this problem carefully.", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "signature": "0987654321", }, }, "text": " The best solution requires careful consideration of all factors.", "type": "reasoning", }, { "providerMetadata": undefined, "text": "Hi there!", "type": "text", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": undefined, "id": "id-0", "messages": [ { "content": [ { "providerOptions": { "testProvider": { "signature": "1234567890", }, }, "text": "I will open the conversation with witty banter.", "type": "reasoning", }, { "providerOptions": { "testProvider": { "redactedData": "redacted-reasoning-data", }, }, "text": "", "type": "reasoning", }, { "providerOptions": { "testProvider": { "signature": "1234567890", }, }, "text": " Once the user has relaxed, I will pry for valuable information.", "type": "reasoning", }, { "providerOptions": { "testProvider": { "signature": "0987654321", }, }, "text": " I need to think about this problem carefully.", "type": "reasoning", }, { "providerOptions": { "testProvider": { "signature": "0987654321", }, }, "text": " The best solution requires careful consideration of all factors.", "type": "reasoning", }, { "providerOptions": undefined, "text": "Hi there!", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, ] `); }); it('should add the sources from the model response to the step result', async () => { const result = streamText({ model: modelWithSources, ...defaultSettings(), }); result.consumeStream(); expect(await result.steps).toMatchInlineSnapshot(` [ DefaultStepResult { "content": [ { "id": "123", "providerMetadata": { "provider": { "custom": "value", }, }, "sourceType": "url", "title": "Example", "type": "source", "url": "https://example.com", }, { "providerMetadata": undefined, "text": "Hello!", "type": "text", }, { "id": "456", "providerMetadata": { "provider": { "custom": "value2", }, }, "sourceType": "url", "title": "Example 2", "type": "source", "url": "https://example.com/2", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": undefined, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "Hello!", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, ] `); }); it('should add the files from the model response to the step result', async () => { const result = streamText({ model: modelWithFiles, ...defaultSettings(), }); result.consumeStream(); expect(await result.steps).toMatchInlineSnapshot(` [ DefaultStepResult { "content": [ { "file": DefaultGeneratedFileWithType { "base64Data": "Hello World", "mediaType": "text/plain", "type": "file", "uint8ArrayData": undefined, }, "type": "file", }, { "providerMetadata": undefined, "text": "Hello!", "type": "text", }, { "file": DefaultGeneratedFileWithType { "base64Data": "QkFVRw==", "mediaType": "image/jpeg", "type": "file", "uint8ArrayData": undefined, }, "type": "file", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": undefined, "id": "id-0", "messages": [ { "content": [ { "data": "Hello World", "mediaType": "text/plain", "providerOptions": undefined, "type": "file", }, { "providerOptions": undefined, "text": "Hello!", "type": "text", }, { "data": "QkFVRw==", "mediaType": "image/jpeg", "providerOptions": undefined, "type": "file", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, ] `); }); }); describe('result.toolCalls', () => { it('should resolve with tool calls', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { tool1: tool({ inputSchema: z.object({ value: z.string() }), }), }, prompt: 'test-input', }); result.consumeStream(); expect(await result.toolCalls).toMatchInlineSnapshot(` [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ] `); }); }); describe('result.toolResults', () => { it('should resolve with tool results', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => `${value}-result`, }, }, prompt: 'test-input', }); result.consumeStream(); expect(await result.toolResults).toMatchInlineSnapshot(` [ { "input": { "value": "value", }, "output": "value-result", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ] `); }); }); describe('options.onChunk', () => { let result: Array< Extract< TextStreamPart<any>, { type: | 'text-delta' | 'reasoning-delta' | 'source' | 'tool-call' | 'tool-input-start' | 'tool-input-delta' | 'tool-result' | 'raw'; } > >; beforeEach(async () => { result = []; const resultObject = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-end', id: '1' }, { type: 'tool-input-start', id: '2', toolName: 'tool1' }, { type: 'tool-input-delta', id: '2', delta: '{"value": "' }, { type: 'reasoning-start', id: '3' }, { type: 'reasoning-delta', id: '3', delta: 'Feeling clever' }, { type: 'reasoning-end', id: '3' }, { type: 'tool-input-delta', id: '2', delta: 'test' }, { type: 'tool-input-delta', id: '2', delta: '"}' }, { type: 'source', sourceType: 'url', id: '123', url: 'https://example.com', title: 'Example', providerMetadata: { provider: { custom: 'value' } }, }, { type: 'tool-input-end', id: '2' }, { type: 'tool-call', toolCallId: '2', toolName: 'tool1', input: `{ "value": "test" }`, providerMetadata: { provider: { custom: 'value' } }, }, { type: 'text-start', id: '4' }, { type: 'text-delta', id: '4', delta: ' World' }, { type: 'text-end', id: '4' }, { type: 'finish', finishReason: 'stop', usage: testUsage2, }, ]), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => `${value}-result`, }, }, prompt: 'test-input', onChunk(event) { result.push(event.chunk); }, }); await resultObject.consumeStream(); }); it('should return events in order', async () => { expect(result).toMatchInlineSnapshot(` [ { "id": "1", "providerMetadata": undefined, "text": "Hello", "type": "text-delta", }, { "dynamic": false, "id": "2", "toolName": "tool1", "type": "tool-input-start", }, { "delta": "{"value": "", "id": "2", "type": "tool-input-delta", }, { "id": "3", "providerMetadata": undefined, "text": "Feeling clever", "type": "reasoning-delta", }, { "delta": "test", "id": "2", "type": "tool-input-delta", }, { "delta": ""}", "id": "2", "type": "tool-input-delta", }, { "id": "123", "providerMetadata": { "provider": { "custom": "value", }, }, "sourceType": "url", "title": "Example", "type": "source", "url": "https://example.com", }, { "input": { "value": "test", }, "providerExecuted": undefined, "providerMetadata": { "provider": { "custom": "value", }, }, "toolCallId": "2", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "test", }, "output": "test-result", "providerExecuted": undefined, "providerMetadata": { "provider": { "custom": "value", }, }, "toolCallId": "2", "toolName": "tool1", "type": "tool-result", }, { "id": "4", "providerMetadata": undefined, "text": " World", "type": "text-delta", }, ] `); }); }); describe('options.onError', () => { it('should invoke onError', async () => { const result: Array<{ error: unknown }> = []; const resultObject = streamText({ model: new MockLanguageModelV2({ doStream: async () => { throw new Error('test error'); }, }), prompt: 'test-input', onError(event) { result.push(event); }, }); await resultObject.consumeStream(); expect(result).toStrictEqual([{ error: new Error('test error') }]); }); }); describe('options.onFinish', () => { it('should send correct information', async () => { let result!: Parameters< Required<Parameters<typeof streamText>[0]>['onFinish'] >[0]; const resultObject = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'text-delta', id: '1', delta: `world!` }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, providerMetadata: { testProvider: { testKey: 'testValue' }, }, }, ]), response: { headers: { call: '2' } }, }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => `${value}-result`, }, }, onFinish: async event => { result = event as unknown as typeof result; }, ...defaultSettings(), }); await resultObject.consumeStream(); expect(result).toMatchInlineSnapshot(` { "content": [ { "providerMetadata": undefined, "text": "Hello, world!", "type": "text", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "value", }, "output": "value-result", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "dynamicToolCalls": [], "dynamicToolResults": [], "files": [], "finishReason": "stop", "providerMetadata": { "testProvider": { "testKey": "testValue", }, }, "reasoning": [], "reasoningText": undefined, "request": {}, "response": { "headers": { "call": "2", }, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "value-result", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "sources": [], "staticToolCalls": [], "staticToolResults": [], "steps": [ DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "Hello, world!", "type": "text", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "value", }, "output": "value-result", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "stop", "providerMetadata": { "testProvider": { "testKey": "testValue", }, }, "request": {}, "response": { "headers": { "call": "2", }, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "value-result", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, ], "text": "Hello, world!", "toolCalls": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "toolResults": [ { "input": { "value": "value", }, "output": "value-result", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], } `); }); it('should send sources', async () => { let result!: Parameters< Required<Parameters<typeof streamText>[0]>['onFinish'] >[0]; const resultObject = streamText({ model: modelWithSources, onFinish: async event => { result = event as unknown as typeof result; }, ...defaultSettings(), }); await resultObject.consumeStream(); expect(result).toMatchInlineSnapshot(` { "content": [ { "id": "123", "providerMetadata": { "provider": { "custom": "value", }, }, "sourceType": "url", "title": "Example", "type": "source", "url": "https://example.com", }, { "providerMetadata": undefined, "text": "Hello!", "type": "text", }, { "id": "456", "providerMetadata": { "provider": { "custom": "value2", }, }, "sourceType": "url", "title": "Example 2", "type": "source", "url": "https://example.com/2", }, ], "dynamicToolCalls": [], "dynamicToolResults": [], "files": [], "finishReason": "stop", "providerMetadata": undefined, "reasoning": [], "reasoningText": undefined, "request": {}, "response": { "headers": undefined, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "Hello!", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "sources": [ { "id": "123", "providerMetadata": { "provider": { "custom": "value", }, }, "sourceType": "url", "title": "Example", "type": "source", "url": "https://example.com", }, { "id": "456", "providerMetadata": { "provider": { "custom": "value2", }, }, "sourceType": "url", "title": "Example 2", "type": "source", "url": "https://example.com/2", }, ], "staticToolCalls": [], "staticToolResults": [], "steps": [ DefaultStepResult { "content": [ { "id": "123", "providerMetadata": { "provider": { "custom": "value", }, }, "sourceType": "url", "title": "Example", "type": "source", "url": "https://example.com", }, { "providerMetadata": undefined, "text": "Hello!", "type": "text", }, { "id": "456", "providerMetadata": { "provider": { "custom": "value2", }, }, "sourceType": "url", "title": "Example 2", "type": "source", "url": "https://example.com/2", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": undefined, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "Hello!", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, ], "text": "Hello!", "toolCalls": [], "toolResults": [], "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], } `); }); it('should send files', async () => { let result!: Parameters< Required<Parameters<typeof streamText>[0]>['onFinish'] >[0]; const resultObject = streamText({ model: modelWithFiles, onFinish: async event => { result = event as unknown as typeof result; }, ...defaultSettings(), }); await resultObject.consumeStream(); expect(result).toMatchInlineSnapshot(` { "content": [ { "file": DefaultGeneratedFileWithType { "base64Data": "Hello World", "mediaType": "text/plain", "type": "file", "uint8ArrayData": undefined, }, "type": "file", }, { "providerMetadata": undefined, "text": "Hello!", "type": "text", }, { "file": DefaultGeneratedFileWithType { "base64Data": "QkFVRw==", "mediaType": "image/jpeg", "type": "file", "uint8ArrayData": undefined, }, "type": "file", }, ], "dynamicToolCalls": [], "dynamicToolResults": [], "files": [ DefaultGeneratedFileWithType { "base64Data": "Hello World", "mediaType": "text/plain", "type": "file", "uint8ArrayData": undefined, }, DefaultGeneratedFileWithType { "base64Data": "QkFVRw==", "mediaType": "image/jpeg", "type": "file", "uint8ArrayData": undefined, }, ], "finishReason": "stop", "providerMetadata": undefined, "reasoning": [], "reasoningText": undefined, "request": {}, "response": { "headers": undefined, "id": "id-0", "messages": [ { "content": [ { "data": "Hello World", "mediaType": "text/plain", "providerOptions": undefined, "type": "file", }, { "providerOptions": undefined, "text": "Hello!", "type": "text", }, { "data": "QkFVRw==", "mediaType": "image/jpeg", "providerOptions": undefined, "type": "file", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "sources": [], "staticToolCalls": [], "staticToolResults": [], "steps": [ DefaultStepResult { "content": [ { "file": DefaultGeneratedFileWithType { "base64Data": "Hello World", "mediaType": "text/plain", "type": "file", "uint8ArrayData": undefined, }, "type": "file", }, { "providerMetadata": undefined, "text": "Hello!", "type": "text", }, { "file": DefaultGeneratedFileWithType { "base64Data": "QkFVRw==", "mediaType": "image/jpeg", "type": "file", "uint8ArrayData": undefined, }, "type": "file", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": undefined, "id": "id-0", "messages": [ { "content": [ { "data": "Hello World", "mediaType": "text/plain", "providerOptions": undefined, "type": "file", }, { "providerOptions": undefined, "text": "Hello!", "type": "text", }, { "data": "QkFVRw==", "mediaType": "image/jpeg", "providerOptions": undefined, "type": "file", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, ], "text": "Hello!", "toolCalls": [], "toolResults": [], "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], } `); }); it('should not prevent error from being forwarded', async () => { const result = streamText({ model: new MockLanguageModelV2({ doStream: async () => { throw new Error('test error'); }, }), prompt: 'test-input', onFinish() {}, // just defined; do nothing onError: () => {}, }); expect( await convertAsyncIterableToArray(result.fullStream), ).toStrictEqual([ { type: 'start', }, { type: 'error', error: new Error('test error'), }, ]); }); }); describe('result.responseMessages', () => { it('should contain assistant response message when there are no tool calls', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello, ' }, { type: 'text-delta', id: '1', delta: 'world!' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), prompt: 'test-input', }); result.consumeStream(); expect((await result.response).messages).toMatchInlineSnapshot(` [ { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, ] `); }); it('should contain assistant response message and tool message when there are tool calls with results', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello, ' }, { type: 'text-delta', id: '1', delta: 'world!' }, { type: 'text-end', id: '1' }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async () => 'result1', }, }, prompt: 'test-input', }); result.consumeStream(); expect((await result.response).messages).toMatchInlineSnapshot(` [ { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ] `); }); }); describe('options.stopWhen', () => { let result: StreamTextResult<any, any>; let onFinishResult: Parameters< Required<Parameters<typeof streamText>[0]>['onFinish'] >[0]; let onStepFinishResults: StepResult<any>[]; let tracer: MockTracer; let stepInputs: Array<any>; beforeEach(() => { tracer = new MockTracer(); stepInputs = []; }); describe('2 steps: initial, tool-result', () => { beforeEach(async () => { result = undefined as any; onFinishResult = undefined as any; onStepFinishResults = []; let responseCount = 0; result = streamText({ model: new MockLanguageModelV2({ doStream: async ({ prompt, tools, toolChoice }) => { stepInputs.push({ prompt, tools, toolChoice }); switch (responseCount++) { case 0: { return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'reasoning-start', id: '0' }, { type: 'reasoning-delta', id: '0', delta: 'thinking' }, { type: 'reasoning-end', id: '0' }, { type: 'tool-call', id: 'call-1', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'tool-calls', usage: testUsage, }, ]), response: { headers: { call: '1' } }, }; } case 1: { return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-1', modelId: 'mock-model-id', timestamp: new Date(1000), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello, ' }, { type: 'text-delta', id: '1', delta: `world!` }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage2, }, ]), response: { headers: { call: '2' } }, }; } default: throw new Error( `Unexpected response count: ${responseCount}`, ); } }, }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async () => 'result1', }, }, prompt: 'test-input', onFinish: async event => { expect(onFinishResult).to.be.undefined; onFinishResult = event as unknown as typeof onFinishResult; }, onStepFinish: async event => { onStepFinishResults.push(event); }, experimental_telemetry: { isEnabled: true, tracer }, stopWhen: stepCountIs(3), _internal: { now: mockValues(0, 100, 500, 600, 1000), generateId: mockId({ prefix: 'id' }), }, }); }); it('should contain correct step inputs', async () => { await result.consumeStream(); expect(stepInputs).toMatchInlineSnapshot(` [ { "prompt": [ { "content": [ { "text": "test-input", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, ], "toolChoice": { "type": "auto", }, "tools": [ { "description": undefined, "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "value": { "type": "string", }, }, "required": [ "value", ], "type": "object", }, "name": "tool1", "providerOptions": undefined, "type": "function", }, ], }, { "prompt": [ { "content": [ { "text": "test-input", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "providerOptions": undefined, "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "providerOptions": undefined, "role": "tool", }, ], "toolChoice": { "type": "auto", }, "tools": [ { "description": undefined, "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "value": { "type": "string", }, }, "required": [ "value", ], "type": "object", }, "name": "tool1", "providerOptions": undefined, "type": "function", }, ], }, ] `); }); it('should contain assistant response message and tool message from all steps', async () => { expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "0", "type": "reasoning-start", }, { "id": "0", "providerMetadata": undefined, "text": "thinking", "type": "reasoning-delta", }, { "id": "0", "type": "reasoning-end", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "value", }, "output": "result1", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, { "finishReason": "tool-calls", "providerMetadata": undefined, "response": { "headers": { "call": "1", }, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hello, ", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": "world!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": { "call": "2", }, "id": "id-1", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:01.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": 3, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": 10, "totalTokens": 23, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": 3, "inputTokens": 6, "outputTokens": 20, "reasoningTokens": 10, "totalTokens": 36, }, "type": "finish", }, ] `); }); describe('callbacks', () => { beforeEach(async () => { await result.consumeStream(); }); it('onFinish should send correct information', async () => { expect(onFinishResult).toMatchInlineSnapshot(` { "content": [ { "providerMetadata": undefined, "text": "Hello, world!", "type": "text", }, ], "dynamicToolCalls": [], "dynamicToolResults": [], "files": [], "finishReason": "stop", "providerMetadata": undefined, "reasoning": [], "reasoningText": undefined, "request": {}, "response": { "headers": { "call": "2", }, "id": "id-1", "messages": [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:01.000Z, }, "sources": [], "staticToolCalls": [], "staticToolResults": [], "steps": [ DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "value", }, "output": "result1", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "tool-calls", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "1", }, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "Hello, world!", "type": "text", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "2", }, "id": "id-1", "messages": [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:01.000Z, }, "usage": { "cachedInputTokens": 3, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": 10, "totalTokens": 23, }, "warnings": [], }, ], "text": "Hello, world!", "toolCalls": [], "toolResults": [], "totalUsage": { "cachedInputTokens": 3, "inputTokens": 6, "outputTokens": 20, "reasoningTokens": 10, "totalTokens": 36, }, "usage": { "cachedInputTokens": 3, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": 10, "totalTokens": 23, }, "warnings": [], } `); }); it('onStepFinish should send correct information', async () => { expect(onStepFinishResults).toMatchInlineSnapshot(` [ DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "value", }, "output": "result1", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "tool-calls", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "1", }, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "Hello, world!", "type": "text", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "2", }, "id": "id-1", "messages": [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:01.000Z, }, "usage": { "cachedInputTokens": 3, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": 10, "totalTokens": 23, }, "warnings": [], }, ] `); }); }); describe('value promises', () => { beforeEach(async () => { await result.consumeStream(); }); it('result.totalUsage should contain total token usage', async () => { expect(await result.totalUsage).toMatchInlineSnapshot(` { "cachedInputTokens": 3, "inputTokens": 6, "outputTokens": 20, "reasoningTokens": 10, "totalTokens": 36, } `); }); it('result.usage should contain token usage from final step', async () => { expect(await result.totalUsage).toMatchInlineSnapshot(` { "cachedInputTokens": 3, "inputTokens": 6, "outputTokens": 20, "reasoningTokens": 10, "totalTokens": 36, } `); }); it('result.finishReason should contain finish reason from final step', async () => { assert.strictEqual(await result.finishReason, 'stop'); }); it('result.text should contain text from final step', async () => { assert.strictEqual(await result.text, 'Hello, world!'); }); it('result.steps should contain all steps', async () => { expect(await result.steps).toMatchInlineSnapshot(` [ DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "value", }, "output": "result1", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "tool-calls", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "1", }, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "Hello, world!", "type": "text", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "2", }, "id": "id-1", "messages": [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:01.000Z, }, "usage": { "cachedInputTokens": 3, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": 10, "totalTokens": 23, }, "warnings": [], }, ] `); }); it('result.response.messages should contain response messages from all steps', async () => { expect((await result.response).messages).toMatchInlineSnapshot(` [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, ] `); }); }); it('should record telemetry data for each step', async () => { await result.consumeStream(); expect(tracer.jsonSpans).toMatchSnapshot(); }); it('should have correct ui message stream', async () => { expect(await convertReadableStreamToArray(result.toUIMessageStream())) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "id": "0", "type": "reasoning-start", }, { "delta": "thinking", "id": "0", "type": "reasoning-delta", }, { "id": "0", "type": "reasoning-end", }, { "input": { "value": "value", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-input-available", }, { "output": "result1", "toolCallId": "call-1", "type": "tool-output-available", }, { "type": "finish-step", }, { "type": "start-step", }, { "id": "1", "type": "text-start", }, { "delta": "Hello, ", "id": "1", "type": "text-delta", }, { "delta": "world!", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); }); describe('2 steps: initial, tool-result with prepareStep', () => { let result: StreamTextResult<any, any>; let doStreamCalls: Array<LanguageModelV2CallOptions>; let prepareStepCalls: Array<{ stepNumber: number; steps: Array<StepResult<any>>; messages: Array<ModelMessage>; }>; beforeEach(async () => { doStreamCalls = []; prepareStepCalls = []; result = streamText({ model: new MockLanguageModelV2({ doStream: async options => { doStreamCalls.push(options); switch (doStreamCalls.length) { case 1: return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'tool-calls', usage: testUsage, }, ]), response: { headers: { call: '1' } }, }; case 2: return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-1', modelId: 'mock-model-id', timestamp: new Date(1000), }, { type: 'text-start', id: '2' }, { type: 'text-delta', id: '2', delta: 'Hello, ' }, { type: 'text-delta', id: '2', delta: `world!` }, { type: 'text-end', id: '2' }, { type: 'finish', finishReason: 'stop', usage: testUsage2, }, ]), response: { headers: { call: '2' } }, }; default: throw new Error( `Unexpected response count: ${doStreamCalls.length}`, ); } }, }), tools: { tool1: tool({ inputSchema: z.object({ value: z.string() }), execute: async () => 'result1', }), }, prompt: 'test-input', stopWhen: stepCountIs(3), prepareStep: async ({ model, stepNumber, steps, messages }) => { prepareStepCalls.push({ stepNumber, steps, messages }); if (stepNumber === 0) { return { toolChoice: { type: 'tool', toolName: 'tool1' as const, }, system: 'system-message-0', messages: [ { role: 'user', content: 'new input from prepareStep', }, ], }; } if (stepNumber === 1) { return { activeTools: [], system: 'system-message-1', }; } }, }); }); it('should contain all doStream calls', async () => { await result.consumeStream(); expect(doStreamCalls).toMatchInlineSnapshot(` [ { "abortSignal": undefined, "frequencyPenalty": undefined, "headers": undefined, "includeRawChunks": false, "maxOutputTokens": undefined, "presencePenalty": undefined, "prompt": [ { "content": "system-message-0", "role": "system", }, { "content": [ { "text": "new input from prepareStep", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, ], "providerOptions": undefined, "responseFormat": undefined, "seed": undefined, "stopSequences": undefined, "temperature": undefined, "toolChoice": { "toolName": "tool1", "type": "tool", }, "tools": [ { "description": undefined, "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "value": { "type": "string", }, }, "required": [ "value", ], "type": "object", }, "name": "tool1", "providerOptions": undefined, "type": "function", }, ], "topK": undefined, "topP": undefined, }, { "abortSignal": undefined, "frequencyPenalty": undefined, "headers": undefined, "includeRawChunks": false, "maxOutputTokens": undefined, "presencePenalty": undefined, "prompt": [ { "content": "system-message-1", "role": "system", }, { "content": [ { "text": "test-input", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "providerOptions": undefined, "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "providerOptions": undefined, "role": "tool", }, ], "providerOptions": undefined, "responseFormat": undefined, "seed": undefined, "stopSequences": undefined, "temperature": undefined, "toolChoice": { "type": "auto", }, "tools": [], "topK": undefined, "topP": undefined, }, ] `); }); it('should contain all prepareStep calls', async () => { await result.consumeStream(); expect(prepareStepCalls).toMatchInlineSnapshot(` [ { "messages": [ { "content": "test-input", "role": "user", }, ], "stepNumber": 0, "steps": [ DefaultStepResult { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "value", }, "output": "result1", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "tool-calls", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "1", }, "id": "id-0", "messages": [ { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "Hello, world!", "type": "text", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "2", }, "id": "id-1", "messages": [ { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:01.000Z, }, "usage": { "cachedInputTokens": 3, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": 10, "totalTokens": 23, }, "warnings": [], }, ], }, { "messages": [ { "content": "test-input", "role": "user", }, { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "stepNumber": 1, "steps": [ DefaultStepResult { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "value", }, "output": "result1", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "tool-calls", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "1", }, "id": "id-0", "messages": [ { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "Hello, world!", "type": "text", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "2", }, "id": "id-1", "messages": [ { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:01.000Z, }, "usage": { "cachedInputTokens": 3, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": 10, "totalTokens": 23, }, "warnings": [], }, ], }, ] `); }); }); describe('2 steps: initial, tool-result with transformed tool results', () => { const upperCaseToolResultTransform = () => new TransformStream< TextStreamPart<{ tool1: Tool<{ value: string }, string> }>, TextStreamPart<{ tool1: Tool<{ value: string }, string> }> >({ transform(chunk, controller) { if (chunk.type === 'tool-result' && !chunk.dynamic) { chunk.output = chunk.output.toUpperCase(); chunk.input = { ...chunk.input, value: chunk.input.value.toUpperCase(), }; } controller.enqueue(chunk); }, }); beforeEach(async () => { result = undefined as any; onFinishResult = undefined as any; onStepFinishResults = []; let responseCount = 0; result = streamText({ model: new MockLanguageModelV2({ doStream: async ({ prompt, tools, toolChoice }) => { switch (responseCount++) { case 0: { return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'reasoning-start', id: 'id-0' }, { type: 'reasoning-delta', id: 'id-0', delta: 'thinking', }, { type: 'reasoning-end', id: 'id-0' }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'tool-calls', usage: testUsage, }, ]), response: { headers: { call: '1' } }, }; } case 1: { return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-1', modelId: 'mock-model-id', timestamp: new Date(1000), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello, ' }, { type: 'text-delta', id: '1', delta: `world!` }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage2, }, ]), response: { headers: { call: '2' } }, }; } default: throw new Error( `Unexpected response count: ${responseCount}`, ); } }, }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async () => 'result1', }, }, experimental_transform: upperCaseToolResultTransform, prompt: 'test-input', onFinish: async event => { expect(onFinishResult).to.be.undefined; onFinishResult = event as unknown as typeof onFinishResult; }, onStepFinish: async event => { onStepFinishResults.push(event); }, experimental_telemetry: { isEnabled: true, tracer }, stopWhen: stepCountIs(3), _internal: { now: mockValues(0, 100, 500, 600, 1000), generateId: mockId({ prefix: 'id' }), }, }); }); it('should contain assistant response message and tool message from all steps', async () => { expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "id-0", "type": "reasoning-start", }, { "id": "id-0", "providerMetadata": undefined, "text": "thinking", "type": "reasoning-delta", }, { "id": "id-0", "type": "reasoning-end", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "VALUE", }, "output": "RESULT1", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, { "finishReason": "tool-calls", "providerMetadata": undefined, "response": { "headers": { "call": "1", }, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hello, ", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": "world!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": { "call": "2", }, "id": "id-1", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:01.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": 3, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": 10, "totalTokens": 23, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": 3, "inputTokens": 6, "outputTokens": 20, "reasoningTokens": 10, "totalTokens": 36, }, "type": "finish", }, ] `); }); describe('callbacks', () => { beforeEach(async () => { await result.consumeStream(); }); it('onFinish should send correct information', async () => { expect(onFinishResult).toMatchInlineSnapshot(` { "content": [ { "providerMetadata": undefined, "text": "Hello, world!", "type": "text", }, ], "dynamicToolCalls": [], "dynamicToolResults": [], "files": [], "finishReason": "stop", "providerMetadata": undefined, "reasoning": [], "reasoningText": undefined, "request": {}, "response": { "headers": { "call": "2", }, "id": "id-1", "messages": [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "RESULT1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:01.000Z, }, "sources": [], "staticToolCalls": [], "staticToolResults": [], "steps": [ DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "VALUE", }, "output": "RESULT1", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "tool-calls", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "1", }, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "RESULT1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "Hello, world!", "type": "text", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "2", }, "id": "id-1", "messages": [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "RESULT1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:01.000Z, }, "usage": { "cachedInputTokens": 3, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": 10, "totalTokens": 23, }, "warnings": [], }, ], "text": "Hello, world!", "toolCalls": [], "toolResults": [], "totalUsage": { "cachedInputTokens": 3, "inputTokens": 6, "outputTokens": 20, "reasoningTokens": 10, "totalTokens": 36, }, "usage": { "cachedInputTokens": 3, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": 10, "totalTokens": 23, }, "warnings": [], } `); }); it('onStepFinish should send correct information', async () => { expect(onStepFinishResults).toMatchInlineSnapshot(` [ DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "VALUE", }, "output": "RESULT1", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "tool-calls", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "1", }, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "RESULT1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "Hello, world!", "type": "text", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "2", }, "id": "id-1", "messages": [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "RESULT1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:01.000Z, }, "usage": { "cachedInputTokens": 3, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": 10, "totalTokens": 23, }, "warnings": [], }, ] `); }); }); describe('value promises', () => { beforeEach(async () => { await result.consumeStream(); }); it('result.totalUsage should contain total token usage', async () => { expect(await result.totalUsage).toMatchInlineSnapshot(` { "cachedInputTokens": 3, "inputTokens": 6, "outputTokens": 20, "reasoningTokens": 10, "totalTokens": 36, } `); }); it('result.usage should contain token usage from final step', async () => { expect(await result.totalUsage).toMatchInlineSnapshot(` { "cachedInputTokens": 3, "inputTokens": 6, "outputTokens": 20, "reasoningTokens": 10, "totalTokens": 36, } `); }); it('result.finishReason should contain finish reason from final step', async () => { assert.strictEqual(await result.finishReason, 'stop'); }); it('result.text should contain text from final step', async () => { assert.strictEqual(await result.text, 'Hello, world!'); }); it('result.steps should contain all steps', async () => { expect(await result.steps).toMatchInlineSnapshot(` [ DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "VALUE", }, "output": "RESULT1", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "tool-calls", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "1", }, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "RESULT1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "Hello, world!", "type": "text", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "2", }, "id": "id-1", "messages": [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "RESULT1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:01.000Z, }, "usage": { "cachedInputTokens": 3, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": 10, "totalTokens": 23, }, "warnings": [], }, ] `); }); it('result.response.messages should contain response messages from all steps', async () => { expect((await result.response).messages).toMatchInlineSnapshot(` [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "RESULT1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, { "content": [ { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, ] `); }); }); it('should record telemetry data for each step', async () => { await result.consumeStream(); expect(tracer.jsonSpans).toMatchInlineSnapshot(` [ { "attributes": { "ai.model.id": "mock-model-id", "ai.model.provider": "mock-provider", "ai.operationId": "ai.streamText", "ai.prompt": "{"prompt":"test-input"}", "ai.response.finishReason": "stop", "ai.response.text": "Hello, world!", "ai.settings.maxRetries": 2, "ai.usage.cachedInputTokens": 3, "ai.usage.inputTokens": 6, "ai.usage.outputTokens": 20, "ai.usage.reasoningTokens": 10, "ai.usage.totalTokens": 36, "operation.name": "ai.streamText", }, "events": [], "name": "ai.streamText", }, { "attributes": { "ai.model.id": "mock-model-id", "ai.model.provider": "mock-provider", "ai.operationId": "ai.streamText.doStream", "ai.prompt.messages": "[{"role":"user","content":[{"type":"text","text":"test-input"}]}]", "ai.prompt.toolChoice": "{"type":"auto"}", "ai.prompt.tools": [ "{"type":"function","name":"tool1","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"value":{"type":"string"}},"required":["value"],"additionalProperties":false}}", ], "ai.response.avgOutputTokensPerSecond": 20, "ai.response.finishReason": "tool-calls", "ai.response.id": "id-0", "ai.response.model": "mock-model-id", "ai.response.msToFinish": 500, "ai.response.msToFirstChunk": 100, "ai.response.text": "", "ai.response.timestamp": "1970-01-01T00:00:00.000Z", "ai.response.toolCalls": "[{"type":"tool-call","toolCallId":"call-1","toolName":"tool1","input":{"value":"value"}}]", "ai.settings.maxRetries": 2, "ai.usage.inputTokens": 3, "ai.usage.outputTokens": 10, "ai.usage.totalTokens": 13, "gen_ai.request.model": "mock-model-id", "gen_ai.response.finish_reasons": [ "tool-calls", ], "gen_ai.response.id": "id-0", "gen_ai.response.model": "mock-model-id", "gen_ai.system": "mock-provider", "gen_ai.usage.input_tokens": 3, "gen_ai.usage.output_tokens": 10, "operation.name": "ai.streamText.doStream", }, "events": [ { "attributes": { "ai.response.msToFirstChunk": 100, }, "name": "ai.stream.firstChunk", }, { "attributes": undefined, "name": "ai.stream.finish", }, ], "name": "ai.streamText.doStream", }, { "attributes": { "ai.operationId": "ai.toolCall", "ai.toolCall.args": "{"value":"value"}", "ai.toolCall.id": "call-1", "ai.toolCall.name": "tool1", "ai.toolCall.result": ""result1"", "operation.name": "ai.toolCall", }, "events": [], "name": "ai.toolCall", }, { "attributes": { "ai.model.id": "mock-model-id", "ai.model.provider": "mock-provider", "ai.operationId": "ai.streamText.doStream", "ai.prompt.messages": "[{"role":"user","content":[{"type":"text","text":"test-input"}]},{"role":"assistant","content":[{"type":"reasoning","text":"thinking"},{"type":"tool-call","toolCallId":"call-1","toolName":"tool1","input":{"value":"value"}}]},{"role":"tool","content":[{"type":"tool-result","toolCallId":"call-1","toolName":"tool1","output":{"type":"text","value":"RESULT1"}}]}]", "ai.prompt.toolChoice": "{"type":"auto"}", "ai.prompt.tools": [ "{"type":"function","name":"tool1","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"value":{"type":"string"}},"required":["value"],"additionalProperties":false}}", ], "ai.response.avgOutputTokensPerSecond": 25, "ai.response.finishReason": "stop", "ai.response.id": "id-1", "ai.response.model": "mock-model-id", "ai.response.msToFinish": 400, "ai.response.msToFirstChunk": 400, "ai.response.text": "Hello, world!", "ai.response.timestamp": "1970-01-01T00:00:01.000Z", "ai.settings.maxRetries": 2, "ai.usage.cachedInputTokens": 3, "ai.usage.inputTokens": 3, "ai.usage.outputTokens": 10, "ai.usage.reasoningTokens": 10, "ai.usage.totalTokens": 23, "gen_ai.request.model": "mock-model-id", "gen_ai.response.finish_reasons": [ "stop", ], "gen_ai.response.id": "id-1", "gen_ai.response.model": "mock-model-id", "gen_ai.system": "mock-provider", "gen_ai.usage.input_tokens": 3, "gen_ai.usage.output_tokens": 10, "operation.name": "ai.streamText.doStream", }, "events": [ { "attributes": { "ai.response.msToFirstChunk": 400, }, "name": "ai.stream.firstChunk", }, { "attributes": undefined, "name": "ai.stream.finish", }, ], "name": "ai.streamText.doStream", }, ] `); }); it('should have correct ui message stream', async () => { expect(await convertReadableStreamToArray(result.toUIMessageStream())) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "id": "id-0", "type": "reasoning-start", }, { "delta": "thinking", "id": "id-0", "type": "reasoning-delta", }, { "id": "id-0", "type": "reasoning-end", }, { "input": { "value": "value", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-input-available", }, { "output": "RESULT1", "toolCallId": "call-1", "type": "tool-output-available", }, { "type": "finish-step", }, { "type": "start-step", }, { "id": "1", "type": "text-start", }, { "delta": "Hello, ", "id": "1", "type": "text-delta", }, { "delta": "world!", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); }); describe('2 stop conditions', () => { let stopConditionCalls: Array<{ number: number; steps: StepResult<any>[]; }>; beforeEach(async () => { stopConditionCalls = []; let responseCount = 0; result = streamText({ model: new MockLanguageModelV2({ doStream: async () => { switch (responseCount++) { case 0: { return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'reasoning-start', id: 'id-0', }, { type: 'reasoning-delta', id: 'id-0', delta: 'thinking', }, { type: 'reasoning-end', id: 'id-0', }, { type: 'tool-call', id: 'call-1', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'tool-calls', usage: testUsage, }, ]), response: { headers: { call: '1' } }, }; } default: throw new Error( `Unexpected response count: ${responseCount}`, ); } }, }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async () => 'result1', }, }, prompt: 'test-input', experimental_telemetry: { isEnabled: true, tracer }, stopWhen: [ ({ steps }) => { stopConditionCalls.push({ number: 0, steps }); return false; }, ({ steps }) => { stopConditionCalls.push({ number: 1, steps }); return true; }, ], _internal: { now: mockValues(0, 100, 500, 600, 1000), }, }); }); it('result.steps should contain a single step', async () => { await result.consumeStream(); expect((await result.steps).length).toStrictEqual(1); }); it('stopConditionCalls should be called for each stop condition', async () => { await result.consumeStream(); expect(stopConditionCalls).toMatchInlineSnapshot(` [ { "number": 0, "steps": [ DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "value", }, "output": "result1", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "tool-calls", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "1", }, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, ], }, { "number": 1, "steps": [ DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "value", }, "output": "result1", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "tool-calls", "providerMetadata": undefined, "request": {}, "response": { "headers": { "call": "1", }, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "thinking", "type": "reasoning", }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, ], }, ] `); }); }); }); describe('options.headers', () => { it('should set headers', async () => { const result = streamText({ model: new MockLanguageModelV2({ doStream: async ({ headers }) => { expect(headers).toStrictEqual({ 'custom-request-header': 'request-header-value', }); return { stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: `world!` }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }), prompt: 'test-input', headers: { 'custom-request-header': 'request-header-value' }, }); assert.deepStrictEqual( await convertAsyncIterableToArray(result.textStream), ['Hello', ', ', 'world!'], ); }); }); describe('provider-executed tools', () => { describe('single provider-executed tool call and result', () => { let result: StreamTextResult<any, any>; beforeEach(async () => { result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'tool-input-start', id: 'call-1', toolName: 'web_search', providerExecuted: true, }, { type: 'tool-input-delta', id: 'call-1', delta: '{ "value": "value" }', }, { type: 'tool-input-end', id: 'call-1', }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'web_search', input: `{ "value": "value" }`, providerExecuted: true, }, { type: 'tool-result', toolCallId: 'call-1', toolName: 'web_search', result: `{ "value": "result1" }`, providerExecuted: true, }, { type: 'tool-call', toolCallId: 'call-2', toolName: 'web_search', input: `{ "value": "value" }`, providerExecuted: true, }, { type: 'tool-result', toolCallId: 'call-2', toolName: 'web_search', result: `ERROR`, isError: true, providerExecuted: true, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { web_search: { type: 'provider-defined', id: 'test.web_search', name: 'web_search', inputSchema: z.object({ value: z.string() }), outputSchema: z.object({ value: z.string() }), args: {}, }, }, ...defaultSettings(), stopWhen: stepCountIs(4), }); }); it('should only execute a single step', async () => { await result.consumeStream(); expect((await result.steps).length).toBe(1); }); it('should include provider-executed tool call and result content', async () => { await result.consumeStream(); expect(await result.content).toMatchInlineSnapshot(` [ { "input": { "value": "value", }, "providerExecuted": true, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "web_search", "type": "tool-call", }, { "input": { "value": "value", }, "output": "{ "value": "result1" }", "providerExecuted": true, "toolCallId": "call-1", "toolName": "web_search", "type": "tool-result", }, { "input": { "value": "value", }, "providerExecuted": true, "providerMetadata": undefined, "toolCallId": "call-2", "toolName": "web_search", "type": "tool-call", }, { "error": "ERROR", "input": { "value": "value", }, "providerExecuted": true, "toolCallId": "call-2", "toolName": "web_search", "type": "tool-error", }, ] `); }); it('should include provider-executed tool call and result in the full stream', async () => { expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "dynamic": false, "id": "call-1", "providerExecuted": true, "toolName": "web_search", "type": "tool-input-start", }, { "delta": "{ "value": "value" }", "id": "call-1", "type": "tool-input-delta", }, { "id": "call-1", "type": "tool-input-end", }, { "input": { "value": "value", }, "providerExecuted": true, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "web_search", "type": "tool-call", }, { "input": { "value": "value", }, "output": "{ "value": "result1" }", "providerExecuted": true, "toolCallId": "call-1", "toolName": "web_search", "type": "tool-result", }, { "input": { "value": "value", }, "providerExecuted": true, "providerMetadata": undefined, "toolCallId": "call-2", "toolName": "web_search", "type": "tool-call", }, { "error": "ERROR", "input": { "value": "value", }, "providerExecuted": true, "toolCallId": "call-2", "toolName": "web_search", "type": "tool-error", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); it('should include provider-executed tool call and result in the ui message stream', async () => { expect(await convertReadableStreamToArray(result.toUIMessageStream())) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "providerExecuted": true, "toolCallId": "call-1", "toolName": "web_search", "type": "tool-input-start", }, { "inputTextDelta": "{ "value": "value" }", "toolCallId": "call-1", "type": "tool-input-delta", }, { "input": { "value": "value", }, "providerExecuted": true, "toolCallId": "call-1", "toolName": "web_search", "type": "tool-input-available", }, { "output": "{ "value": "result1" }", "providerExecuted": true, "toolCallId": "call-1", "type": "tool-output-available", }, { "input": { "value": "value", }, "providerExecuted": true, "toolCallId": "call-2", "toolName": "web_search", "type": "tool-input-available", }, { "errorText": "ERROR", "providerExecuted": true, "toolCallId": "call-2", "type": "tool-output-error", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); }); }); describe('dynamic tools', () => { describe('single dynamic tool call and result', () => { let result: StreamTextResult<any, any>; beforeEach(async () => { result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'tool-input-start', id: 'call-1', toolName: 'dynamicTool', }, { type: 'tool-input-delta', id: 'call-1', delta: '{ "value": "value" }', }, { type: 'tool-input-end', id: 'call-1', }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'dynamicTool', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'tool-calls', usage: testUsage, }, ]), }), tools: { dynamicTool: dynamicTool({ inputSchema: z.object({ value: z.string() }), execute: async () => { return { value: 'test-result' }; }, }), }, ...defaultSettings(), }); }); it('should include dynamic tool call and result content', async () => { await result.consumeStream(); expect(await result.content).toMatchInlineSnapshot(` [ { "dynamic": true, "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "dynamicTool", "type": "tool-call", }, { "dynamic": true, "input": { "value": "value", }, "output": { "value": "test-result", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "dynamicTool", "type": "tool-result", }, ] `); }); it('should include dynamic tool call and result in the full stream', async () => { expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "dynamic": true, "id": "call-1", "toolName": "dynamicTool", "type": "tool-input-start", }, { "delta": "{ "value": "value" }", "id": "call-1", "type": "tool-input-delta", }, { "id": "call-1", "type": "tool-input-end", }, { "dynamic": true, "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "dynamicTool", "type": "tool-call", }, { "dynamic": true, "input": { "value": "value", }, "output": { "value": "test-result", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "dynamicTool", "type": "tool-result", }, { "finishReason": "tool-calls", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, { "finishReason": "tool-calls", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); it('should include dynamic tool call and result in the ui message stream', async () => { expect(await convertReadableStreamToArray(result.toUIMessageStream())) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "dynamic": true, "toolCallId": "call-1", "toolName": "dynamicTool", "type": "tool-input-start", }, { "inputTextDelta": "{ "value": "value" }", "toolCallId": "call-1", "type": "tool-input-delta", }, { "dynamic": true, "input": { "value": "value", }, "toolCallId": "call-1", "toolName": "dynamicTool", "type": "tool-input-available", }, { "dynamic": true, "output": { "value": "test-result", }, "toolCallId": "call-1", "type": "tool-output-available", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); }); }); describe('options.providerMetadata', () => { it('should pass provider metadata to model', async () => { const result = streamText({ model: new MockLanguageModelV2({ doStream: async ({ providerOptions }) => { expect(providerOptions).toStrictEqual({ aProvider: { someKey: 'someValue' }, }); return { stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'provider metadata test', }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }), prompt: 'test-input', providerOptions: { aProvider: { someKey: 'someValue' }, }, }); assert.deepStrictEqual( await convertAsyncIterableToArray(result.textStream), ['provider metadata test'], ); }); }); describe('options.abortSignal', () => { it('should forward abort signal to tool execution during streaming', async () => { const abortController = new AbortController(); const toolExecuteMock = vi.fn().mockResolvedValue('tool result'); const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: toolExecuteMock, }, }, prompt: 'test-input', abortSignal: abortController.signal, }); await convertAsyncIterableToArray(result.fullStream); abortController.abort(); expect(toolExecuteMock).toHaveBeenCalledWith( { value: 'value' }, { abortSignal: abortController.signal, toolCallId: 'call-1', messages: expect.any(Array), }, ); }); }); describe('telemetry', () => { let tracer: MockTracer; beforeEach(() => { tracer = new MockTracer(); }); it('should not record any telemetry data when not explicitly enabled', async () => { const result = streamText({ model: createTestModel(), prompt: 'test-input', _internal: { now: mockValues(0, 100, 500), }, }); await result.consumeStream(); expect(tracer.jsonSpans).toMatchSnapshot(); }); it('should record telemetry data when enabled', async () => { const result = streamText({ model: createTestModel(), prompt: 'test-input', topK: 0.1, topP: 0.2, frequencyPenalty: 0.3, presencePenalty: 0.4, temperature: 0.5, stopSequences: ['stop'], headers: { header1: 'value1', header2: 'value2', }, experimental_telemetry: { isEnabled: true, functionId: 'test-function-id', metadata: { test1: 'value1', test2: false, }, tracer, }, _internal: { now: mockValues(0, 100, 500) }, }); await result.consumeStream(); expect(tracer.jsonSpans).toMatchSnapshot(); }); it('should record successful tool call', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => `${value}-result`, }, }, prompt: 'test-input', experimental_telemetry: { isEnabled: true, tracer }, _internal: { now: mockValues(0, 100, 500) }, }); await result.consumeStream(); expect(tracer.jsonSpans).toMatchSnapshot(); }); it('should record error on tool call', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async () => { throw new Error('Tool execution failed'); }, }, }, prompt: 'test-input', experimental_telemetry: { isEnabled: true, tracer }, _internal: { now: mockValues(0, 100, 500) }, }); await result.consumeStream(); expect(tracer.jsonSpans).toHaveLength(3); // Check that we have the expected spans expect(tracer.jsonSpans[0].name).toBe('ai.streamText'); expect(tracer.jsonSpans[1].name).toBe('ai.streamText.doStream'); expect(tracer.jsonSpans[2].name).toBe('ai.toolCall'); // Check that the tool call span has error status const toolCallSpan = tracer.jsonSpans[2]; expect(toolCallSpan.status).toEqual({ code: 2, message: 'Tool execution failed', }); // Check that the tool call span has exception event expect(toolCallSpan.events).toHaveLength(1); const exceptionEvent = toolCallSpan.events[0]; expect(exceptionEvent.name).toBe('exception'); expect(exceptionEvent.attributes).toMatchObject({ 'exception.message': 'Tool execution failed', 'exception.name': 'Error', }); expect(exceptionEvent.attributes?.['exception.stack']).toContain( 'Tool execution failed', ); expect(exceptionEvent.time).toEqual([0, 0]); }); it('should not record telemetry inputs / outputs when disabled', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => `${value}-result`, }, }, prompt: 'test-input', experimental_telemetry: { isEnabled: true, recordInputs: false, recordOutputs: false, tracer, }, _internal: { now: mockValues(0, 100, 500) }, }); await result.consumeStream(); expect(tracer.jsonSpans).toMatchSnapshot(); }); }); describe('tool callbacks', () => { it('should invoke callbacks in the correct order', async () => { const recordedCalls: unknown[] = []; const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-input-start', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', toolName: 'test-tool', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: '{"', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: 'value', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: '":"', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: 'Spark', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: 'le', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: ' Day', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: '"}', }, { type: 'tool-input-end', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', }, { type: 'tool-call', toolCallId: 'call_O17Uplv4lJvD6DVdIvFFeRMw', toolName: 'test-tool', input: '{"value":"Sparkle Day"}', }, { type: 'finish', finishReason: 'tool-calls', usage: testUsage, }, ]), }), tools: { 'test-tool': tool({ inputSchema: jsonSchema<{ value: string }>({ type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, }), onInputAvailable: options => { recordedCalls.push({ type: 'onInputAvailable', options }); }, onInputStart: options => { recordedCalls.push({ type: 'onInputStart', options }); }, onInputDelta: options => { recordedCalls.push({ type: 'onInputDelta', options }); }, }), }, toolChoice: 'required', prompt: 'test-input', _internal: { now: mockValues(0, 100, 500), }, }); await result.consumeStream(); expect(recordedCalls).toMatchInlineSnapshot(` [ { "options": { "abortSignal": undefined, "experimental_context": undefined, "messages": [ { "content": "test-input", "role": "user", }, ], "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", }, "type": "onInputStart", }, { "options": { "abortSignal": undefined, "experimental_context": undefined, "inputTextDelta": "{"", "messages": [ { "content": "test-input", "role": "user", }, ], "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", }, "type": "onInputDelta", }, { "options": { "abortSignal": undefined, "experimental_context": undefined, "inputTextDelta": "value", "messages": [ { "content": "test-input", "role": "user", }, ], "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", }, "type": "onInputDelta", }, { "options": { "abortSignal": undefined, "experimental_context": undefined, "inputTextDelta": "":"", "messages": [ { "content": "test-input", "role": "user", }, ], "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", }, "type": "onInputDelta", }, { "options": { "abortSignal": undefined, "experimental_context": undefined, "inputTextDelta": "Spark", "messages": [ { "content": "test-input", "role": "user", }, ], "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", }, "type": "onInputDelta", }, { "options": { "abortSignal": undefined, "experimental_context": undefined, "inputTextDelta": "le", "messages": [ { "content": "test-input", "role": "user", }, ], "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", }, "type": "onInputDelta", }, { "options": { "abortSignal": undefined, "experimental_context": undefined, "inputTextDelta": " Day", "messages": [ { "content": "test-input", "role": "user", }, ], "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", }, "type": "onInputDelta", }, { "options": { "abortSignal": undefined, "experimental_context": undefined, "inputTextDelta": ""}", "messages": [ { "content": "test-input", "role": "user", }, ], "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", }, "type": "onInputDelta", }, { "options": { "abortSignal": undefined, "experimental_context": undefined, "input": { "value": "Sparkle Day", }, "messages": [ { "content": "test-input", "role": "user", }, ], "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", }, "type": "onInputAvailable", }, ] `); }); }); describe('tools with custom schema', () => { it('should send tool calls', async () => { const result = streamText({ model: new MockLanguageModelV2({ doStream: async ({ prompt, tools, toolChoice }) => { expect(tools).toStrictEqual([ { type: 'function', name: 'tool1', description: undefined, inputSchema: { additionalProperties: false, properties: { value: { type: 'string' } }, required: ['value'], type: 'object', }, providerOptions: undefined, }, ]); expect(toolChoice).toStrictEqual({ type: 'required' }); expect(prompt).toStrictEqual([ { role: 'user', content: [{ type: 'text', text: 'test-input' }], providerOptions: undefined, }, ]); return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }), tools: { tool1: { inputSchema: jsonSchema<{ value: string }>({ type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, }), }, }, toolChoice: 'required', prompt: 'test-input', _internal: { now: mockValues(0, 100, 500), }, }); expect( await convertAsyncIterableToArray(result.fullStream), ).toMatchSnapshot(); }); }); describe('options.messages', () => { it('should support models that use "this" context in supportedUrls', async () => { let supportedUrlsCalled = false; class MockLanguageModelWithImageSupport extends MockLanguageModelV2 { constructor() { super({ supportedUrls() { supportedUrlsCalled = true; // Reference 'this' to verify context return this.modelId === 'mock-model-id' ? ({ 'image/*': [/^https:\/\/.*$/] } as Record< string, RegExp[] >) : {}; }, doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: 'world!' }, { type: 'text-end', id: '1' }, ]), }), }); } } const model = new MockLanguageModelWithImageSupport(); const result = streamText({ model, messages: [ { role: 'user', content: [{ type: 'image', image: 'https://example.com/test.jpg' }], }, ], }); await result.consumeStream(); expect(supportedUrlsCalled).toBe(true); expect(await result.text).toBe('Hello, world!'); }); }); describe('tool execution errors', () => { let result: StreamTextResult<any, any>; beforeEach(async () => { result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { tool1: tool({ inputSchema: z.object({ value: z.string() }), execute: async (): Promise<string> => { throw new Error('test error'); }, }), }, ...defaultSettings(), }); }); it('should include tool error part in the full stream', async () => { expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "error": [Error: test error], "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-error", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); it('should include the error part in the step stream', async () => { await result.consumeStream(); expect(await result.steps).toMatchInlineSnapshot(` [ DefaultStepResult { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "error": [Error: test error], "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-error", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": undefined, "id": "id-0", "messages": [ { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "error-text", "value": "test error", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, ] `); }); it('should include error result in response messages', async () => { await result.consumeStream(); expect((await result.response).messages).toMatchInlineSnapshot(` [ { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "error-text", "value": "test error", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ] `); }); it('should add tool-error parts to ui message stream', async () => { expect(await convertReadableStreamToArray(result.toUIMessageStream())) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "input": { "value": "value", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-input-available", }, { "errorText": "test error", "toolCallId": "call-1", "type": "tool-output-error", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); }); describe('options.transform', () => { describe('with base transformation', () => { const upperCaseTransform = () => new TransformStream< TextStreamPart<{ tool1: Tool<{ value: string }> }>, TextStreamPart<{ tool1: Tool<{ value: string }> }> >({ transform(chunk, controller) { if ( chunk.type === 'text-delta' || chunk.type === 'reasoning-delta' ) { chunk.text = chunk.text.toUpperCase(); } if (chunk.type === 'tool-input-delta') { chunk.delta = chunk.delta.toUpperCase(); } // assuming test arg structure: if (chunk.type === 'tool-call' && !chunk.dynamic) { chunk.input = { ...chunk.input, value: chunk.input.value.toUpperCase(), }; } if (chunk.type === 'tool-result' && !chunk.dynamic) { chunk.output = chunk.output.toUpperCase(); chunk.input = { ...chunk.input, value: chunk.input.value.toUpperCase(), }; } if (chunk.type === 'start-step') { if (chunk.request.body != null) { chunk.request.body = ( chunk.request.body as string ).toUpperCase(); } } if (chunk.type === 'finish-step') { if (chunk.providerMetadata?.testProvider != null) { chunk.providerMetadata.testProvider = { testKey: 'TEST VALUE', }; } } controller.enqueue(chunk); }, }); it('should transform the stream', async () => { const result = streamText({ model: createTestModel(), experimental_transform: upperCaseTransform, prompt: 'test-input', }); expect( await convertAsyncIterableToArray(result.textStream), ).toStrictEqual(['HELLO', ', ', 'WORLD!']); }); it('result.text should be transformed', async () => { const result = streamText({ model: createTestModel(), experimental_transform: upperCaseTransform, prompt: 'test-input', }); await result.consumeStream(); expect(await result.text).toStrictEqual('HELLO, WORLD!'); }); it('result.response.messages should be transformed', async () => { const result = streamText({ model: createTestModel(), experimental_transform: upperCaseTransform, prompt: 'test-input', }); await result.consumeStream(); expect(await result.response).toStrictEqual({ id: expect.any(String), timestamp: expect.any(Date), modelId: expect.any(String), headers: undefined, messages: [ { role: 'assistant', content: [ { providerOptions: undefined, text: 'HELLO, WORLD!', type: 'text', }, ], }, ], }); }); it('result.totalUsage should be transformed', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), experimental_transform: () => new TransformStream<TextStreamPart<any>, TextStreamPart<any>>({ transform(chunk, controller) { if (chunk.type === 'finish') { chunk.totalUsage = { inputTokens: 200, outputTokens: 300, totalTokens: undefined, reasoningTokens: undefined, cachedInputTokens: undefined, }; } controller.enqueue(chunk); }, }), prompt: 'test-input', }); await result.consumeStream(); expect(await result.totalUsage).toStrictEqual({ inputTokens: 200, outputTokens: 300, totalTokens: undefined, reasoningTokens: undefined, cachedInputTokens: undefined, }); }); it('result.finishReason should be transformed', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'length', usage: testUsage, }, ]), }), experimental_transform: () => new TransformStream<TextStreamPart<any>, TextStreamPart<any>>({ transform(chunk, controller) { if (chunk.type === 'finish') { chunk.finishReason = 'stop'; } controller.enqueue(chunk); }, }), prompt: 'test-input', }); await result.consumeStream(); expect(await result.finishReason).toStrictEqual('stop'); }); it('result.toolCalls should be transformed', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello, ' }, { type: 'text-delta', id: '1', delta: 'world!' }, { type: 'text-end', id: '1' }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async () => 'result1', }, }, experimental_transform: upperCaseTransform, prompt: 'test-input', }); await result.consumeStream(); expect(await result.toolCalls).toMatchInlineSnapshot(` [ { "input": { "value": "VALUE", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ] `); }); it('result.toolResults should be transformed', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello, ' }, { type: 'text-delta', id: '1', delta: 'world!' }, { type: 'text-end', id: '1' }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async () => 'result1', }, }, experimental_transform: upperCaseTransform, prompt: 'test-input', }); await result.consumeStream(); expect(await result.toolResults).toMatchInlineSnapshot(` [ { "input": { "value": "VALUE", }, "output": "RESULT1", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ] `); }); it('result.steps should be transformed', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello, ' }, { type: 'text-delta', id: '1', delta: 'world!' }, { type: 'text-end', id: '1' }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async () => 'result1', }, }, experimental_transform: upperCaseTransform, prompt: 'test-input', }); result.consumeStream(); expect(await result.steps).toMatchInlineSnapshot(` [ DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "HELLO, WORLD!", "type": "text", }, { "input": { "value": "VALUE", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "VALUE", }, "output": "RESULT1", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": undefined, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "HELLO, WORLD!", "type": "text", }, { "input": { "value": "VALUE", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "RESULT1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, ] `); }); it('result.request should be transformed', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), request: { body: 'test body' }, }), prompt: 'test-input', experimental_transform: upperCaseTransform, }); result.consumeStream(); expect(await result.request).toStrictEqual({ body: 'TEST BODY', }); }); it('result.providerMetadata should be transformed', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, providerMetadata: { testProvider: { testKey: 'testValue', }, }, }, ]), request: { body: 'test body' }, }), prompt: 'test-input', experimental_transform: upperCaseTransform, }); result.consumeStream(); expect(JSON.stringify(await result.providerMetadata)).toStrictEqual( JSON.stringify({ testProvider: { testKey: 'TEST VALUE', }, }), ); }); it('options.onFinish should receive transformed data', async () => { let result!: Parameters< Required<Parameters<typeof streamText>[0]>['onFinish'] >[0]; const resultObject = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'text-delta', id: '1', delta: 'world!' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, providerMetadata: { testProvider: { testKey: 'testValue' }, }, }, ]), response: { headers: { call: '2' } }, }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => `${value}-result`, }, }, prompt: 'test-input', onFinish: async event => { result = event as unknown as typeof result; }, experimental_transform: upperCaseTransform, }); await resultObject.consumeStream(); expect(result).toMatchInlineSnapshot(` { "content": [ { "providerMetadata": undefined, "text": "HELLO, WORLD!", "type": "text", }, { "input": { "value": "VALUE", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "VALUE", }, "output": "VALUE-RESULT", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "dynamicToolCalls": [], "dynamicToolResults": [], "files": [], "finishReason": "stop", "providerMetadata": { "testProvider": { "testKey": "TEST VALUE", }, }, "reasoning": [], "reasoningText": undefined, "request": {}, "response": { "headers": { "call": "2", }, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "HELLO, WORLD!", "type": "text", }, { "input": { "value": "VALUE", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "VALUE-RESULT", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "sources": [], "staticToolCalls": [], "staticToolResults": [], "steps": [ DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "HELLO, WORLD!", "type": "text", }, { "input": { "value": "VALUE", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "VALUE", }, "output": "VALUE-RESULT", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "stop", "providerMetadata": { "testProvider": { "testKey": "TEST VALUE", }, }, "request": {}, "response": { "headers": { "call": "2", }, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "HELLO, WORLD!", "type": "text", }, { "input": { "value": "VALUE", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "VALUE-RESULT", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, ], "text": "HELLO, WORLD!", "toolCalls": [ { "input": { "value": "VALUE", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "toolResults": [ { "input": { "value": "VALUE", }, "output": "VALUE-RESULT", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], } `); }); it('options.onStepFinish should receive transformed data', async () => { let result!: Parameters< Required<Parameters<typeof streamText>[0]>['onStepFinish'] >[0]; const resultObject = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'text-delta', id: '1', delta: 'world!' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, providerMetadata: { testProvider: { testKey: 'testValue' }, }, }, ]), response: { headers: { call: '2' } }, }), tools: { tool1: tool({ inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => `${value}-result`, }), }, prompt: 'test-input', onStepFinish: async event => { result = event as unknown as typeof result; }, experimental_transform: upperCaseTransform, }); await resultObject.consumeStream(); expect(result).toMatchInlineSnapshot(` DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "HELLO, WORLD!", "type": "text", }, { "input": { "value": "VALUE", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "VALUE", }, "output": "VALUE-RESULT", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "stop", "providerMetadata": { "testProvider": { "testKey": "TEST VALUE", }, }, "request": {}, "response": { "headers": { "call": "2", }, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "HELLO, WORLD!", "type": "text", }, { "input": { "value": "VALUE", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "VALUE-RESULT", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], } `); }); it('telemetry should record transformed data when enabled', async () => { const tracer = new MockTracer(); const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'text-delta', id: '1', delta: 'world!' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, providerMetadata: { testProvider: { testKey: 'testValue' }, }, }, ]), }), tools: { tool1: tool({ inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => `${value}-result`, }), }, prompt: 'test-input', experimental_transform: upperCaseTransform, experimental_telemetry: { isEnabled: true, tracer }, _internal: { now: mockValues(0, 100, 500) }, }); await result.consumeStream(); expect(tracer.jsonSpans).toMatchSnapshot(); }); it('it should send transformed chunks to onChunk', async () => { const result: Array< Extract< TextStreamPart<any>, { type: | 'text-delta' | 'reasoning-delta' | 'source' | 'tool-call' | 'tool-input-start' | 'tool-input-delta' | 'tool-result' | 'raw'; } > > = []; const resultObject = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'reasoning-start', id: '2' }, { type: 'reasoning-delta', id: '2', delta: 'Feeling clever' }, { type: 'reasoning-end', id: '2' }, { type: 'tool-input-start', id: 'call-1', toolName: 'tool1' }, { type: 'tool-input-delta', id: 'call-1', delta: '{"value": "' }, { type: 'tool-input-delta', id: 'call-1', delta: 'test' }, { type: 'tool-input-delta', id: 'call-1', delta: '"}' }, { type: 'tool-input-end', id: 'call-1' }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "test" }`, }, { type: 'text-delta', id: '1', delta: ' World' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => `${value}-result`, }, }, prompt: 'test-input', onChunk(event) { result.push(event.chunk); }, experimental_transform: upperCaseTransform, }); await resultObject.consumeStream(); expect(result).toMatchInlineSnapshot(` [ { "id": "1", "providerMetadata": undefined, "text": "HELLO", "type": "text-delta", }, { "id": "2", "providerMetadata": undefined, "text": "FEELING CLEVER", "type": "reasoning-delta", }, { "dynamic": false, "id": "call-1", "toolName": "tool1", "type": "tool-input-start", }, { "delta": "{"VALUE": "", "id": "call-1", "type": "tool-input-delta", }, { "delta": "TEST", "id": "call-1", "type": "tool-input-delta", }, { "delta": ""}", "id": "call-1", "type": "tool-input-delta", }, { "input": { "value": "TEST", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "TEST", }, "output": "TEST-RESULT", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, { "id": "1", "providerMetadata": undefined, "text": " WORLD", "type": "text-delta", }, ] `); }); }); describe('with multiple transformations', () => { const toUppercaseAndAddCommaTransform = <TOOLS extends ToolSet>() => (options: { tools: TOOLS }) => new TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>({ transform(chunk, controller) { if (chunk.type !== 'text-delta') { controller.enqueue(chunk); return; } controller.enqueue({ ...chunk, text: `${chunk.text.toUpperCase()},`, }); }, }); const omitCommaTransform = <TOOLS extends ToolSet>() => (options: { tools: TOOLS }) => new TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>({ transform(chunk, controller) { if (chunk.type !== 'text-delta') { controller.enqueue(chunk); return; } controller.enqueue({ ...chunk, text: chunk.text.replaceAll(',', ''), }); }, }); it('should transform the stream', async () => { const result = streamText({ model: createTestModel(), experimental_transform: [ toUppercaseAndAddCommaTransform(), omitCommaTransform(), ], prompt: 'test-input', }); expect( await convertAsyncIterableToArray(result.textStream), ).toStrictEqual(['HELLO', ' ', 'WORLD!']); }); }); describe('with transformation that aborts stream', () => { const stopWordTransform = <TOOLS extends ToolSet>() => ({ stopStream }: { stopStream: () => void }) => new TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>({ // note: this is a simplified transformation for testing; // in a real-world version more there would need to be // stream buffering and scanning to correctly emit prior text // and to detect all STOP occurrences. transform(chunk, controller) { if (chunk.type !== 'text-delta') { controller.enqueue(chunk); return; } if (chunk.text.includes('STOP')) { stopStream(); controller.enqueue({ type: 'finish-step', finishReason: 'stop', providerMetadata: undefined, usage: { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, reasoningTokens: undefined, cachedInputTokens: undefined, }, response: { id: 'response-id', modelId: 'mock-model-id', timestamp: new Date(0), }, }); controller.enqueue({ type: 'finish', finishReason: 'stop', totalUsage: { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, reasoningTokens: undefined, cachedInputTokens: undefined, }, }); return; } controller.enqueue(chunk); }, }); it('stream should stop when STOP token is encountered', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello, ' }, { type: 'text-delta', id: '1', delta: 'STOP' }, { type: 'text-delta', id: '1', delta: ' World' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, reasoningTokens: undefined, cachedInputTokens: undefined, }, }, ]), }), prompt: 'test-input', experimental_transform: stopWordTransform(), }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hello, ", "type": "text-delta", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "id": "response-id", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": undefined, "outputTokens": undefined, "reasoningTokens": undefined, "totalTokens": undefined, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": undefined, "outputTokens": undefined, "reasoningTokens": undefined, "totalTokens": undefined, }, "type": "finish", }, ] `); }); it('options.onStepFinish should be called', async () => { let result!: Parameters< Required<Parameters<typeof streamText>[0]>['onStepFinish'] >[0]; const resultObject = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello, ' }, { type: 'text-delta', id: '1', delta: 'STOP' }, { type: 'text-delta', id: '1', delta: ' World' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), prompt: 'test-input', onStepFinish: async event => { result = event as unknown as typeof result; }, experimental_transform: stopWordTransform(), }); await resultObject.consumeStream(); expect(result).toMatchInlineSnapshot(` DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "Hello, ", "type": "text", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "id": "response-id", "messages": [ { "content": [ { "providerOptions": undefined, "text": "Hello, ", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": undefined, "outputTokens": undefined, "reasoningTokens": undefined, "totalTokens": undefined, }, "warnings": [], } `); }); }); }); describe('options.output', () => { describe('no output', () => { it('should throw error when accessing partial output stream', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"value": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world` }, { type: 'text-delta', id: '1', delta: `!"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), prompt: 'prompt', }); await expect(async () => { await convertAsyncIterableToArray( result.experimental_partialOutputStream, ); }).rejects.toThrow('No output specified'); }); }); describe('text output', () => { it('should send partial output stream', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello, ' }, { type: 'text-delta', id: '1', delta: ',' }, { type: 'text-delta', id: '1', delta: ' world!' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), experimental_output: text(), prompt: 'prompt', }); expect( await convertAsyncIterableToArray( result.experimental_partialOutputStream, ), ).toStrictEqual(['Hello, ', 'Hello, ,', 'Hello, , world!']); }); }); describe('object output', () => { it('should set responseFormat to json and send schema as part of the responseFormat', async () => { let callOptions!: LanguageModelV2CallOptions; const result = streamText({ model: new MockLanguageModelV2({ doStream: async args => { callOptions = args; return { stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"value": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world` }, { type: 'text-delta', id: '1', delta: `!"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }), experimental_output: object({ schema: z.object({ value: z.string() }), }), prompt: 'prompt', }); await result.consumeStream(); expect(callOptions).toMatchInlineSnapshot(` { "abortSignal": undefined, "frequencyPenalty": undefined, "headers": undefined, "includeRawChunks": false, "maxOutputTokens": undefined, "presencePenalty": undefined, "prompt": [ { "content": [ { "text": "prompt", "type": "text", }, ], "providerOptions": undefined, "role": "user", }, ], "providerOptions": undefined, "responseFormat": { "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "value": { "type": "string", }, }, "required": [ "value", ], "type": "object", }, "type": "json", }, "seed": undefined, "stopSequences": undefined, "temperature": undefined, "toolChoice": undefined, "tools": undefined, "topK": undefined, "topP": undefined, } `); }); it('should send valid partial text fragments', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"value": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world` }, { type: 'text-delta', id: '1', delta: `!"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), experimental_output: object({ schema: z.object({ value: z.string() }), }), prompt: 'prompt', }); expect( await convertAsyncIterableToArray(result.textStream), ).toStrictEqual([ `{ `, // key difference: need to combine after `:` `"value": "Hello, `, `world`, `!"`, ` }`, ]); }); it('should send partial output stream', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"value": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world` }, { type: 'text-delta', id: '1', delta: `!"` }, { type: 'text-delta', id: '1', delta: ' }' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), experimental_output: object({ schema: z.object({ value: z.string() }), }), prompt: 'prompt', }); expect( await convertAsyncIterableToArray( result.experimental_partialOutputStream, ), ).toStrictEqual([ {}, { value: 'Hello, ' }, { value: 'Hello, world' }, { value: 'Hello, world!' }, ]); }); it('should send partial output stream when last chunk contains content', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"value": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world!" }` }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), experimental_output: object({ schema: z.object({ value: z.string() }), }), prompt: 'prompt', }); expect( await convertAsyncIterableToArray( result.experimental_partialOutputStream, ), ).toStrictEqual([{}, { value: 'Hello, ' }, { value: 'Hello, world!' }]); }); it('should resolve text promise with the correct content', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"value": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world!" ` }, { type: 'text-delta', id: '1', delta: '}' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), experimental_output: object({ schema: z.object({ value: z.string() }), }), prompt: 'prompt', }); result.consumeStream(); expect(await result.text).toStrictEqual('{ "value": "Hello, world!" }'); }); it('should call onFinish with the correct content', async () => { let result!: Parameters< Required<Parameters<typeof streamText>[0]>['onFinish'] >[0]; const resultObject = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '{ ' }, { type: 'text-delta', id: '1', delta: '"value": ' }, { type: 'text-delta', id: '1', delta: `"Hello, ` }, { type: 'text-delta', id: '1', delta: `world!" ` }, { type: 'text-delta', id: '1', delta: '}' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), experimental_output: object({ schema: z.object({ value: z.string() }), }), prompt: 'prompt', onFinish: async event => { result = event as unknown as typeof result; }, _internal: { generateId: mockId({ prefix: 'id' }), currentDate: () => new Date(0), }, }); resultObject.consumeStream(); await resultObject.consumeStream(); expect(result).toMatchInlineSnapshot(` { "content": [ { "providerMetadata": undefined, "text": "{ "value": "Hello, world!" }", "type": "text", }, ], "dynamicToolCalls": [], "dynamicToolResults": [], "files": [], "finishReason": "stop", "providerMetadata": undefined, "reasoning": [], "reasoningText": undefined, "request": {}, "response": { "headers": undefined, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "{ "value": "Hello, world!" }", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "sources": [], "staticToolCalls": [], "staticToolResults": [], "steps": [ DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "{ "value": "Hello, world!" }", "type": "text", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": undefined, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "{ "value": "Hello, world!" }", "type": "text", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, ], "text": "{ "value": "Hello, world!" }", "toolCalls": [], "toolResults": [], "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], } `); }); }); }); describe('options.activeTools', () => { it('should filter available tools to only the ones in activeTools', async () => { let tools: | (LanguageModelV2FunctionTool | LanguageModelV2ProviderDefinedTool)[] | undefined; const result = streamText({ model: new MockLanguageModelV2({ doStream: async ({ tools: toolsArg }) => { tools = toolsArg; return { stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: `world!` }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async () => 'result1', }, tool2: { inputSchema: z.object({ value: z.string() }), execute: async () => 'result2', }, }, prompt: 'test-input', activeTools: ['tool1'], }); await result.consumeStream(); expect(tools).toMatchInlineSnapshot(` [ { "description": undefined, "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "value": { "type": "string", }, }, "required": [ "value", ], "type": "object", }, "name": "tool1", "providerOptions": undefined, "type": "function", }, ] `); }); }); describe('raw chunks forwarding', () => { it('should forward raw chunks when includeRawChunks is enabled', async () => { const modelWithRawChunks = createTestModel({ stream: convertArrayToReadableStream([ { type: 'stream-start', warnings: [] }, { type: 'raw', rawValue: { type: 'raw-data', content: 'should appear', }, }, { type: 'response-metadata', id: 'test-id', modelId: 'test-model', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello, world!' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }); const result = streamText({ model: modelWithRawChunks, prompt: 'test prompt', includeRawChunks: true, }); const chunks = await convertAsyncIterableToArray(result.fullStream); expect(chunks.filter(chunk => chunk.type === 'raw')) .toMatchInlineSnapshot(` [ { "rawValue": { "content": "should appear", "type": "raw-data", }, "type": "raw", }, ] `); }); it('should not forward raw chunks when includeRawChunks is disabled', async () => { const modelWithRawChunks = createTestModel({ stream: convertArrayToReadableStream([ { type: 'stream-start', warnings: [] }, { type: 'raw', rawValue: { type: 'raw-data', content: 'should not appear', }, }, { type: 'response-metadata', id: 'test-id', modelId: 'test-model', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello, world!' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }); const result = streamText({ model: modelWithRawChunks, prompt: 'test prompt', includeRawChunks: false, }); const chunks = await convertAsyncIterableToArray(result.fullStream); expect(chunks.filter(chunk => chunk.type === 'raw')).toHaveLength(0); }); it('should pass through the includeRawChunks flag correctly to the model', async () => { let capturedOptions: any; const model = new MockLanguageModelV2({ doStream: async options => { capturedOptions = options; return { stream: convertArrayToReadableStream([ { type: 'stream-start', warnings: [] }, { type: 'finish', finishReason: 'stop', usage: testUsage }, ]), }; }, }); await streamText({ model, prompt: 'test prompt', includeRawChunks: true, }).consumeStream(); expect(capturedOptions.includeRawChunks).toBe(true); }); it('should call onChunk with raw chunks when includeRawChunks is enabled', async () => { const onChunkCalls: Array<any> = []; const modelWithRawChunks = createTestModel({ stream: convertArrayToReadableStream([ { type: 'stream-start', warnings: [] }, { type: 'raw', rawValue: { type: 'stream-start', data: 'start' }, }, { type: 'raw', rawValue: { type: 'response-metadata', id: 'test-id', modelId: 'test-model', }, }, { type: 'raw', rawValue: { type: 'text-delta', content: 'Hello' }, }, { type: 'raw', rawValue: { type: 'text-delta', content: ', world!' }, }, { type: 'raw', rawValue: { type: 'finish', reason: 'stop' }, }, { type: 'response-metadata', id: 'test-id', modelId: 'test-model', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello, world!' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }); const result = streamText({ model: modelWithRawChunks, prompt: 'test prompt', includeRawChunks: true, onChunk({ chunk }) { onChunkCalls.push(chunk); }, }); await result.consumeStream(); expect(onChunkCalls).toMatchInlineSnapshot(` [ { "rawValue": { "data": "start", "type": "stream-start", }, "type": "raw", }, { "rawValue": { "id": "test-id", "modelId": "test-model", "type": "response-metadata", }, "type": "raw", }, { "rawValue": { "content": "Hello", "type": "text-delta", }, "type": "raw", }, { "rawValue": { "content": ", world!", "type": "text-delta", }, "type": "raw", }, { "rawValue": { "reason": "stop", "type": "finish", }, "type": "raw", }, { "id": "1", "providerMetadata": undefined, "text": "Hello, world!", "type": "text-delta", }, ] `); }); it('should pass includeRawChunks flag correctly to the model', async () => { let capturedOptions: any; const model = new MockLanguageModelV2({ doStream: async options => { capturedOptions = options; return { stream: convertArrayToReadableStream([ { type: 'stream-start', warnings: [] }, { type: 'response-metadata', id: 'test-id', modelId: 'test-model', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }); await streamText({ model, prompt: 'test prompt', includeRawChunks: true, }).consumeStream(); expect(capturedOptions.includeRawChunks).toBe(true); await streamText({ model, prompt: 'test prompt', includeRawChunks: false, }).consumeStream(); expect(capturedOptions.includeRawChunks).toBe(false); await streamText({ model, prompt: 'test prompt', }).consumeStream(); expect(capturedOptions.includeRawChunks).toBe(false); }); }); describe('mixed multi content streaming with interleaving parts', () => { describe('mixed text and reasoning blocks', () => { let result: StreamTextResult<any, any>; beforeEach(async () => { result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'stream-start', warnings: [] }, { type: 'reasoning-start', id: '0' }, { type: 'text-start', id: '1' }, { type: 'reasoning-delta', id: '0', delta: 'Thinking...' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-start', id: '2' }, { type: 'text-delta', id: '2', delta: `This ` }, { type: 'text-delta', id: '2', delta: `is ` }, { type: 'reasoning-start', id: '3' }, { type: 'reasoning-delta', id: '0', delta: `I'm thinking...` }, { type: 'reasoning-delta', id: '3', delta: `Separate thoughts` }, { type: 'text-delta', id: '2', delta: `a` }, { type: 'text-delta', id: '1', delta: `world!` }, { type: 'reasoning-end', id: '0' }, { type: 'text-delta', id: '2', delta: ` test.` }, { type: 'text-end', id: '2' }, { type: 'reasoning-end', id: '3' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), prompt: 'test-input', _internal: { currentDate: mockValues(new Date(2000)), generateId: mockId(), }, }); }); it('should return the full stream with the correct parts', async () => { expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "0", "type": "reasoning-start", }, { "id": "1", "type": "text-start", }, { "id": "0", "providerMetadata": undefined, "text": "Thinking...", "type": "reasoning-delta", }, { "id": "1", "providerMetadata": undefined, "text": "Hello", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": ", ", "type": "text-delta", }, { "id": "2", "type": "text-start", }, { "id": "2", "providerMetadata": undefined, "text": "This ", "type": "text-delta", }, { "id": "2", "providerMetadata": undefined, "text": "is ", "type": "text-delta", }, { "id": "3", "type": "reasoning-start", }, { "id": "0", "providerMetadata": undefined, "text": "I'm thinking...", "type": "reasoning-delta", }, { "id": "3", "providerMetadata": undefined, "text": "Separate thoughts", "type": "reasoning-delta", }, { "id": "2", "providerMetadata": undefined, "text": "a", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": "world!", "type": "text-delta", }, { "id": "0", "type": "reasoning-end", }, { "id": "2", "providerMetadata": undefined, "text": " test.", "type": "text-delta", }, { "id": "2", "type": "text-end", }, { "id": "3", "type": "reasoning-end", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:02.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); it('should return the content parts in the correct order', async () => { await result.consumeStream(); expect(await result.content).toMatchInlineSnapshot(` [ { "providerMetadata": undefined, "text": "Thinking...I'm thinking...", "type": "reasoning", }, { "providerMetadata": undefined, "text": "Hello, world!", "type": "text", }, { "providerMetadata": undefined, "text": "This is a test.", "type": "text", }, { "providerMetadata": undefined, "text": "Separate thoughts", "type": "reasoning", }, ] `); }); it('should return the step content parts in the correct order', async () => { await result.consumeStream(); expect(await result.steps).toMatchInlineSnapshot(` [ DefaultStepResult { "content": [ { "providerMetadata": undefined, "text": "Thinking...I'm thinking...", "type": "reasoning", }, { "providerMetadata": undefined, "text": "Hello, world!", "type": "text", }, { "providerMetadata": undefined, "text": "This is a test.", "type": "text", }, { "providerMetadata": undefined, "text": "Separate thoughts", "type": "reasoning", }, ], "finishReason": "stop", "providerMetadata": undefined, "request": {}, "response": { "headers": undefined, "id": "id-0", "messages": [ { "content": [ { "providerOptions": undefined, "text": "Thinking...I'm thinking...", "type": "reasoning", }, { "providerOptions": undefined, "text": "Hello, world!", "type": "text", }, { "providerOptions": undefined, "text": "This is a test.", "type": "text", }, { "providerOptions": undefined, "text": "Separate thoughts", "type": "reasoning", }, ], "role": "assistant", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:02.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, ] `); }); }); }); describe('abort signal', () => { describe('basic abort', () => { let result: StreamTextResult<ToolSet, TextStreamPart<ToolSet>>; let onErrorCalls: Array<{ error: unknown }> = []; let onAbortCalls: Array<{ steps: StepResult<ToolSet>[] }> = []; beforeEach(() => { onErrorCalls = []; onAbortCalls = []; const abortController = new AbortController(); let pullCalls = 0; result = streamText({ abortSignal: abortController.signal, onError: error => { onErrorCalls.push({ error }); }, onAbort: event => { onAbortCalls.push(event); }, model: new MockLanguageModelV2({ doStream: async () => ({ stream: new ReadableStream({ pull(controller) { switch (pullCalls++) { case 0: controller.enqueue({ type: 'stream-start', warnings: [], }); break; case 1: controller.enqueue({ type: 'text-start', id: '1', }); break; case 2: controller.enqueue({ type: 'text-delta', id: '1', delta: 'Hello', }); break; case 3: abortController.abort(); controller.error( new DOMException( 'The user aborted a request.', 'AbortError', ), ); break; } }, }), }), }), prompt: 'test-input', }); }); it('should not call onError for abort errors', async () => { await result.consumeStream(); expect(onErrorCalls).toMatchInlineSnapshot(`[]`); }); it('should call onAbort when the abort signal is triggered', async () => { await result.consumeStream(); expect(onAbortCalls).toMatchInlineSnapshot(` [ { "steps": [], }, ] `); }); it('should only stream initial chunks in full stream', async () => { expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hello", "type": "text-delta", }, { "type": "abort", }, ] `); }); it('should sent an abort chunk in the ui message stream', async () => { expect(await convertAsyncIterableToArray(result.toUIMessageStream())) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "id": "1", "type": "text-start", }, { "delta": "Hello", "id": "1", "type": "text-delta", }, { "type": "abort", }, ] `); }); }); describe('abort in 2nd step', () => { let result: StreamTextResult<any, TextStreamPart<any>>; let onErrorCalls: Array<{ error: unknown }> = []; let onAbortCalls: Array<{ steps: StepResult<any>[] }> = []; beforeEach(() => { onErrorCalls = []; onAbortCalls = []; const abortController = new AbortController(); let pullCalls = 0; let streamCalls = 0; result = streamText({ abortSignal: abortController.signal, onAbort: event => { onAbortCalls.push(event); }, model: new MockLanguageModelV2({ doStream: async () => ({ stream: new ReadableStream({ start(controller) { streamCalls++; pullCalls = 0; }, pull(controller) { if (streamCalls === 1) { switch (pullCalls++) { case 0: controller.enqueue({ type: 'stream-start', warnings: [], }); break; case 1: controller.enqueue({ type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }); break; case 2: controller.enqueue({ type: 'finish', finishReason: 'tool-calls', usage: testUsage, }); controller.close(); break; } } else switch (pullCalls++) { case 0: controller.enqueue({ type: 'stream-start', warnings: [], }); break; case 1: controller.enqueue({ type: 'text-start', id: '1', }); break; case 2: controller.enqueue({ type: 'text-delta', id: '1', delta: 'Hello', }); break; case 3: abortController.abort(); controller.error( new DOMException( 'The user aborted a request.', 'AbortError', ), ); break; } }, }), }), }), tools: { tool1: { inputSchema: z.object({ value: z.string() }), execute: async () => 'result1', }, }, stopWhen: stepCountIs(3), ...defaultSettings(), onError: error => { onErrorCalls.push({ error }); }, }); }); it('should not call onError for abort errors', async () => { await result.consumeStream(); expect(onErrorCalls).toMatchInlineSnapshot(`[]`); }); it('should call onAbort when the abort signal is triggered', async () => { await result.consumeStream(); expect(onAbortCalls).toMatchInlineSnapshot(` [ { "steps": [ DefaultStepResult { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "value", }, "output": "result1", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "finishReason": "tool-calls", "providerMetadata": undefined, "request": {}, "response": { "headers": undefined, "id": "id-0", "messages": [ { "content": [ { "input": { "value": "value", }, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result1", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, ], "role": "tool", }, ], "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "warnings": [], }, ], }, ] `); }); it('should only stream initial chunks in full stream', async () => { expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "input": { "value": "value", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-call", }, { "input": { "value": "value", }, "output": "result1", "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-result", }, { "finishReason": "tool-calls", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hello", "type": "text-delta", }, { "type": "abort", }, ] `); }); it('should sent an abort chunk in the ui message stream', async () => { expect(await convertAsyncIterableToArray(result.toUIMessageStream())) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "input": { "value": "value", }, "toolCallId": "call-1", "toolName": "tool1", "type": "tool-input-available", }, { "output": "result1", "toolCallId": "call-1", "type": "tool-output-available", }, { "type": "finish-step", }, { "type": "start-step", }, { "id": "1", "type": "text-start", }, { "delta": "Hello", "id": "1", "type": "text-delta", }, { "type": "abort", }, ] `); }); }); }); describe('tool execution context', () => { it('should send context to tool execution', async () => { let recordedContext: unknown | undefined; const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'tool-input-start', id: 'call-1', toolName: 'web_search', providerExecuted: true, }, { type: 'tool-input-delta', id: 'call-1', delta: '{ "value": "value" }', }, { type: 'tool-input-end', id: 'call-1', }, { type: 'tool-call', toolCallId: 'call-1', toolName: 't1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), tools: { t1: tool({ inputSchema: z.object({ value: z.string() }), execute: async ({ value }, { experimental_context }) => { recordedContext = experimental_context; return { value: 'test-result' }; }, }), }, experimental_context: { context: 'test', }, prompt: 'test-input', }); await result.consumeStream(); // tool should be executed by client expect(recordedContext).toStrictEqual({ context: 'test', }); }); }); describe('invalid tool calls', () => { describe('single invalid tool call', () => { let result: StreamTextResult<any, any>; beforeEach(async () => { result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'stream-start', warnings: [] }, { type: 'tool-input-start', id: 'call-1', toolName: 'cityAttractions', }, { type: 'tool-input-delta', id: 'call-1', delta: `{ "cities": "San Francisco" }`, }, { type: 'tool-input-end', id: 'call-1', }, { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'cityAttractions', // wrong tool call arguments (city vs cities): input: `{ "cities": "San Francisco" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }), prompt: 'test-input', _internal: { currentDate: mockValues(new Date(2000)), generateId: mockId(), }, tools: { cityAttractions: tool({ inputSchema: z.object({ city: z.string() }), }), }, }); }); it('should add tool call and result error parts to the content', async () => { await result.consumeStream(); expect(await result.content).toMatchInlineSnapshot(` [ { "dynamic": true, "error": [AI_InvalidToolInputError: Invalid input for tool cityAttractions: Type validation failed: Value: {"cities":"San Francisco"}. Error message: [ { "expected": "string", "code": "invalid_type", "path": [ "city" ], "message": "Invalid input: expected string, received undefined" } ]], "input": "{ "cities": "San Francisco" }", "invalid": true, "toolCallId": "call-1", "toolName": "cityAttractions", "type": "tool-call", }, { "dynamic": true, "error": "Invalid input for tool cityAttractions: Type validation failed: Value: {"cities":"San Francisco"}. Error message: [ { "expected": "string", "code": "invalid_type", "path": [ "city" ], "message": "Invalid input: expected string, received undefined" } ]", "input": "{ "cities": "San Francisco" }", "toolCallId": "call-1", "toolName": "cityAttractions", "type": "tool-error", }, ] `); }); it('should add tool call and result error parts to the full stream', async () => { expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "dynamic": false, "id": "call-1", "toolName": "cityAttractions", "type": "tool-input-start", }, { "delta": "{ "cities": "San Francisco" }", "id": "call-1", "type": "tool-input-delta", }, { "id": "call-1", "type": "tool-input-end", }, { "dynamic": true, "error": [AI_InvalidToolInputError: Invalid input for tool cityAttractions: Type validation failed: Value: {"cities":"San Francisco"}. Error message: [ { "expected": "string", "code": "invalid_type", "path": [ "city" ], "message": "Invalid input: expected string, received undefined" } ]], "input": "{ "cities": "San Francisco" }", "invalid": true, "toolCallId": "call-1", "toolName": "cityAttractions", "type": "tool-call", }, { "dynamic": true, "error": "Invalid input for tool cityAttractions: Type validation failed: Value: {"cities":"San Francisco"}. Error message: [ { "expected": "string", "code": "invalid_type", "path": [ "city" ], "message": "Invalid input: expected string, received undefined" } ]", "input": "{ "cities": "San Francisco" }", "toolCallId": "call-1", "toolName": "cityAttractions", "type": "tool-error", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:02.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 3, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); it('should add tool call and result error parts to the ui message stream', async () => { expect(await convertAsyncIterableToArray(result.toUIMessageStream())) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "toolCallId": "call-1", "toolName": "cityAttractions", "type": "tool-input-start", }, { "inputTextDelta": "{ "cities": "San Francisco" }", "toolCallId": "call-1", "type": "tool-input-delta", }, { "errorText": "Invalid input for tool cityAttractions: Type validation failed: Value: {"cities":"San Francisco"}. Error message: [ { "expected": "string", "code": "invalid_type", "path": [ "city" ], "message": "Invalid input: expected string, received undefined" } ]", "input": "{ "cities": "San Francisco" }", "toolCallId": "call-1", "toolName": "cityAttractions", "type": "tool-input-error", }, { "errorText": "Invalid input for tool cityAttractions: Type validation failed: Value: {"cities":"San Francisco"}. Error message: [ { "expected": "string", "code": "invalid_type", "path": [ "city" ], "message": "Invalid input: expected string, received undefined" } ]", "toolCallId": "call-1", "type": "tool-output-error", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); }); }); }); --- File: /ai/packages/ai/src/generate-text/stream-text.ts --- import { getErrorMessage, LanguageModelV2, LanguageModelV2CallWarning, } from '@ai-sdk/provider'; import { createIdGenerator, IdGenerator, isAbortError, ProviderOptions, } from '@ai-sdk/provider-utils'; import { Span } from '@opentelemetry/api'; import { ServerResponse } from 'node:http'; import { NoOutputSpecifiedError } from '../error/no-output-specified-error'; import { resolveLanguageModel } from '../model/resolve-model'; import { CallSettings } from '../prompt/call-settings'; import { convertToLanguageModelPrompt } from '../prompt/convert-to-language-model-prompt'; import { prepareCallSettings } from '../prompt/prepare-call-settings'; import { prepareToolsAndToolChoice } from '../prompt/prepare-tools-and-tool-choice'; import { Prompt } from '../prompt/prompt'; import { standardizePrompt } from '../prompt/standardize-prompt'; import { wrapGatewayError } from '../prompt/wrap-gateway-error'; import { assembleOperationName } from '../telemetry/assemble-operation-name'; import { getBaseTelemetryAttributes } from '../telemetry/get-base-telemetry-attributes'; import { getTracer } from '../telemetry/get-tracer'; import { recordSpan } from '../telemetry/record-span'; import { selectTelemetryAttributes } from '../telemetry/select-telemetry-attributes'; import { stringifyForTelemetry } from '../telemetry/stringify-for-telemetry'; import { TelemetrySettings } from '../telemetry/telemetry-settings'; import { createTextStreamResponse } from '../text-stream/create-text-stream-response'; import { pipeTextStreamToResponse } from '../text-stream/pipe-text-stream-to-response'; import { LanguageModelRequestMetadata } from '../types'; import { CallWarning, FinishReason, LanguageModel, ToolChoice, } from '../types/language-model'; import { ProviderMetadata } from '../types/provider-metadata'; import { addLanguageModelUsage, LanguageModelUsage } from '../types/usage'; import { UIMessage } from '../ui'; import { createUIMessageStreamResponse } from '../ui-message-stream/create-ui-message-stream-response'; import { getResponseUIMessageId } from '../ui-message-stream/get-response-ui-message-id'; import { handleUIMessageStreamFinish } from '../ui-message-stream/handle-ui-message-stream-finish'; import { pipeUIMessageStreamToResponse } from '../ui-message-stream/pipe-ui-message-stream-to-response'; import { InferUIMessageChunk, UIMessageChunk, } from '../ui-message-stream/ui-message-chunks'; import { UIMessageStreamResponseInit } from '../ui-message-stream/ui-message-stream-response-init'; import { InferUIMessageData, InferUIMessageMetadata } from '../ui/ui-messages'; import { asArray } from '../util/as-array'; import { AsyncIterableStream, createAsyncIterableStream, } from '../util/async-iterable-stream'; import { consumeStream } from '../util/consume-stream'; import { createStitchableStream } from '../util/create-stitchable-stream'; import { DelayedPromise } from '../util/delayed-promise'; import { filterStreamErrors } from '../util/filter-stream-errors'; import { now as originalNow } from '../util/now'; import { prepareRetries } from '../util/prepare-retries'; import { ContentPart } from './content-part'; import { Output } from './output'; import { PrepareStepFunction } from './prepare-step'; import { ResponseMessage } from './response-message'; import { runToolsTransformation, SingleRequestTextStreamPart, } from './run-tools-transformation'; import { DefaultStepResult, StepResult } from './step-result'; import { isStopConditionMet, stepCountIs, StopCondition, } from './stop-condition'; import { ConsumeStreamOptions, StreamTextResult, TextStreamPart, UIMessageStreamOptions, } from './stream-text-result'; import { toResponseMessages } from './to-response-messages'; import { TypedToolCall } from './tool-call'; import { ToolCallRepairFunction } from './tool-call-repair-function'; import { ToolOutput } from './tool-output'; import { ToolSet } from './tool-set'; const originalGenerateId = createIdGenerator({ prefix: 'aitxt', size: 24, }); /** A transformation that is applied to the stream. @param stopStream - A function that stops the source stream. @param tools - The tools that are accessible to and can be called by the model. The model needs to support calling tools. */ export type StreamTextTransform<TOOLS extends ToolSet> = (options: { tools: TOOLS; // for type inference stopStream: () => void; }) => TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>; /** Callback that is set using the `onError` option. @param event - The event that is passed to the callback. */ export type StreamTextOnErrorCallback = (event: { error: unknown; }) => PromiseLike<void> | void; /** Callback that is set using the `onStepFinish` option. @param stepResult - The result of the step. */ export type StreamTextOnStepFinishCallback<TOOLS extends ToolSet> = ( stepResult: StepResult<TOOLS>, ) => PromiseLike<void> | void; /** Callback that is set using the `onChunk` option. @param event - The event that is passed to the callback. */ export type StreamTextOnChunkCallback<TOOLS extends ToolSet> = (event: { chunk: Extract< TextStreamPart<TOOLS>, { type: | 'text-delta' | 'reasoning-delta' | 'source' | 'tool-call' | 'tool-input-start' | 'tool-input-delta' | 'tool-result' | 'raw'; } >; }) => PromiseLike<void> | void; /** Callback that is set using the `onFinish` option. @param event - The event that is passed to the callback. */ export type StreamTextOnFinishCallback<TOOLS extends ToolSet> = ( event: StepResult<TOOLS> & { /** Details for all steps. */ readonly steps: StepResult<TOOLS>[]; /** Total usage for all steps. This is the sum of the usage of all steps. */ readonly totalUsage: LanguageModelUsage; }, ) => PromiseLike<void> | void; /** Callback that is set using the `onAbort` option. @param event - The event that is passed to the callback. */ export type StreamTextOnAbortCallback<TOOLS extends ToolSet> = (event: { /** Details for all previously finished steps. */ readonly steps: StepResult<TOOLS>[]; }) => PromiseLike<void> | void; /** Generate a text and call tools for a given prompt using a language model. This function streams the output. If you do not want to stream the output, use `generateText` instead. @param model - The language model to use. @param tools - Tools that are accessible to and can be called by the model. The model needs to support calling tools. @param system - A system message that will be part of the prompt. @param prompt - A simple text prompt. You can either use `prompt` or `messages` but not both. @param messages - A list of messages. You can either use `prompt` or `messages` but not both. @param maxOutputTokens - Maximum number of tokens to generate. @param temperature - Temperature setting. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both. @param topP - Nucleus sampling. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both. @param topK - Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature. @param presencePenalty - Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The value is passed through to the provider. The range depends on the provider and model. @param frequencyPenalty - Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The value is passed through to the provider. The range depends on the provider and model. @param stopSequences - Stop sequences. If set, the model will stop generating text when one of the stop sequences is generated. @param seed - The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results. @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2. @param abortSignal - An optional abort signal that can be used to cancel the call. @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. @param maxSteps - Maximum number of sequential LLM calls (steps), e.g. when you use tool calls. @param onChunk - Callback that is called for each chunk of the stream. The stream processing will pause until the callback promise is resolved. @param onError - Callback that is called when an error occurs during streaming. You can use it to log errors. @param onStepFinish - Callback that is called when each step (LLM call) is finished, including intermediate steps. @param onFinish - Callback that is called when the LLM response and all request tool executions (for tools that have an `execute` function) are finished. @return A result object for accessing different stream types and additional information. */ export function streamText< TOOLS extends ToolSet, OUTPUT = never, PARTIAL_OUTPUT = never, >({ model, tools, toolChoice, system, prompt, messages, maxRetries, abortSignal, headers, stopWhen = stepCountIs(1), experimental_output: output, experimental_telemetry: telemetry, prepareStep, providerOptions, experimental_activeTools, activeTools = experimental_activeTools, experimental_repairToolCall: repairToolCall, experimental_transform: transform, includeRawChunks = false, onChunk, onError = ({ error }) => { console.error(error); }, onFinish, onAbort, onStepFinish, experimental_context, _internal: { now = originalNow, generateId = originalGenerateId, currentDate = () => new Date(), } = {}, ...settings }: CallSettings & Prompt & { /** The language model to use. */ model: LanguageModel; /** The tools that the model can call. The model needs to support calling tools. */ tools?: TOOLS; /** The tool choice strategy. Default: 'auto'. */ toolChoice?: ToolChoice<TOOLS>; /** Condition for stopping the generation when there are tool results in the last step. When the condition is an array, any of the conditions can be met to stop the generation. @default stepCountIs(1) */ stopWhen?: | StopCondition<NoInfer<TOOLS>> | Array<StopCondition<NoInfer<TOOLS>>>; /** Optional telemetry configuration (experimental). */ experimental_telemetry?: TelemetrySettings; /** Additional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; /** * @deprecated Use `activeTools` instead. */ experimental_activeTools?: Array<keyof NoInfer<TOOLS>>; /** Limits the tools that are available for the model to call without changing the tool call and result types in the result. */ activeTools?: Array<keyof NoInfer<TOOLS>>; /** Optional specification for parsing structured outputs from the LLM response. */ experimental_output?: Output<OUTPUT, PARTIAL_OUTPUT>; /** Optional function that you can use to provide different settings for a step. @param options - The options for the step. @param options.steps - The steps that have been executed so far. @param options.stepNumber - The number of the step that is being executed. @param options.model - The model that is being used. @returns An object that contains the settings for the step. If you return undefined (or for undefined settings), the settings from the outer level will be used. */ prepareStep?: PrepareStepFunction<NoInfer<TOOLS>>; /** A function that attempts to repair a tool call that failed to parse. */ experimental_repairToolCall?: ToolCallRepairFunction<TOOLS>; /** Optional stream transformations. They are applied in the order they are provided. The stream transformations must maintain the stream structure for streamText to work correctly. */ experimental_transform?: | StreamTextTransform<TOOLS> | Array<StreamTextTransform<TOOLS>>; /** Whether to include raw chunks from the provider in the stream. When enabled, you will receive raw chunks with type 'raw' that contain the unprocessed data from the provider. This allows access to cutting-edge provider features not yet wrapped by the AI SDK. Defaults to false. */ includeRawChunks?: boolean; /** Callback that is called for each chunk of the stream. The stream processing will pause until the callback promise is resolved. */ onChunk?: StreamTextOnChunkCallback<TOOLS>; /** Callback that is invoked when an error occurs during streaming. You can use it to log errors. The stream processing will pause until the callback promise is resolved. */ onError?: StreamTextOnErrorCallback; /** Callback that is called when the LLM response and all request tool executions (for tools that have an `execute` function) are finished. The usage is the combined usage of all steps. */ onFinish?: StreamTextOnFinishCallback<TOOLS>; onAbort?: StreamTextOnAbortCallback<TOOLS>; /** Callback that is called when each step (LLM call) is finished, including intermediate steps. */ onStepFinish?: StreamTextOnStepFinishCallback<TOOLS>; /** * Context that is passed into tool execution. * * Experimental (can break in patch releases). * * @default undefined */ experimental_context?: unknown; /** Internal. For test use only. May change without notice. */ _internal?: { now?: () => number; generateId?: IdGenerator; currentDate?: () => Date; }; }): StreamTextResult<TOOLS, PARTIAL_OUTPUT> { return new DefaultStreamTextResult<TOOLS, OUTPUT, PARTIAL_OUTPUT>({ model: resolveLanguageModel(model), telemetry, headers, settings, maxRetries, abortSignal, system, prompt, messages, tools, toolChoice, transforms: asArray(transform), activeTools, repairToolCall, stopConditions: asArray(stopWhen), output, providerOptions, prepareStep, includeRawChunks, onChunk, onError, onFinish, onAbort, onStepFinish, now, currentDate, generateId, experimental_context, }); } type EnrichedStreamPart<TOOLS extends ToolSet, PARTIAL_OUTPUT> = { part: TextStreamPart<TOOLS>; partialOutput: PARTIAL_OUTPUT | undefined; }; function createOutputTransformStream< TOOLS extends ToolSet, OUTPUT, PARTIAL_OUTPUT, >( output: Output<OUTPUT, PARTIAL_OUTPUT> | undefined, ): TransformStream< TextStreamPart<TOOLS>, EnrichedStreamPart<TOOLS, PARTIAL_OUTPUT> > { if (!output) { return new TransformStream< TextStreamPart<TOOLS>, EnrichedStreamPart<TOOLS, PARTIAL_OUTPUT> >({ transform(chunk, controller) { controller.enqueue({ part: chunk, partialOutput: undefined }); }, }); } let firstTextChunkId: string | undefined = undefined; let text = ''; let textChunk = ''; let lastPublishedJson = ''; function publishTextChunk({ controller, partialOutput = undefined, }: { controller: TransformStreamDefaultController< EnrichedStreamPart<TOOLS, PARTIAL_OUTPUT> >; partialOutput?: PARTIAL_OUTPUT; }) { controller.enqueue({ part: { type: 'text-delta', id: firstTextChunkId!, text: textChunk, }, partialOutput, }); textChunk = ''; } return new TransformStream< TextStreamPart<TOOLS>, EnrichedStreamPart<TOOLS, PARTIAL_OUTPUT> >({ async transform(chunk, controller) { // ensure that we publish the last text chunk before the step finish: if (chunk.type === 'finish-step' && textChunk.length > 0) { publishTextChunk({ controller }); } if ( chunk.type !== 'text-delta' && chunk.type !== 'text-start' && chunk.type !== 'text-end' ) { controller.enqueue({ part: chunk, partialOutput: undefined }); return; } // we have to pick a text chunk which contains the json text // since we are streaming, we have to pick the first text chunk if (firstTextChunkId == null) { firstTextChunkId = chunk.id; } else if (chunk.id !== firstTextChunkId) { controller.enqueue({ part: chunk, partialOutput: undefined }); return; } if (chunk.type === 'text-start') { controller.enqueue({ part: chunk, partialOutput: undefined }); return; } if (chunk.type === 'text-end') { if (textChunk.length > 0) { publishTextChunk({ controller }); } controller.enqueue({ part: chunk, partialOutput: undefined }); return; } text += chunk.text; textChunk += chunk.text; // only publish if partial json can be parsed: const result = await output.parsePartial({ text }); if (result != null) { // only send new json if it has changed: const currentJson = JSON.stringify(result.partial); if (currentJson !== lastPublishedJson) { publishTextChunk({ controller, partialOutput: result.partial }); lastPublishedJson = currentJson; } } }, }); } class DefaultStreamTextResult<TOOLS extends ToolSet, OUTPUT, PARTIAL_OUTPUT> implements StreamTextResult<TOOLS, PARTIAL_OUTPUT> { private readonly _totalUsage = new DelayedPromise< Awaited<StreamTextResult<TOOLS, PARTIAL_OUTPUT>['usage']> >(); private readonly _finishReason = new DelayedPromise< Awaited<StreamTextResult<TOOLS, PARTIAL_OUTPUT>['finishReason']> >(); private readonly _steps = new DelayedPromise< Awaited<StreamTextResult<TOOLS, PARTIAL_OUTPUT>['steps']> >(); private readonly addStream: ( stream: ReadableStream<TextStreamPart<TOOLS>>, ) => void; private readonly closeStream: () => void; private baseStream: ReadableStream<EnrichedStreamPart<TOOLS, PARTIAL_OUTPUT>>; private output: Output<OUTPUT, PARTIAL_OUTPUT> | undefined; private includeRawChunks: boolean; private tools: TOOLS | undefined; constructor({ model, telemetry, headers, settings, maxRetries: maxRetriesArg, abortSignal, system, prompt, messages, tools, toolChoice, transforms, activeTools, repairToolCall, stopConditions, output, providerOptions, prepareStep, includeRawChunks, now, currentDate, generateId, onChunk, onError, onFinish, onAbort, onStepFinish, experimental_context, }: { model: LanguageModelV2; telemetry: TelemetrySettings | undefined; headers: Record<string, string | undefined> | undefined; settings: Omit<CallSettings, 'abortSignal' | 'headers'>; maxRetries: number | undefined; abortSignal: AbortSignal | undefined; system: Prompt['system']; prompt: Prompt['prompt']; messages: Prompt['messages']; tools: TOOLS | undefined; toolChoice: ToolChoice<TOOLS> | undefined; transforms: Array<StreamTextTransform<TOOLS>>; activeTools: Array<keyof TOOLS> | undefined; repairToolCall: ToolCallRepairFunction<TOOLS> | undefined; stopConditions: Array<StopCondition<NoInfer<TOOLS>>>; output: Output<OUTPUT, PARTIAL_OUTPUT> | undefined; providerOptions: ProviderOptions | undefined; prepareStep: PrepareStepFunction<NoInfer<TOOLS>> | undefined; includeRawChunks: boolean; now: () => number; currentDate: () => Date; generateId: () => string; experimental_context: unknown; // callbacks: onChunk: undefined | StreamTextOnChunkCallback<TOOLS>; onError: StreamTextOnErrorCallback; onFinish: undefined | StreamTextOnFinishCallback<TOOLS>; onAbort: undefined | StreamTextOnAbortCallback<TOOLS>; onStepFinish: undefined | StreamTextOnStepFinishCallback<TOOLS>; }) { this.output = output; this.includeRawChunks = includeRawChunks; this.tools = tools; // promise to ensure that the step has been fully processed by the event processor // before a new step is started. This is required because the continuation condition // needs the updated steps to determine if another step is needed. let stepFinish!: DelayedPromise<void>; let recordedContent: Array<ContentPart<TOOLS>> = []; const recordedResponseMessages: Array<ResponseMessage> = []; let recordedFinishReason: FinishReason | undefined = undefined; let recordedTotalUsage: LanguageModelUsage | undefined = undefined; let recordedRequest: LanguageModelRequestMetadata = {}; let recordedWarnings: Array<CallWarning> = []; const recordedSteps: StepResult<TOOLS>[] = []; let rootSpan!: Span; let activeTextContent: Record< string, { type: 'text'; text: string; providerMetadata: ProviderMetadata | undefined; } > = {}; let activeReasoningContent: Record< string, { type: 'reasoning'; text: string; providerMetadata: ProviderMetadata | undefined; } > = {}; const eventProcessor = new TransformStream< EnrichedStreamPart<TOOLS, PARTIAL_OUTPUT>, EnrichedStreamPart<TOOLS, PARTIAL_OUTPUT> >({ async transform(chunk, controller) { controller.enqueue(chunk); // forward the chunk to the next stream const { part } = chunk; if ( part.type === 'text-delta' || part.type === 'reasoning-delta' || part.type === 'source' || part.type === 'tool-call' || part.type === 'tool-result' || part.type === 'tool-input-start' || part.type === 'tool-input-delta' || part.type === 'raw' ) { await onChunk?.({ chunk: part }); } if (part.type === 'error') { await onError({ error: wrapGatewayError(part.error) }); } if (part.type === 'text-start') { activeTextContent[part.id] = { type: 'text', text: '', providerMetadata: part.providerMetadata, }; recordedContent.push(activeTextContent[part.id]); } if (part.type === 'text-delta') { const activeText = activeTextContent[part.id]; if (activeText == null) { controller.enqueue({ part: { type: 'error', error: `text part ${part.id} not found`, }, partialOutput: undefined, }); return; } activeText.text += part.text; activeText.providerMetadata = part.providerMetadata ?? activeText.providerMetadata; } if (part.type === 'text-end') { delete activeTextContent[part.id]; } if (part.type === 'reasoning-start') { activeReasoningContent[part.id] = { type: 'reasoning', text: '', providerMetadata: part.providerMetadata, }; recordedContent.push(activeReasoningContent[part.id]); } if (part.type === 'reasoning-delta') { const activeReasoning = activeReasoningContent[part.id]; if (activeReasoning == null) { controller.enqueue({ part: { type: 'error', error: `reasoning part ${part.id} not found`, }, partialOutput: undefined, }); return; } activeReasoning.text += part.text; activeReasoning.providerMetadata = part.providerMetadata ?? activeReasoning.providerMetadata; } if (part.type === 'reasoning-end') { const activeReasoning = activeReasoningContent[part.id]; if (activeReasoning == null) { controller.enqueue({ part: { type: 'error', error: `reasoning part ${part.id} not found`, }, partialOutput: undefined, }); return; } activeReasoning.providerMetadata = part.providerMetadata ?? activeReasoning.providerMetadata; delete activeReasoningContent[part.id]; } if (part.type === 'file') { recordedContent.push({ type: 'file', file: part.file }); } if (part.type === 'source') { recordedContent.push(part); } if (part.type === 'tool-call') { recordedContent.push(part); } if (part.type === 'tool-result') { recordedContent.push(part); } if (part.type === 'tool-error') { recordedContent.push(part); } if (part.type === 'start-step') { recordedRequest = part.request; recordedWarnings = part.warnings; } if (part.type === 'finish-step') { const stepMessages = toResponseMessages({ content: recordedContent, tools, }); // Add step information (after response messages are updated): const currentStepResult: StepResult<TOOLS> = new DefaultStepResult({ content: recordedContent, finishReason: part.finishReason, usage: part.usage, warnings: recordedWarnings, request: recordedRequest, response: { ...part.response, messages: [...recordedResponseMessages, ...stepMessages], }, providerMetadata: part.providerMetadata, }); await onStepFinish?.(currentStepResult); recordedSteps.push(currentStepResult); recordedContent = []; activeReasoningContent = {}; activeTextContent = {}; recordedResponseMessages.push(...stepMessages); // resolve the promise to signal that the step has been fully processed // by the event processor: stepFinish.resolve(); } if (part.type === 'finish') { recordedTotalUsage = part.totalUsage; recordedFinishReason = part.finishReason; } }, async flush(controller) { try { if (recordedSteps.length === 0) { return; // no steps recorded (e.g. in error scenario) } // derived: const finishReason = recordedFinishReason ?? 'unknown'; const totalUsage = recordedTotalUsage ?? { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, }; // from finish: self._finishReason.resolve(finishReason); self._totalUsage.resolve(totalUsage); // aggregate results: self._steps.resolve(recordedSteps); // call onFinish callback: const finalStep = recordedSteps[recordedSteps.length - 1]; await onFinish?.({ finishReason, totalUsage, usage: finalStep.usage, content: finalStep.content, text: finalStep.text, reasoningText: finalStep.reasoningText, reasoning: finalStep.reasoning, files: finalStep.files, sources: finalStep.sources, toolCalls: finalStep.toolCalls, staticToolCalls: finalStep.staticToolCalls, dynamicToolCalls: finalStep.dynamicToolCalls, toolResults: finalStep.toolResults, staticToolResults: finalStep.staticToolResults, dynamicToolResults: finalStep.dynamicToolResults, request: finalStep.request, response: finalStep.response, warnings: finalStep.warnings, providerMetadata: finalStep.providerMetadata, steps: recordedSteps, }); // Add response information to the root span: rootSpan.setAttributes( selectTelemetryAttributes({ telemetry, attributes: { 'ai.response.finishReason': finishReason, 'ai.response.text': { output: () => finalStep.text }, 'ai.response.toolCalls': { output: () => finalStep.toolCalls?.length ? JSON.stringify(finalStep.toolCalls) : undefined, }, 'ai.response.providerMetadata': JSON.stringify( finalStep.providerMetadata, ), 'ai.usage.inputTokens': totalUsage.inputTokens, 'ai.usage.outputTokens': totalUsage.outputTokens, 'ai.usage.totalTokens': totalUsage.totalTokens, 'ai.usage.reasoningTokens': totalUsage.reasoningTokens, 'ai.usage.cachedInputTokens': totalUsage.cachedInputTokens, }, }), ); } catch (error) { controller.error(error); } finally { rootSpan.end(); } }, }); // initialize the stitchable stream and the transformed stream: const stitchableStream = createStitchableStream<TextStreamPart<TOOLS>>(); this.addStream = stitchableStream.addStream; this.closeStream = stitchableStream.close; let stream = stitchableStream.stream; // filter out abort errors: stream = filterStreamErrors(stream, ({ error, controller }) => { if (isAbortError(error) && abortSignal?.aborted) { onAbort?.({ steps: recordedSteps }); controller.enqueue({ type: 'abort' }); controller.close(); } else { controller.error(error); } }); // add a stream that emits a start event: stream = stream.pipeThrough( new TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>({ start(controller) { controller.enqueue({ type: 'start' }); }, }), ); // transform the stream before output parsing // to enable replacement of stream segments: for (const transform of transforms) { stream = stream.pipeThrough( transform({ tools: tools as TOOLS, stopStream() { stitchableStream.terminate(); }, }), ); } this.baseStream = stream .pipeThrough(createOutputTransformStream(output)) .pipeThrough(eventProcessor); const { maxRetries, retry } = prepareRetries({ maxRetries: maxRetriesArg, abortSignal, }); const tracer = getTracer(telemetry); const callSettings = prepareCallSettings(settings); const baseTelemetryAttributes = getBaseTelemetryAttributes({ model, telemetry, headers, settings: { ...callSettings, maxRetries }, }); const self = this; recordSpan({ name: 'ai.streamText', attributes: selectTelemetryAttributes({ telemetry, attributes: { ...assembleOperationName({ operationId: 'ai.streamText', telemetry }), ...baseTelemetryAttributes, // specific settings that only make sense on the outer level: 'ai.prompt': { input: () => JSON.stringify({ system, prompt, messages }), }, }, }), tracer, endWhenDone: false, fn: async rootSpanArg => { rootSpan = rootSpanArg; async function streamStep({ currentStep, responseMessages, usage, }: { currentStep: number; responseMessages: Array<ResponseMessage>; usage: LanguageModelUsage; }) { const includeRawChunks = self.includeRawChunks; stepFinish = new DelayedPromise<void>(); const initialPrompt = await standardizePrompt({ system, prompt, messages, }); const stepInputMessages = [ ...initialPrompt.messages, ...responseMessages, ]; const prepareStepResult = await prepareStep?.({ model, steps: recordedSteps, stepNumber: recordedSteps.length, messages: stepInputMessages, }); const promptMessages = await convertToLanguageModelPrompt({ prompt: { system: prepareStepResult?.system ?? initialPrompt.system, messages: prepareStepResult?.messages ?? stepInputMessages, }, supportedUrls: await model.supportedUrls, }); const stepModel = resolveLanguageModel( prepareStepResult?.model ?? model, ); const { toolChoice: stepToolChoice, tools: stepTools } = prepareToolsAndToolChoice({ tools, toolChoice: prepareStepResult?.toolChoice ?? toolChoice, activeTools: prepareStepResult?.activeTools ?? activeTools, }); const { result: { stream, response, request }, doStreamSpan, startTimestampMs, } = await retry(() => recordSpan({ name: 'ai.streamText.doStream', attributes: selectTelemetryAttributes({ telemetry, attributes: { ...assembleOperationName({ operationId: 'ai.streamText.doStream', telemetry, }), ...baseTelemetryAttributes, // model: 'ai.model.provider': stepModel.provider, 'ai.model.id': stepModel.modelId, // prompt: 'ai.prompt.messages': { input: () => stringifyForTelemetry(promptMessages), }, 'ai.prompt.tools': { // convert the language model level tools: input: () => stepTools?.map(tool => JSON.stringify(tool)), }, 'ai.prompt.toolChoice': { input: () => stepToolChoice != null ? JSON.stringify(stepToolChoice) : undefined, }, // standardized gen-ai llm span attributes: 'gen_ai.system': stepModel.provider, 'gen_ai.request.model': stepModel.modelId, 'gen_ai.request.frequency_penalty': callSettings.frequencyPenalty, 'gen_ai.request.max_tokens': callSettings.maxOutputTokens, 'gen_ai.request.presence_penalty': callSettings.presencePenalty, 'gen_ai.request.stop_sequences': callSettings.stopSequences, 'gen_ai.request.temperature': callSettings.temperature, 'gen_ai.request.top_k': callSettings.topK, 'gen_ai.request.top_p': callSettings.topP, }, }), tracer, endWhenDone: false, fn: async doStreamSpan => { return { startTimestampMs: now(), // get before the call doStreamSpan, result: await stepModel.doStream({ ...callSettings, tools: stepTools, toolChoice: stepToolChoice, responseFormat: output?.responseFormat, prompt: promptMessages, providerOptions, abortSignal, headers, includeRawChunks, }), }; }, }), ); const streamWithToolResults = runToolsTransformation({ tools, generatorStream: stream, tracer, telemetry, system, messages: stepInputMessages, repairToolCall, abortSignal, experimental_context, }); const stepRequest = request ?? {}; const stepToolCalls: TypedToolCall<TOOLS>[] = []; const stepToolOutputs: ToolOutput<TOOLS>[] = []; let warnings: LanguageModelV2CallWarning[] | undefined; const activeToolCallToolNames: Record<string, string> = {}; let stepFinishReason: FinishReason = 'unknown'; let stepUsage: LanguageModelUsage = { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, }; let stepProviderMetadata: ProviderMetadata | undefined; let stepFirstChunk = true; let stepResponse: { id: string; timestamp: Date; modelId: string } = { id: generateId(), timestamp: currentDate(), modelId: model.modelId, }; // raw text as it comes from the provider. recorded for telemetry. let activeText = ''; self.addStream( streamWithToolResults.pipeThrough( new TransformStream< SingleRequestTextStreamPart<TOOLS>, TextStreamPart<TOOLS> >({ async transform(chunk, controller): Promise<void> { if (chunk.type === 'stream-start') { warnings = chunk.warnings; return; // stream start chunks are sent immediately and do not count as first chunk } if (stepFirstChunk) { // Telemetry for first chunk: const msToFirstChunk = now() - startTimestampMs; stepFirstChunk = false; doStreamSpan.addEvent('ai.stream.firstChunk', { 'ai.response.msToFirstChunk': msToFirstChunk, }); doStreamSpan.setAttributes({ 'ai.response.msToFirstChunk': msToFirstChunk, }); // Step start: controller.enqueue({ type: 'start-step', request: stepRequest, warnings: warnings ?? [], }); } const chunkType = chunk.type; switch (chunkType) { case 'text-start': case 'text-end': { controller.enqueue(chunk); break; } case 'text-delta': { if (chunk.delta.length > 0) { controller.enqueue({ type: 'text-delta', id: chunk.id, text: chunk.delta, providerMetadata: chunk.providerMetadata, }); activeText += chunk.delta; } break; } case 'reasoning-start': case 'reasoning-end': { controller.enqueue(chunk); break; } case 'reasoning-delta': { controller.enqueue({ type: 'reasoning-delta', id: chunk.id, text: chunk.delta, providerMetadata: chunk.providerMetadata, }); break; } case 'tool-call': { controller.enqueue(chunk); // store tool calls for onFinish callback and toolCalls promise: stepToolCalls.push(chunk); break; } case 'tool-result': { controller.enqueue(chunk); stepToolOutputs.push(chunk); break; } case 'tool-error': { controller.enqueue(chunk); stepToolOutputs.push(chunk); break; } case 'response-metadata': { stepResponse = { id: chunk.id ?? stepResponse.id, timestamp: chunk.timestamp ?? stepResponse.timestamp, modelId: chunk.modelId ?? stepResponse.modelId, }; break; } case 'finish': { // Note: tool executions might not be finished yet when the finish event is emitted. // store usage and finish reason for promises and onFinish callback: stepUsage = chunk.usage; stepFinishReason = chunk.finishReason; stepProviderMetadata = chunk.providerMetadata; // Telemetry for finish event timing // (since tool executions can take longer and distort calculations) const msToFinish = now() - startTimestampMs; doStreamSpan.addEvent('ai.stream.finish'); doStreamSpan.setAttributes({ 'ai.response.msToFinish': msToFinish, 'ai.response.avgOutputTokensPerSecond': (1000 * (stepUsage.outputTokens ?? 0)) / msToFinish, }); break; } case 'file': { controller.enqueue(chunk); break; } case 'source': { controller.enqueue(chunk); break; } case 'tool-input-start': { activeToolCallToolNames[chunk.id] = chunk.toolName; const tool = tools?.[chunk.toolName]; if (tool?.onInputStart != null) { await tool.onInputStart({ toolCallId: chunk.id, messages: stepInputMessages, abortSignal, experimental_context, }); } controller.enqueue({ ...chunk, dynamic: tool?.type === 'dynamic', }); break; } case 'tool-input-end': { delete activeToolCallToolNames[chunk.id]; controller.enqueue(chunk); break; } case 'tool-input-delta': { const toolName = activeToolCallToolNames[chunk.id]; const tool = tools?.[toolName]; if (tool?.onInputDelta != null) { await tool.onInputDelta({ inputTextDelta: chunk.delta, toolCallId: chunk.id, messages: stepInputMessages, abortSignal, experimental_context, }); } controller.enqueue(chunk); break; } case 'error': { controller.enqueue(chunk); stepFinishReason = 'error'; break; } case 'raw': { if (includeRawChunks) { controller.enqueue(chunk); } break; } default: { const exhaustiveCheck: never = chunkType; throw new Error(`Unknown chunk type: ${exhaustiveCheck}`); } } }, // invoke onFinish callback and resolve toolResults promise when the stream is about to close: async flush(controller) { const stepToolCallsJson = stepToolCalls.length > 0 ? JSON.stringify(stepToolCalls) : undefined; // record telemetry information first to ensure best effort timing try { doStreamSpan.setAttributes( selectTelemetryAttributes({ telemetry, attributes: { 'ai.response.finishReason': stepFinishReason, 'ai.response.text': { output: () => activeText, }, 'ai.response.toolCalls': { output: () => stepToolCallsJson, }, 'ai.response.id': stepResponse.id, 'ai.response.model': stepResponse.modelId, 'ai.response.timestamp': stepResponse.timestamp.toISOString(), 'ai.response.providerMetadata': JSON.stringify(stepProviderMetadata), 'ai.usage.inputTokens': stepUsage.inputTokens, 'ai.usage.outputTokens': stepUsage.outputTokens, 'ai.usage.totalTokens': stepUsage.totalTokens, 'ai.usage.reasoningTokens': stepUsage.reasoningTokens, 'ai.usage.cachedInputTokens': stepUsage.cachedInputTokens, // standardized gen-ai llm span attributes: 'gen_ai.response.finish_reasons': [stepFinishReason], 'gen_ai.response.id': stepResponse.id, 'gen_ai.response.model': stepResponse.modelId, 'gen_ai.usage.input_tokens': stepUsage.inputTokens, 'gen_ai.usage.output_tokens': stepUsage.outputTokens, }, }), ); } catch (error) { // ignore error setting telemetry attributes } finally { // finish doStreamSpan before other operations for correct timing: doStreamSpan.end(); } controller.enqueue({ type: 'finish-step', finishReason: stepFinishReason, usage: stepUsage, providerMetadata: stepProviderMetadata, response: { ...stepResponse, headers: response?.headers, }, }); const combinedUsage = addLanguageModelUsage(usage, stepUsage); // wait for the step to be fully processed by the event processor // to ensure that the recorded steps are complete: await stepFinish.promise; const clientToolCalls = stepToolCalls.filter( toolCall => toolCall.providerExecuted !== true, ); const clientToolOutputs = stepToolOutputs.filter( toolOutput => toolOutput.providerExecuted !== true, ); if ( clientToolCalls.length > 0 && // all current tool calls have outputs (incl. execution errors): clientToolOutputs.length === clientToolCalls.length && // continue until a stop condition is met: !(await isStopConditionMet({ stopConditions, steps: recordedSteps, })) ) { // append to messages for the next step: responseMessages.push( ...toResponseMessages({ content: // use transformed content to create the messages for the next step: recordedSteps[recordedSteps.length - 1].content, tools, }), ); try { await streamStep({ currentStep: currentStep + 1, responseMessages, usage: combinedUsage, }); } catch (error) { controller.enqueue({ type: 'error', error, }); self.closeStream(); } } else { controller.enqueue({ type: 'finish', finishReason: stepFinishReason, totalUsage: combinedUsage, }); self.closeStream(); // close the stitchable stream } }, }), ), ); } // add the initial stream to the stitchable stream await streamStep({ currentStep: 0, responseMessages: [], usage: { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, }, }); }, }).catch(error => { // add an error stream part and close the streams: self.addStream( new ReadableStream({ start(controller) { controller.enqueue({ type: 'error', error }); controller.close(); }, }), ); self.closeStream(); }); } get steps() { return this._steps.promise; } private get finalStep() { return this.steps.then(steps => steps[steps.length - 1]); } get content() { return this.finalStep.then(step => step.content); } get warnings() { return this.finalStep.then(step => step.warnings); } get providerMetadata() { return this.finalStep.then(step => step.providerMetadata); } get text() { return this.finalStep.then(step => step.text); } get reasoningText() { return this.finalStep.then(step => step.reasoningText); } get reasoning() { return this.finalStep.then(step => step.reasoning); } get sources() { return this.finalStep.then(step => step.sources); } get files() { return this.finalStep.then(step => step.files); } get toolCalls() { return this.finalStep.then(step => step.toolCalls); } get staticToolCalls() { return this.finalStep.then(step => step.staticToolCalls); } get dynamicToolCalls() { return this.finalStep.then(step => step.dynamicToolCalls); } get toolResults() { return this.finalStep.then(step => step.toolResults); } get staticToolResults() { return this.finalStep.then(step => step.staticToolResults); } get dynamicToolResults() { return this.finalStep.then(step => step.dynamicToolResults); } get usage() { return this.finalStep.then(step => step.usage); } get request() { return this.finalStep.then(step => step.request); } get response() { return this.finalStep.then(step => step.response); } get totalUsage() { return this._totalUsage.promise; } get finishReason() { return this._finishReason.promise; } /** Split out a new stream from the original stream. The original stream is replaced to allow for further splitting, since we do not know how many times the stream will be split. Note: this leads to buffering the stream content on the server. However, the LLM results are expected to be small enough to not cause issues. */ private teeStream() { const [stream1, stream2] = this.baseStream.tee(); this.baseStream = stream2; return stream1; } get textStream(): AsyncIterableStream<string> { return createAsyncIterableStream( this.teeStream().pipeThrough( new TransformStream<EnrichedStreamPart<TOOLS, PARTIAL_OUTPUT>, string>({ transform({ part }, controller) { if (part.type === 'text-delta') { controller.enqueue(part.text); } }, }), ), ); } get fullStream(): AsyncIterableStream<TextStreamPart<TOOLS>> { return createAsyncIterableStream( this.teeStream().pipeThrough( new TransformStream< EnrichedStreamPart<TOOLS, PARTIAL_OUTPUT>, TextStreamPart<TOOLS> >({ transform({ part }, controller) { controller.enqueue(part); }, }), ), ); } async consumeStream(options?: ConsumeStreamOptions): Promise<void> { try { await consumeStream({ stream: this.fullStream, onError: options?.onError, }); } catch (error) { options?.onError?.(error); } } get experimental_partialOutputStream(): AsyncIterableStream<PARTIAL_OUTPUT> { if (this.output == null) { throw new NoOutputSpecifiedError(); } return createAsyncIterableStream( this.teeStream().pipeThrough( new TransformStream< EnrichedStreamPart<TOOLS, PARTIAL_OUTPUT>, PARTIAL_OUTPUT >({ transform({ partialOutput }, controller) { if (partialOutput != null) { controller.enqueue(partialOutput); } }, }), ), ); } toUIMessageStream<UI_MESSAGE extends UIMessage>({ originalMessages, generateMessageId, onFinish, messageMetadata, sendReasoning = true, sendSources = false, sendStart = true, sendFinish = true, onError = getErrorMessage, }: UIMessageStreamOptions<UI_MESSAGE> = {}): AsyncIterableStream< InferUIMessageChunk<UI_MESSAGE> > { const responseMessageId = generateMessageId != null ? getResponseUIMessageId({ originalMessages, responseMessageId: generateMessageId, }) : undefined; const toolNamesByCallId: Record<string, string> = {}; const isDynamic = (toolCallId: string) => { const toolName = toolNamesByCallId[toolCallId]; const dynamic = this.tools?.[toolName]?.type === 'dynamic'; return dynamic ? true : undefined; // only send when dynamic to reduce data transfer }; const baseStream = this.fullStream.pipeThrough( new TransformStream< TextStreamPart<TOOLS>, UIMessageChunk< InferUIMessageMetadata<UI_MESSAGE>, InferUIMessageData<UI_MESSAGE> > >({ transform: async (part, controller) => { const messageMetadataValue = messageMetadata?.({ part }); const partType = part.type; switch (partType) { case 'text-start': { controller.enqueue({ type: 'text-start', id: part.id, ...(part.providerMetadata != null ? { providerMetadata: part.providerMetadata } : {}), }); break; } case 'text-delta': { controller.enqueue({ type: 'text-delta', id: part.id, delta: part.text, ...(part.providerMetadata != null ? { providerMetadata: part.providerMetadata } : {}), }); break; } case 'text-end': { controller.enqueue({ type: 'text-end', id: part.id, ...(part.providerMetadata != null ? { providerMetadata: part.providerMetadata } : {}), }); break; } case 'reasoning-start': { controller.enqueue({ type: 'reasoning-start', id: part.id, ...(part.providerMetadata != null ? { providerMetadata: part.providerMetadata } : {}), }); break; } case 'reasoning-delta': { if (sendReasoning) { controller.enqueue({ type: 'reasoning-delta', id: part.id, delta: part.text, ...(part.providerMetadata != null ? { providerMetadata: part.providerMetadata } : {}), }); } break; } case 'reasoning-end': { controller.enqueue({ type: 'reasoning-end', id: part.id, ...(part.providerMetadata != null ? { providerMetadata: part.providerMetadata } : {}), }); break; } case 'file': { controller.enqueue({ type: 'file', mediaType: part.file.mediaType, url: `data:${part.file.mediaType};base64,${part.file.base64}`, }); break; } case 'source': { if (sendSources && part.sourceType === 'url') { controller.enqueue({ type: 'source-url', sourceId: part.id, url: part.url, title: part.title, ...(part.providerMetadata != null ? { providerMetadata: part.providerMetadata } : {}), }); } if (sendSources && part.sourceType === 'document') { controller.enqueue({ type: 'source-document', sourceId: part.id, mediaType: part.mediaType, title: part.title, filename: part.filename, ...(part.providerMetadata != null ? { providerMetadata: part.providerMetadata } : {}), }); } break; } case 'tool-input-start': { toolNamesByCallId[part.id] = part.toolName; const dynamic = isDynamic(part.id); controller.enqueue({ type: 'tool-input-start', toolCallId: part.id, toolName: part.toolName, ...(part.providerExecuted != null ? { providerExecuted: part.providerExecuted } : {}), ...(dynamic != null ? { dynamic } : {}), }); break; } case 'tool-input-delta': { controller.enqueue({ type: 'tool-input-delta', toolCallId: part.id, inputTextDelta: part.delta, }); break; } case 'tool-call': { toolNamesByCallId[part.toolCallId] = part.toolName; const dynamic = isDynamic(part.toolCallId); if (part.invalid) { controller.enqueue({ type: 'tool-input-error', toolCallId: part.toolCallId, toolName: part.toolName, input: part.input, ...(part.providerExecuted != null ? { providerExecuted: part.providerExecuted } : {}), ...(part.providerMetadata != null ? { providerMetadata: part.providerMetadata } : {}), ...(dynamic != null ? { dynamic } : {}), errorText: onError(part.error), }); } else { controller.enqueue({ type: 'tool-input-available', toolCallId: part.toolCallId, toolName: part.toolName, input: part.input, ...(part.providerExecuted != null ? { providerExecuted: part.providerExecuted } : {}), ...(part.providerMetadata != null ? { providerMetadata: part.providerMetadata } : {}), ...(dynamic != null ? { dynamic } : {}), }); } break; } case 'tool-result': { const dynamic = isDynamic(part.toolCallId); controller.enqueue({ type: 'tool-output-available', toolCallId: part.toolCallId, output: part.output, ...(part.providerExecuted != null ? { providerExecuted: part.providerExecuted } : {}), ...(dynamic != null ? { dynamic } : {}), }); break; } case 'tool-error': { const dynamic = isDynamic(part.toolCallId); controller.enqueue({ type: 'tool-output-error', toolCallId: part.toolCallId, errorText: onError(part.error), ...(part.providerExecuted != null ? { providerExecuted: part.providerExecuted } : {}), ...(dynamic != null ? { dynamic } : {}), }); break; } case 'error': { controller.enqueue({ type: 'error', errorText: onError(part.error), }); break; } case 'start-step': { controller.enqueue({ type: 'start-step' }); break; } case 'finish-step': { controller.enqueue({ type: 'finish-step' }); break; } case 'start': { if (sendStart) { controller.enqueue({ type: 'start', ...(messageMetadataValue != null ? { messageMetadata: messageMetadataValue } : {}), ...(responseMessageId != null ? { messageId: responseMessageId } : {}), }); } break; } case 'finish': { if (sendFinish) { controller.enqueue({ type: 'finish', ...(messageMetadataValue != null ? { messageMetadata: messageMetadataValue } : {}), }); } break; } case 'abort': { controller.enqueue(part); break; } case 'tool-input-end': { break; } case 'raw': { // Raw chunks are not included in UI message streams // as they contain provider-specific data for developer use break; } default: { const exhaustiveCheck: never = partType; throw new Error(`Unknown chunk type: ${exhaustiveCheck}`); } } // start and finish events already have metadata // so we only need to send metadata for other parts if ( messageMetadataValue != null && partType !== 'start' && partType !== 'finish' ) { controller.enqueue({ type: 'message-metadata', messageMetadata: messageMetadataValue, }); } }, }), ); return createAsyncIterableStream( handleUIMessageStreamFinish<UI_MESSAGE>({ stream: baseStream, messageId: responseMessageId ?? generateMessageId?.(), originalMessages, onFinish, onError, }), ); } pipeUIMessageStreamToResponse<UI_MESSAGE extends UIMessage>( response: ServerResponse, { originalMessages, generateMessageId, onFinish, messageMetadata, sendReasoning, sendSources, sendFinish, sendStart, onError, ...init }: UIMessageStreamResponseInit & UIMessageStreamOptions<UI_MESSAGE> = {}, ) { pipeUIMessageStreamToResponse({ response, stream: this.toUIMessageStream({ originalMessages, generateMessageId, onFinish, messageMetadata, sendReasoning, sendSources, sendFinish, sendStart, onError, }), ...init, }); } pipeTextStreamToResponse(response: ServerResponse, init?: ResponseInit) { pipeTextStreamToResponse({ response, textStream: this.textStream, ...init, }); } toUIMessageStreamResponse<UI_MESSAGE extends UIMessage>({ originalMessages, generateMessageId, onFinish, messageMetadata, sendReasoning, sendSources, sendFinish, sendStart, onError, ...init }: UIMessageStreamResponseInit & UIMessageStreamOptions<UI_MESSAGE> = {}): Response { return createUIMessageStreamResponse({ stream: this.toUIMessageStream({ originalMessages, generateMessageId, onFinish, messageMetadata, sendReasoning, sendSources, sendFinish, sendStart, onError, }), ...init, }); } toTextStreamResponse(init?: ResponseInit): Response { return createTextStreamResponse({ textStream: this.textStream, ...init, }); } } --- File: /ai/packages/ai/src/generate-text/to-response-messages.test.ts --- import { tool } from '@ai-sdk/provider-utils'; import z from 'zod/v4'; import { DefaultGeneratedFile } from './generated-file'; import { toResponseMessages } from './to-response-messages'; describe('toResponseMessages', () => { it('should return an assistant message with text when no tool calls or results', () => { const result = toResponseMessages({ content: [ { type: 'text', text: 'Hello, world!', }, ], tools: { testTool: tool({ description: 'A test tool', inputSchema: z.object({}), }), }, }); expect(result).toEqual([ { role: 'assistant', content: [{ type: 'text', text: 'Hello, world!' }], }, ]); }); it('should include tool calls in the assistant message', () => { const result = toResponseMessages({ content: [ { type: 'text', text: 'Using a tool', }, { type: 'tool-call', toolCallId: '123', toolName: 'testTool', input: {}, }, ], tools: { testTool: tool({ description: 'A test tool', inputSchema: z.object({}), }), }, }); expect(result).toEqual([ { role: 'assistant', content: [ { type: 'text', text: 'Using a tool' }, { type: 'tool-call', toolCallId: '123', toolName: 'testTool', input: {}, }, ], }, ]); }); it('should include tool calls with metadata in the assistant message', () => { const result = toResponseMessages({ content: [ { type: 'text', text: 'Using a tool', }, { type: 'tool-call', toolCallId: '123', toolName: 'testTool', input: {}, providerMetadata: { testProvider: { signature: 'sig', }, }, }, ], tools: { testTool: tool({ description: 'A test tool', inputSchema: z.object({}), }), }, }); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "providerOptions": undefined, "text": "Using a tool", "type": "text", }, { "input": {}, "providerExecuted": undefined, "providerOptions": { "testProvider": { "signature": "sig", }, }, "toolCallId": "123", "toolName": "testTool", "type": "tool-call", }, ], "role": "assistant", }, ] `); }); it('should include tool results as a separate message', () => { const result = toResponseMessages({ content: [ { type: 'text', text: 'Tool used', }, { type: 'tool-call', toolCallId: '123', toolName: 'testTool', input: {}, }, { type: 'tool-result', toolCallId: '123', toolName: 'testTool', output: 'Tool result', input: {}, }, ], tools: { testTool: tool({ description: 'A test tool', inputSchema: z.object({}), }), }, }); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "providerOptions": undefined, "text": "Tool used", "type": "text", }, { "input": {}, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "123", "toolName": "testTool", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "Tool result", }, "toolCallId": "123", "toolName": "testTool", "type": "tool-result", }, ], "role": "tool", }, ] `); }); it('should include tool errors as a separate message', () => { const result = toResponseMessages({ content: [ { type: 'text', text: 'Tool used', }, { type: 'tool-call', toolCallId: '123', toolName: 'testTool', input: {}, }, { type: 'tool-error', toolCallId: '123', toolName: 'testTool', error: 'Tool error', input: {}, }, ], tools: { testTool: tool({ description: 'A test tool', inputSchema: z.object({}), }), }, }); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "providerOptions": undefined, "text": "Tool used", "type": "text", }, { "input": {}, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "123", "toolName": "testTool", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "error-text", "value": "Tool error", }, "toolCallId": "123", "toolName": "testTool", "type": "tool-result", }, ], "role": "tool", }, ] `); }); it('should handle undefined text', () => { const result = toResponseMessages({ content: [ { type: 'reasoning', text: 'Thinking text', providerMetadata: { testProvider: { signature: 'sig', }, }, }, ], tools: {}, }); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "providerOptions": { "testProvider": { "signature": "sig", }, }, "text": "Thinking text", "type": "reasoning", }, ], "role": "assistant", }, ] `); }); it('should include reasoning array with redacted reasoning in the assistant message', () => { const result = toResponseMessages({ content: [ { type: 'reasoning', text: 'redacted-data', providerMetadata: { testProvider: { isRedacted: true }, }, }, { type: 'reasoning', text: 'Thinking text', providerMetadata: { testProvider: { signature: 'sig' }, }, }, { type: 'text', text: 'Final text', }, ], tools: {}, }); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "providerOptions": { "testProvider": { "isRedacted": true, }, }, "text": "redacted-data", "type": "reasoning", }, { "providerOptions": { "testProvider": { "signature": "sig", }, }, "text": "Thinking text", "type": "reasoning", }, { "providerOptions": undefined, "text": "Final text", "type": "text", }, ], "role": "assistant", }, ] `); }); it('should handle multipart tool results', () => { const result = toResponseMessages({ content: [ { type: 'text', text: 'multipart tool result', }, { type: 'tool-call', toolCallId: '123', toolName: 'testTool', input: {}, }, { type: 'tool-result', toolCallId: '123', toolName: 'testTool', output: 'image-base64', input: {}, }, ], tools: { testTool: tool({ description: 'A test tool', inputSchema: z.object({}), toModelOutput: () => ({ type: 'json', value: { proof: 'that toModelOutput is called', }, }), }), }, }); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "providerOptions": undefined, "text": "multipart tool result", "type": "text", }, { "input": {}, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "123", "toolName": "testTool", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "json", "value": { "proof": "that toModelOutput is called", }, }, "toolCallId": "123", "toolName": "testTool", "type": "tool-result", }, ], "role": "tool", }, ] `); }); it('should include images in the assistant message', () => { const pngFile = new DefaultGeneratedFile({ data: new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]), mediaType: 'image/png', }); const result = toResponseMessages({ content: [ { type: 'text', text: 'Here is an image', }, { type: 'file', file: pngFile }, ], tools: {}, }); expect(result).toStrictEqual([ { role: 'assistant', content: [ { type: 'text', text: 'Here is an image', providerOptions: undefined, }, { type: 'file', data: pngFile.base64, mediaType: pngFile.mediaType, providerOptions: undefined, }, ], }, ]); }); it('should handle multiple images in the assistant message', () => { const pngFile = new DefaultGeneratedFile({ data: new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]), mediaType: 'image/png', }); const jpegFile = new DefaultGeneratedFile({ data: new Uint8Array([255, 216, 255]), mediaType: 'image/jpeg', }); const result = toResponseMessages({ content: [ { type: 'text', text: 'Here are multiple images', }, { type: 'file', file: pngFile }, { type: 'file', file: jpegFile }, ], tools: {}, }); expect(result).toStrictEqual([ { role: 'assistant', content: [ { type: 'text', text: 'Here are multiple images', providerOptions: undefined, }, { type: 'file', data: pngFile.base64, mediaType: pngFile.mediaType, providerOptions: undefined, }, { type: 'file', data: jpegFile.base64, mediaType: jpegFile.mediaType, providerOptions: undefined, }, ], }, ]); }); it('should handle Uint8Array images', () => { const pngFile = new DefaultGeneratedFile({ data: new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]), mediaType: 'image/png', }); const result = toResponseMessages({ content: [ { type: 'text', text: 'Here is a binary image', }, { type: 'file', file: pngFile }, ], tools: {}, }); expect(result).toStrictEqual([ { role: 'assistant', content: [ { type: 'text', text: 'Here is a binary image', providerOptions: undefined, }, { type: 'file', data: pngFile.base64, mediaType: pngFile.mediaType, providerOptions: undefined, }, ], }, ]); }); it('should include images, reasoning, and tool calls in the correct order', () => { const pngFile = new DefaultGeneratedFile({ data: new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]), mediaType: 'image/png', }); const result = toResponseMessages({ content: [ { type: 'reasoning', text: 'Thinking text', providerMetadata: { testProvider: { signature: 'sig' } }, }, { type: 'file', file: pngFile }, { type: 'text', text: 'Combined response', }, { type: 'tool-call', toolCallId: '123', toolName: 'testTool', input: {}, }, ], tools: { testTool: tool({ description: 'A test tool', inputSchema: z.object({}), }), }, }); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "providerOptions": { "testProvider": { "signature": "sig", }, }, "text": "Thinking text", "type": "reasoning", }, { "data": "iVBORw0KGgo=", "mediaType": "image/png", "providerOptions": undefined, "type": "file", }, { "providerOptions": undefined, "text": "Combined response", "type": "text", }, { "input": {}, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "123", "toolName": "testTool", "type": "tool-call", }, ], "role": "assistant", }, ] `); }); it('should not append text parts if text is empty string', () => { const result = toResponseMessages({ content: [ { type: 'text', text: '', }, { type: 'tool-call', toolCallId: '123', toolName: 'testTool', input: {}, }, ], tools: { testTool: tool({ description: 'A test tool', inputSchema: z.object({}), }), }, }); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "input": {}, "providerExecuted": undefined, "providerOptions": undefined, "toolCallId": "123", "toolName": "testTool", "type": "tool-call", }, ], "role": "assistant", }, ] `); }); it('should not append assistant message if there is no content', () => { const result = toResponseMessages({ content: [], tools: {}, }); expect(result).toEqual([]); }); describe('provider-executed tool calls', () => { it('should include provider-executed tool calls and results', () => { const result = toResponseMessages({ content: [ { type: 'text', text: 'Let me search for recent news from San Francisco.', }, { type: 'tool-call', toolCallId: 'srvtoolu_011cNtbtzFARKPcAcp7w4nh9', toolName: 'web_search', input: { query: 'San Francisco major news events June 22 2025', }, providerExecuted: true, }, { type: 'tool-result', toolCallId: 'srvtoolu_011cNtbtzFARKPcAcp7w4nh9', toolName: 'web_search', input: { query: 'San Francisco major news events June 22 2025', }, output: [ { url: 'https://patch.com/california/san-francisco/calendar' }, ], providerExecuted: true, }, { type: 'text', text: 'Based on the search results, several significant events took place in San Francisco yesterday (June 22, 2025). Here are the main highlights:\n\n1. Juneteenth Celebration:\n', }, ], tools: { web_search: tool({ type: 'provider-defined', id: 'test.web_search', name: 'web_search', inputSchema: z.object({ query: z.string(), }), outputSchema: z.array( z.object({ url: z.string(), }), ), args: {}, }), }, }); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "providerOptions": undefined, "text": "Let me search for recent news from San Francisco.", "type": "text", }, { "input": { "query": "San Francisco major news events June 22 2025", }, "providerExecuted": true, "providerOptions": undefined, "toolCallId": "srvtoolu_011cNtbtzFARKPcAcp7w4nh9", "toolName": "web_search", "type": "tool-call", }, { "output": { "type": "json", "value": [ { "url": "https://patch.com/california/san-francisco/calendar", }, ], }, "providerExecuted": true, "providerOptions": undefined, "toolCallId": "srvtoolu_011cNtbtzFARKPcAcp7w4nh9", "toolName": "web_search", "type": "tool-result", }, { "providerOptions": undefined, "text": "Based on the search results, several significant events took place in San Francisco yesterday (June 22, 2025). Here are the main highlights: 1. Juneteenth Celebration: ", "type": "text", }, ], "role": "assistant", }, ] `); }); }); it('should include provider metadata in the text parts', () => { const result = toResponseMessages({ content: [ { type: 'text', text: 'Here is a text', providerMetadata: { testProvider: { signature: 'sig' } }, }, ], tools: {}, }); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "providerOptions": { "testProvider": { "signature": "sig", }, }, "text": "Here is a text", "type": "text", }, ], "role": "assistant", }, ] `); }); }); --- File: /ai/packages/ai/src/generate-text/to-response-messages.ts --- import { AssistantContent, AssistantModelMessage, ToolContent, ToolModelMessage, } from '../prompt'; import { createToolModelOutput } from '../prompt/create-tool-model-output'; import { ContentPart } from './content-part'; import { ToolSet } from './tool-set'; /** Converts the result of a `generateText` or `streamText` call to a list of response messages. */ export function toResponseMessages<TOOLS extends ToolSet>({ content: inputContent, tools, }: { content: Array<ContentPart<TOOLS>>; tools: TOOLS | undefined; }): Array<AssistantModelMessage | ToolModelMessage> { const responseMessages: Array<AssistantModelMessage | ToolModelMessage> = []; const content: AssistantContent = inputContent .filter(part => part.type !== 'source') .filter( part => (part.type !== 'tool-result' || part.providerExecuted) && (part.type !== 'tool-error' || part.providerExecuted), ) .filter(part => part.type !== 'text' || part.text.length > 0) .map(part => { switch (part.type) { case 'text': return { type: 'text', text: part.text, providerOptions: part.providerMetadata, }; case 'reasoning': return { type: 'reasoning', text: part.text, providerOptions: part.providerMetadata, }; case 'file': return { type: 'file', data: part.file.base64, mediaType: part.file.mediaType, providerOptions: part.providerMetadata, }; case 'tool-call': return { type: 'tool-call', toolCallId: part.toolCallId, toolName: part.toolName, input: part.input, providerExecuted: part.providerExecuted, providerOptions: part.providerMetadata, }; case 'tool-result': return { type: 'tool-result', toolCallId: part.toolCallId, toolName: part.toolName, output: createToolModelOutput({ tool: tools?.[part.toolName], output: part.output, errorMode: 'none', }), providerExecuted: true, providerOptions: part.providerMetadata, }; case 'tool-error': return { type: 'tool-result', toolCallId: part.toolCallId, toolName: part.toolName, output: createToolModelOutput({ tool: tools?.[part.toolName], output: part.error, errorMode: 'json', }), providerOptions: part.providerMetadata, }; } }); if (content.length > 0) { responseMessages.push({ role: 'assistant', content, }); } const toolResultContent: ToolContent = inputContent .filter(part => part.type === 'tool-result' || part.type === 'tool-error') .filter(part => !part.providerExecuted) .map(toolResult => ({ type: 'tool-result', toolCallId: toolResult.toolCallId, toolName: toolResult.toolName, output: createToolModelOutput({ tool: tools?.[toolResult.toolName], output: toolResult.type === 'tool-result' ? toolResult.output : toolResult.error, errorMode: toolResult.type === 'tool-error' ? 'text' : 'none', }), })); if (toolResultContent.length > 0) { responseMessages.push({ role: 'tool', content: toolResultContent, }); } return responseMessages; } --- File: /ai/packages/ai/src/generate-text/tool-call-repair-function.ts --- import { JSONSchema7, LanguageModelV2ToolCall } from '@ai-sdk/provider'; import { InvalidToolInputError } from '../error/invalid-tool-input-error'; import { NoSuchToolError } from '../error/no-such-tool-error'; import { ModelMessage } from '../prompt'; import { ToolSet } from './tool-set'; /** * A function that attempts to repair a tool call that failed to parse. * * It receives the error and the context as arguments and returns the repair * tool call JSON as text. * * @param options.system - The system prompt. * @param options.messages - The messages in the current generation step. * @param options.toolCall - The tool call that failed to parse. * @param options.tools - The tools that are available. * @param options.inputSchema - A function that returns the JSON Schema for a tool. * @param options.error - The error that occurred while parsing the tool call. */ export type ToolCallRepairFunction<TOOLS extends ToolSet> = (options: { system: string | undefined; messages: ModelMessage[]; toolCall: LanguageModelV2ToolCall; tools: TOOLS; inputSchema: (options: { toolName: string }) => JSONSchema7; error: NoSuchToolError | InvalidToolInputError; }) => Promise<LanguageModelV2ToolCall | null>; --- File: /ai/packages/ai/src/generate-text/tool-call.ts --- import { Tool } from '@ai-sdk/provider-utils'; import { ProviderMetadata } from '../types'; import { ValueOf } from '../util/value-of'; import { ToolSet } from './tool-set'; export type StaticToolCall<TOOLS extends ToolSet> = ValueOf<{ [NAME in keyof TOOLS]: { type: 'tool-call'; toolCallId: string; toolName: NAME & string; input: TOOLS[NAME] extends Tool<infer PARAMETERS> ? PARAMETERS : never; providerExecuted?: boolean; dynamic?: false | undefined; invalid?: false | undefined; error?: never; providerMetadata?: ProviderMetadata; }; }>; export type DynamicToolCall = { type: 'tool-call'; toolCallId: string; toolName: string; input: unknown; providerExecuted?: boolean; dynamic: true; providerMetadata?: ProviderMetadata; /** * True if this is caused by an unparsable tool call or * a tool that does not exist. */ // Added into DynamicToolCall to avoid breaking changes. // TODO AI SDK 6: separate into a new InvalidToolCall type invalid?: boolean; /** * The error that caused the tool call to be invalid. */ // TODO AI SDK 6: separate into a new InvalidToolCall type error?: unknown; }; export type TypedToolCall<TOOLS extends ToolSet> = | StaticToolCall<TOOLS> | DynamicToolCall; --- File: /ai/packages/ai/src/generate-text/tool-error.ts --- import { InferToolInput } from '@ai-sdk/provider-utils'; import { ValueOf } from '../util/value-of'; import { ToolSet } from './tool-set'; export type StaticToolError<TOOLS extends ToolSet> = ValueOf<{ [NAME in keyof TOOLS]: { type: 'tool-error'; toolCallId: string; toolName: NAME & string; input: InferToolInput<TOOLS[NAME]>; error: unknown; providerExecuted?: boolean; dynamic?: false | undefined; }; }>; export type DynamicToolError = { type: 'tool-error'; toolCallId: string; toolName: string; input: unknown; error: unknown; providerExecuted?: boolean; dynamic: true; }; export type TypedToolError<TOOLS extends ToolSet> = | StaticToolError<TOOLS> | DynamicToolError; --- File: /ai/packages/ai/src/generate-text/tool-output.ts --- import { TypedToolError } from './tool-error'; import { TypedToolResult } from './tool-result'; import { ToolSet } from './tool-set'; export type ToolOutput<TOOLS extends ToolSet> = | TypedToolResult<TOOLS> | TypedToolError<TOOLS>; --- File: /ai/packages/ai/src/generate-text/tool-result.ts --- import { InferToolInput, InferToolOutput } from '@ai-sdk/provider-utils'; import { ValueOf } from '../../src/util/value-of'; import { ToolSet } from './tool-set'; export type StaticToolResult<TOOLS extends ToolSet> = ValueOf<{ [NAME in keyof TOOLS]: { type: 'tool-result'; toolCallId: string; toolName: NAME & string; input: InferToolInput<TOOLS[NAME]>; output: InferToolOutput<TOOLS[NAME]>; providerExecuted?: boolean; dynamic?: false | undefined; }; }>; export type DynamicToolResult = { type: 'tool-result'; toolCallId: string; toolName: string; input: unknown; output: unknown; providerExecuted?: boolean; dynamic: true; }; export type TypedToolResult<TOOLS extends ToolSet> = | StaticToolResult<TOOLS> | DynamicToolResult; --- File: /ai/packages/ai/src/generate-text/tool-set.ts --- import { Tool } from '@ai-sdk/provider-utils'; export type ToolSet = Record< string, (Tool<never, never> | Tool<any, any> | Tool<any, never> | Tool<never, any>) & Pick< Tool<any, any>, 'execute' | 'onInputAvailable' | 'onInputStart' | 'onInputDelta' > >; --- File: /ai/packages/ai/src/middleware/default-settings-middleware.test.ts --- import { LanguageModelV2CallOptions } from '@ai-sdk/provider'; import { defaultSettingsMiddleware } from './default-settings-middleware'; import { MockLanguageModelV2 } from '../test/mock-language-model-v2'; const BASE_PARAMS: LanguageModelV2CallOptions = { prompt: [ { role: 'user', content: [{ type: 'text', text: 'Hello, world!' }] }, ], }; const MOCK_MODEL = new MockLanguageModelV2(); describe('defaultSettingsMiddleware', () => { describe('transformParams', () => { it('should apply default settings', async () => { const middleware = defaultSettingsMiddleware({ settings: { temperature: 0.7 }, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS }, model: MOCK_MODEL, }); expect(result.temperature).toBe(0.7); }); it('should give precedence to user-provided settings', async () => { const middleware = defaultSettingsMiddleware({ settings: { temperature: 0.7 }, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS, temperature: 0.5, }, model: MOCK_MODEL, }); expect(result.temperature).toBe(0.5); }); it('should merge provider metadata with default settings', async () => { const middleware = defaultSettingsMiddleware({ settings: { temperature: 0.7, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS }, model: MOCK_MODEL, }); expect(result.temperature).toBe(0.7); expect(result.providerOptions).toEqual({ anthropic: { cacheControl: { type: 'ephemeral' }, }, }); }); it('should merge complex provider metadata objects', async () => { const middleware = defaultSettingsMiddleware({ settings: { providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, feature: { enabled: true }, }, openai: { logit_bias: { '50256': -100 }, }, }, }, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS, providerOptions: { anthropic: { feature: { enabled: false }, otherSetting: 'value', }, }, }, model: MOCK_MODEL, }); expect(result.providerOptions).toEqual({ anthropic: { cacheControl: { type: 'ephemeral' }, feature: { enabled: false }, otherSetting: 'value', }, openai: { logit_bias: { '50256': -100 }, }, }); }); it('should handle nested provider metadata objects correctly', async () => { const middleware = defaultSettingsMiddleware({ settings: { providerOptions: { anthropic: { tools: { retrieval: { enabled: true }, math: { enabled: true }, }, }, }, }, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS, providerOptions: { anthropic: { tools: { retrieval: { enabled: false }, code: { enabled: true }, }, }, }, }, model: MOCK_MODEL, }); expect(result.providerOptions).toEqual({ anthropic: { tools: { retrieval: { enabled: false }, math: { enabled: true }, code: { enabled: true }, }, }, }); }); }); describe('temperature', () => { it('should keep 0 if settings.temperature is not set', async () => { const middleware = defaultSettingsMiddleware({ settings: {}, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS, temperature: 0 }, model: MOCK_MODEL, }); expect(result.temperature).toBe(0); }); it('should use default temperature if param temperature is undefined', async () => { const middleware = defaultSettingsMiddleware({ settings: { temperature: 0.7 }, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS, temperature: undefined }, model: MOCK_MODEL, }); expect(result.temperature).toBe(0.7); }); it('should not use default temperature if param temperature is null', async () => { const middleware = defaultSettingsMiddleware({ settings: { temperature: 0.7 }, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS, temperature: null as any }, model: MOCK_MODEL, }); expect(result.temperature).toBe(null); }); it('should use param temperature by default', async () => { const middleware = defaultSettingsMiddleware({ settings: { temperature: 0.7 }, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS, temperature: 0.9 }, model: MOCK_MODEL, }); expect(result.temperature).toBe(0.9); }); }); describe('other settings', () => { it('should apply default maxOutputTokens', async () => { const middleware = defaultSettingsMiddleware({ settings: { maxOutputTokens: 100 }, }); const result = await middleware.transformParams!({ type: 'generate', params: BASE_PARAMS, model: MOCK_MODEL, }); expect(result.maxOutputTokens).toBe(100); }); it('should prioritize param maxOutputTokens', async () => { const middleware = defaultSettingsMiddleware({ settings: { maxOutputTokens: 100 }, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS, maxOutputTokens: 50 }, model: MOCK_MODEL, }); expect(result.maxOutputTokens).toBe(50); }); it('should apply default stopSequences', async () => { const middleware = defaultSettingsMiddleware({ settings: { stopSequences: ['stop'] }, }); const result = await middleware.transformParams!({ type: 'generate', params: BASE_PARAMS, model: MOCK_MODEL, }); expect(result.stopSequences).toEqual(['stop']); }); it('should prioritize param stopSequences', async () => { const middleware = defaultSettingsMiddleware({ settings: { stopSequences: ['stop'] }, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS, stopSequences: ['end'] }, model: MOCK_MODEL, }); expect(result.stopSequences).toEqual(['end']); }); it('should apply default topP', async () => { const middleware = defaultSettingsMiddleware({ settings: { topP: 0.9 } }); const result = await middleware.transformParams!({ type: 'generate', params: BASE_PARAMS, model: MOCK_MODEL, }); expect(result.topP).toBe(0.9); }); it('should prioritize param topP', async () => { const middleware = defaultSettingsMiddleware({ settings: { topP: 0.9 } }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS, topP: 0.5 }, model: MOCK_MODEL, }); expect(result.topP).toBe(0.5); }); }); describe('headers', () => { it('should merge headers', async () => { const middleware = defaultSettingsMiddleware({ settings: { headers: { 'X-Custom-Header': 'test', 'X-Another-Header': 'test2' }, }, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS, headers: { 'X-Custom-Header': 'test2' }, }, model: MOCK_MODEL, }); expect(result.headers).toEqual({ 'X-Custom-Header': 'test2', 'X-Another-Header': 'test2', }); }); it('should handle empty default headers', async () => { const middleware = defaultSettingsMiddleware({ settings: { headers: {} }, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS, headers: { 'X-Param-Header': 'param' } }, model: MOCK_MODEL, }); expect(result.headers).toEqual({ 'X-Param-Header': 'param' }); }); it('should handle empty param headers', async () => { const middleware = defaultSettingsMiddleware({ settings: { headers: { 'X-Default-Header': 'default' } }, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS, headers: {} }, model: MOCK_MODEL, }); expect(result.headers).toEqual({ 'X-Default-Header': 'default' }); }); it('should handle both headers being undefined', async () => { const middleware = defaultSettingsMiddleware({ settings: {}, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS }, model: MOCK_MODEL, }); expect(result.headers).toBeUndefined(); }); }); describe('providerOptions', () => { it('should handle empty default providerOptions', async () => { const middleware = defaultSettingsMiddleware({ settings: { providerOptions: {} }, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS, providerOptions: { openai: { user: 'param-user' } }, }, model: MOCK_MODEL, }); expect(result.providerOptions).toEqual({ openai: { user: 'param-user' }, }); }); it('should handle empty param providerOptions', async () => { const middleware = defaultSettingsMiddleware({ settings: { providerOptions: { anthropic: { user: 'default-user' } } }, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS, providerOptions: {} }, model: MOCK_MODEL, }); expect(result.providerOptions).toEqual({ anthropic: { user: 'default-user' }, }); }); it('should handle both providerOptions being undefined', async () => { const middleware = defaultSettingsMiddleware({ settings: {}, }); const result = await middleware.transformParams!({ type: 'generate', params: { ...BASE_PARAMS }, model: MOCK_MODEL, }); expect(result.providerOptions).toBeUndefined(); }); }); }); --- File: /ai/packages/ai/src/middleware/default-settings-middleware.ts --- import { LanguageModelV2CallOptions, LanguageModelV2Middleware, } from '@ai-sdk/provider'; import { mergeObjects } from '../util/merge-objects'; /** * Applies default settings for a language model. */ export function defaultSettingsMiddleware({ settings, }: { settings: Partial<{ maxOutputTokens?: LanguageModelV2CallOptions['maxOutputTokens']; temperature?: LanguageModelV2CallOptions['temperature']; stopSequences?: LanguageModelV2CallOptions['stopSequences']; topP?: LanguageModelV2CallOptions['topP']; topK?: LanguageModelV2CallOptions['topK']; presencePenalty?: LanguageModelV2CallOptions['presencePenalty']; frequencyPenalty?: LanguageModelV2CallOptions['frequencyPenalty']; responseFormat?: LanguageModelV2CallOptions['responseFormat']; seed?: LanguageModelV2CallOptions['seed']; tools?: LanguageModelV2CallOptions['tools']; toolChoice?: LanguageModelV2CallOptions['toolChoice']; headers?: LanguageModelV2CallOptions['headers']; providerOptions?: LanguageModelV2CallOptions['providerOptions']; }>; }): LanguageModelV2Middleware { return { middlewareVersion: 'v2', transformParams: async ({ params }) => { return mergeObjects(settings, params) as LanguageModelV2CallOptions; }, }; } --- File: /ai/packages/ai/src/middleware/extract-reasoning-middleware.test.ts --- import { convertArrayToReadableStream, convertAsyncIterableToArray, } from '@ai-sdk/provider-utils/test'; import { generateText, streamText } from '../generate-text'; import { wrapLanguageModel } from '../middleware/wrap-language-model'; import { MockLanguageModelV2 } from '../test/mock-language-model-v2'; import { extractReasoningMiddleware } from './extract-reasoning-middleware'; const testUsage = { inputTokens: 5, outputTokens: 10, totalTokens: 18, reasoningTokens: 3, cachedInputTokens: undefined, }; describe('extractReasoningMiddleware', () => { describe('wrapGenerate', () => { it('should extract reasoning from <think> tags', async () => { const mockModel = new MockLanguageModelV2({ async doGenerate() { return { content: [ { type: 'text', text: '<think>analyzing the request</think>Here is the response', }, ], finishReason: 'stop', usage: testUsage, warnings: [], }; }, }); const result = await generateText({ model: wrapLanguageModel({ model: mockModel, middleware: extractReasoningMiddleware({ tagName: 'think' }), }), prompt: 'Hello, how can I help?', }); expect(result.reasoningText).toStrictEqual('analyzing the request'); expect(result.text).toStrictEqual('Here is the response'); }); it('should extract reasoning from <think> tags when there is no text', async () => { const mockModel = new MockLanguageModelV2({ async doGenerate() { return { content: [ { type: 'text', text: '<think>analyzing the request\n</think>', }, ], finishReason: 'stop', usage: testUsage, warnings: [], }; }, }); const result = await generateText({ model: wrapLanguageModel({ model: mockModel, middleware: extractReasoningMiddleware({ tagName: 'think' }), }), prompt: 'Hello, how can I help?', }); expect(result.reasoningText).toStrictEqual('analyzing the request\n'); expect(result.text).toStrictEqual(''); }); it('should extract reasoning from multiple <think> tags', async () => { const mockModel = new MockLanguageModelV2({ async doGenerate() { return { content: [ { type: 'text', text: '<think>analyzing the request</think>Here is the response<think>thinking about the response</think>more', }, ], finishReason: 'stop', usage: testUsage, warnings: [], }; }, }); const result = await generateText({ model: wrapLanguageModel({ model: mockModel, middleware: extractReasoningMiddleware({ tagName: 'think' }), }), prompt: 'Hello, how can I help?', }); expect(result.reasoningText).toStrictEqual( 'analyzing the request\nthinking about the response', ); expect(result.text).toStrictEqual('Here is the response\nmore'); }); it('should prepend <think> tag IFF startWithReasoning is true', async () => { const mockModel = new MockLanguageModelV2({ async doGenerate() { return { content: [ { type: 'text', text: 'analyzing the request</think>Here is the response', }, ], finishReason: 'stop', usage: testUsage, warnings: [], }; }, }); const resultTrue = await generateText({ model: wrapLanguageModel({ model: mockModel, middleware: extractReasoningMiddleware({ tagName: 'think', startWithReasoning: true, }), }), prompt: 'Hello, how can I help?', }); const resultFalse = await generateText({ model: wrapLanguageModel({ model: mockModel, middleware: extractReasoningMiddleware({ tagName: 'think', }), }), prompt: 'Hello, how can I help?', }); expect(resultTrue.reasoningText).toStrictEqual('analyzing the request'); expect(resultTrue.text).toStrictEqual('Here is the response'); expect(resultFalse.reasoningText).toBeUndefined(); expect(resultFalse.text).toStrictEqual( 'analyzing the request</think>Here is the response', ); }); it('should preserve reasoning property even when rest contains other properties', async () => { const mockModel = new MockLanguageModelV2({ async doGenerate() { return { content: [ { type: 'text', text: '<think>analyzing the request</think>Here is the response', }, ], finishReason: 'stop', usage: testUsage, reasoning: undefined, warnings: [], }; }, }); const result = await generateText({ model: wrapLanguageModel({ model: mockModel, middleware: extractReasoningMiddleware({ tagName: 'think' }), }), prompt: 'Hello, how can I help?', }); expect(result.reasoningText).toStrictEqual('analyzing the request'); expect(result.text).toStrictEqual('Here is the response'); }); }); describe('wrapStream', () => { it('should extract reasoning from split <think> tags', async () => { const mockModel = new MockLanguageModelV2({ async doStream() { return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '<think>' }, { type: 'text-delta', id: '1', delta: 'ana' }, { type: 'text-delta', id: '1', delta: 'lyzing the request' }, { type: 'text-delta', id: '1', delta: '</think>' }, { type: 'text-delta', id: '1', delta: 'Here' }, { type: 'text-delta', id: '1', delta: ' is the response' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }); const result = streamText({ model: wrapLanguageModel({ model: mockModel, middleware: extractReasoningMiddleware({ tagName: 'think' }), }), prompt: 'Hello, how can I help?', }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "reasoning-0", "type": "reasoning-start", }, { "id": "reasoning-0", "providerMetadata": undefined, "text": "ana", "type": "reasoning-delta", }, { "id": "reasoning-0", "providerMetadata": undefined, "text": "lyzing the request", "type": "reasoning-delta", }, { "id": "reasoning-0", "type": "reasoning-end", }, { "id": "1", "providerMetadata": undefined, "text": "Here", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": " is the response", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, "type": "finish", }, ] `); }); it('should extract reasoning from single chunk with multiple <think> tags', async () => { const mockModel = new MockLanguageModelV2({ async doStream() { return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '<think>analyzing the request</think>Here is the response<think>thinking about the response</think>more', }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }); const result = streamText({ model: wrapLanguageModel({ model: mockModel, middleware: extractReasoningMiddleware({ tagName: 'think' }), }), prompt: 'Hello, how can I help?', }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "reasoning-0", "type": "reasoning-start", }, { "id": "reasoning-0", "providerMetadata": undefined, "text": "analyzing the request", "type": "reasoning-delta", }, { "id": "reasoning-0", "type": "reasoning-end", }, { "id": "1", "providerMetadata": undefined, "text": "Here is the response", "type": "text-delta", }, { "id": "reasoning-1", "type": "reasoning-start", }, { "id": "reasoning-1", "providerMetadata": undefined, "text": " thinking about the response", "type": "reasoning-delta", }, { "id": "reasoning-1", "type": "reasoning-end", }, { "id": "1", "providerMetadata": undefined, "text": " more", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, "type": "finish", }, ] `); }); it('should extract reasoning from <think> when there is no text', async () => { const mockModel = new MockLanguageModelV2({ async doStream() { return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '<think>' }, { type: 'text-delta', id: '1', delta: 'ana' }, { type: 'text-delta', id: '1', delta: 'lyzing the request\n' }, { type: 'text-delta', id: '1', delta: '</think>' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }); const result = streamText({ model: wrapLanguageModel({ model: mockModel, middleware: extractReasoningMiddleware({ tagName: 'think' }), }), prompt: 'Hello, how can I help?', }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "reasoning-0", "type": "reasoning-start", }, { "id": "reasoning-0", "providerMetadata": undefined, "text": "ana", "type": "reasoning-delta", }, { "id": "reasoning-0", "providerMetadata": undefined, "text": "lyzing the request ", "type": "reasoning-delta", }, { "id": "reasoning-0", "type": "reasoning-end", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, "type": "finish", }, ] `); }); it('should prepend <think> tag IFF startWithReasoning is true', async () => { const mockModel = new MockLanguageModelV2({ async doStream() { return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'ana' }, { type: 'text-delta', id: '1', delta: 'lyzing the request\n' }, { type: 'text-delta', id: '1', delta: '</think>' }, { type: 'text-delta', id: '1', delta: 'this is the response' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }); const resultTrue = streamText({ model: wrapLanguageModel({ model: mockModel, middleware: extractReasoningMiddleware({ tagName: 'think', startWithReasoning: true, }), }), prompt: 'Hello, how can I help?', }); const resultFalse = streamText({ model: wrapLanguageModel({ model: mockModel, middleware: extractReasoningMiddleware({ tagName: 'think' }), }), prompt: 'Hello, how can I help?', }); expect(await convertAsyncIterableToArray(resultTrue.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "reasoning-0", "type": "reasoning-start", }, { "id": "reasoning-0", "providerMetadata": undefined, "text": "ana", "type": "reasoning-delta", }, { "id": "reasoning-0", "providerMetadata": undefined, "text": "lyzing the request ", "type": "reasoning-delta", }, { "id": "reasoning-0", "type": "reasoning-end", }, { "id": "1", "providerMetadata": undefined, "text": "this is the response", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, "type": "finish", }, ] `); expect(await convertAsyncIterableToArray(resultFalse.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "ana", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": "lyzing the request ", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": "</think>", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": "this is the response", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, "type": "finish", }, ] `); }); it('should keep original text when <think> tag is not present', async () => { const mockModel = new MockLanguageModelV2({ async doStream() { return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'this is the response' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }); const result = streamText({ model: wrapLanguageModel({ model: mockModel, middleware: extractReasoningMiddleware({ tagName: 'think' }), }), prompt: 'Hello, how can I help?', }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "this is the response", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, "type": "finish", }, ] `); }); }); }); --- File: /ai/packages/ai/src/middleware/extract-reasoning-middleware.ts --- import type { LanguageModelV2Content, LanguageModelV2Middleware, LanguageModelV2StreamPart, } from '@ai-sdk/provider'; import { getPotentialStartIndex } from '../util/get-potential-start-index'; /** * Extract an XML-tagged reasoning section from the generated text and exposes it * as a `reasoning` property on the result. * * @param tagName - The name of the XML tag to extract reasoning from. * @param separator - The separator to use between reasoning and text sections. * @param startWithReasoning - Whether to start with reasoning tokens. */ export function extractReasoningMiddleware({ tagName, separator = '\n', startWithReasoning = false, }: { tagName: string; separator?: string; startWithReasoning?: boolean; }): LanguageModelV2Middleware { const openingTag = `<${tagName}>`; const closingTag = `<\/${tagName}>`; return { middlewareVersion: 'v2', wrapGenerate: async ({ doGenerate }) => { const { content, ...rest } = await doGenerate(); const transformedContent: LanguageModelV2Content[] = []; for (const part of content) { if (part.type !== 'text') { transformedContent.push(part); continue; } const text = startWithReasoning ? openingTag + part.text : part.text; const regexp = new RegExp(`${openingTag}(.*?)${closingTag}`, 'gs'); const matches = Array.from(text.matchAll(regexp)); if (!matches.length) { transformedContent.push(part); continue; } const reasoningText = matches.map(match => match[1]).join(separator); let textWithoutReasoning = text; for (let i = matches.length - 1; i >= 0; i--) { const match = matches[i]; const beforeMatch = textWithoutReasoning.slice(0, match.index); const afterMatch = textWithoutReasoning.slice( match.index! + match[0].length, ); textWithoutReasoning = beforeMatch + (beforeMatch.length > 0 && afterMatch.length > 0 ? separator : '') + afterMatch; } transformedContent.push({ type: 'reasoning', text: reasoningText, }); transformedContent.push({ type: 'text', text: textWithoutReasoning, }); } return { content: transformedContent, ...rest }; }, wrapStream: async ({ doStream }) => { const { stream, ...rest } = await doStream(); const reasoningExtractions: Record< string, { isFirstReasoning: boolean; isFirstText: boolean; afterSwitch: boolean; isReasoning: boolean; buffer: string; idCounter: number; textId: string; } > = {}; return { stream: stream.pipeThrough( new TransformStream< LanguageModelV2StreamPart, LanguageModelV2StreamPart >({ transform: (chunk, controller) => { if (chunk.type !== 'text-delta') { controller.enqueue(chunk); return; } if (reasoningExtractions[chunk.id] == null) { reasoningExtractions[chunk.id] = { isFirstReasoning: true, isFirstText: true, afterSwitch: false, isReasoning: startWithReasoning, buffer: '', idCounter: 0, textId: chunk.id, }; } const activeExtraction = reasoningExtractions[chunk.id]; activeExtraction.buffer += chunk.delta; function publish(text: string) { if (text.length > 0) { const prefix = activeExtraction.afterSwitch && (activeExtraction.isReasoning ? !activeExtraction.isFirstReasoning : !activeExtraction.isFirstText) ? separator : ''; if ( activeExtraction.isReasoning && (activeExtraction.afterSwitch || activeExtraction.isFirstReasoning) ) { controller.enqueue({ type: 'reasoning-start', id: `reasoning-${activeExtraction.idCounter}`, }); } controller.enqueue( activeExtraction.isReasoning ? { type: 'reasoning-delta', delta: prefix + text, id: `reasoning-${activeExtraction.idCounter}`, } : { type: 'text-delta', delta: prefix + text, id: activeExtraction.textId, }, ); activeExtraction.afterSwitch = false; if (activeExtraction.isReasoning) { activeExtraction.isFirstReasoning = false; } else { activeExtraction.isFirstText = false; } } } do { const nextTag = activeExtraction.isReasoning ? closingTag : openingTag; const startIndex = getPotentialStartIndex( activeExtraction.buffer, nextTag, ); // no opening or closing tag found, publish the buffer if (startIndex == null) { publish(activeExtraction.buffer); activeExtraction.buffer = ''; break; } // publish text before the tag publish(activeExtraction.buffer.slice(0, startIndex)); const foundFullMatch = startIndex + nextTag.length <= activeExtraction.buffer.length; if (foundFullMatch) { activeExtraction.buffer = activeExtraction.buffer.slice( startIndex + nextTag.length, ); // reasoning part finished: if (activeExtraction.isReasoning) { controller.enqueue({ type: 'reasoning-end', id: `reasoning-${activeExtraction.idCounter++}`, }); } activeExtraction.isReasoning = !activeExtraction.isReasoning; activeExtraction.afterSwitch = true; } else { activeExtraction.buffer = activeExtraction.buffer.slice(startIndex); break; } } while (true); }, }), ), ...rest, }; }, }; } --- File: /ai/packages/ai/src/middleware/index.ts --- export { defaultSettingsMiddleware } from './default-settings-middleware'; export { extractReasoningMiddleware } from './extract-reasoning-middleware'; export { simulateStreamingMiddleware } from './simulate-streaming-middleware'; export { wrapLanguageModel } from './wrap-language-model'; export { wrapProvider } from './wrap-provider'; --- File: /ai/packages/ai/src/middleware/simulate-streaming-middleware.test.ts --- import { jsonSchema } from '@ai-sdk/provider-utils'; import { convertAsyncIterableToArray, mockId, } from '@ai-sdk/provider-utils/test'; import { streamText } from '../generate-text'; import { wrapLanguageModel } from '../middleware/wrap-language-model'; import { MockLanguageModelV2 } from '../test/mock-language-model-v2'; import { tool } from '@ai-sdk/provider-utils'; import { simulateStreamingMiddleware } from './simulate-streaming-middleware'; const DEFAULT_SETTINGs = { prompt: 'Test prompt', experimental_generateMessageId: mockId({ prefix: 'msg' }), _internal: { generateId: mockId({ prefix: 'id' }), currentDate: () => new Date('2025-01-01'), }, }; const testUsage = { inputTokens: 5, outputTokens: 10, totalTokens: 18, reasoningTokens: 3, cachedInputTokens: undefined, }; describe('simulateStreamingMiddleware', () => { it('should simulate streaming with text response', async () => { const mockModel = new MockLanguageModelV2({ async doGenerate() { return { content: [{ type: 'text', text: 'This is a test response' }], finishReason: 'stop', usage: testUsage, warnings: [], }; }, }); const result = streamText({ model: wrapLanguageModel({ model: mockModel, middleware: simulateStreamingMiddleware(), }), ...DEFAULT_SETTINGs, }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "0", "type": "text-start", }, { "id": "0", "providerMetadata": undefined, "text": "This is a test response", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 2025-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, "type": "finish", }, ] `); }); it('should simulate streaming with reasoning as string', async () => { const mockModel = new MockLanguageModelV2({ async doGenerate() { return { content: [ { type: 'reasoning', reasoningType: 'text', text: 'This is the reasoning process', }, { type: 'text', text: 'This is a test response' }, ], finishReason: 'stop', usage: testUsage, warnings: [], }; }, }); const result = streamText({ model: wrapLanguageModel({ model: mockModel, middleware: simulateStreamingMiddleware(), }), ...DEFAULT_SETTINGs, }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "0", "providerMetadata": undefined, "type": "reasoning-start", }, { "id": "0", "providerMetadata": undefined, "text": "This is the reasoning process", "type": "reasoning-delta", }, { "id": "0", "type": "reasoning-end", }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "This is a test response", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-1", "modelId": "mock-model-id", "timestamp": 2025-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, "type": "finish", }, ] `); }); it('should simulate streaming with reasoning as array of text objects', async () => { const mockModel = new MockLanguageModelV2({ async doGenerate() { return { content: [ { type: 'text', text: 'This is a test response' }, { type: 'reasoning', text: 'First reasoning step', }, { type: 'reasoning', text: 'Second reasoning step', }, { type: 'reasoning', text: '', providerMetadata: { testProvider: { signature: 'abc', }, }, }, ], finishReason: 'stop', usage: testUsage, warnings: [], }; }, }); const result = streamText({ model: wrapLanguageModel({ model: mockModel, middleware: simulateStreamingMiddleware(), }), ...DEFAULT_SETTINGs, }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "0", "type": "text-start", }, { "id": "0", "providerMetadata": undefined, "text": "This is a test response", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "id": "1", "providerMetadata": undefined, "type": "reasoning-start", }, { "id": "1", "providerMetadata": undefined, "text": "First reasoning step", "type": "reasoning-delta", }, { "id": "1", "type": "reasoning-end", }, { "id": "2", "providerMetadata": undefined, "type": "reasoning-start", }, { "id": "2", "providerMetadata": undefined, "text": "Second reasoning step", "type": "reasoning-delta", }, { "id": "2", "type": "reasoning-end", }, { "id": "3", "providerMetadata": { "testProvider": { "signature": "abc", }, }, "type": "reasoning-start", }, { "id": "3", "providerMetadata": undefined, "text": "", "type": "reasoning-delta", }, { "id": "3", "type": "reasoning-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-2", "modelId": "mock-model-id", "timestamp": 2025-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, "type": "finish", }, ] `); }); it('should simulate streaming with reasoning as array of mixed objects', async () => { const mockModel = new MockLanguageModelV2({ async doGenerate() { return { content: [ { type: 'reasoning', text: 'First reasoning step', }, { type: 'reasoning', text: 'data', providerMetadata: { testProvider: { isRedacted: true }, }, }, { type: 'text', text: 'This is a test response', }, ], finishReason: 'stop', usage: testUsage, warnings: [], }; }, }); const result = streamText({ model: wrapLanguageModel({ model: mockModel, middleware: simulateStreamingMiddleware(), }), ...DEFAULT_SETTINGs, }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "0", "providerMetadata": undefined, "type": "reasoning-start", }, { "id": "0", "providerMetadata": undefined, "text": "First reasoning step", "type": "reasoning-delta", }, { "id": "0", "type": "reasoning-end", }, { "id": "1", "providerMetadata": { "testProvider": { "isRedacted": true, }, }, "type": "reasoning-start", }, { "id": "1", "providerMetadata": undefined, "text": "data", "type": "reasoning-delta", }, { "id": "1", "type": "reasoning-end", }, { "id": "2", "type": "text-start", }, { "id": "2", "providerMetadata": undefined, "text": "This is a test response", "type": "text-delta", }, { "id": "2", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-3", "modelId": "mock-model-id", "timestamp": 2025-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, "type": "finish", }, ] `); }); it('should simulate streaming with tool calls', async () => { const mockModel = new MockLanguageModelV2({ async doGenerate() { return { content: [ { type: 'text', text: 'This is a test response', }, { type: 'tool-call', toolCallId: 'tool-1', toolName: 'calculator', input: '{"expression": "2+2"}', toolCallType: 'function', }, { type: 'tool-call', toolCallId: 'tool-2', toolName: 'weather', input: '{"location": "New York"}', toolCallType: 'function', }, ], finishReason: 'tool-calls', usage: testUsage, warnings: [], }; }, }); const result = streamText({ model: wrapLanguageModel({ model: mockModel, middleware: simulateStreamingMiddleware(), }), tools: { calculator: tool({ inputSchema: jsonSchema<{ expression: string }>({ type: 'object', }), }), weather: tool({ inputSchema: jsonSchema<{ location: string }>({ type: 'object', }), }), }, ...DEFAULT_SETTINGs, }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "0", "type": "text-start", }, { "id": "0", "providerMetadata": undefined, "text": "This is a test response", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "input": { "expression": "2+2", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "tool-1", "toolName": "calculator", "type": "tool-call", }, { "input": { "location": "New York", }, "providerExecuted": undefined, "providerMetadata": undefined, "toolCallId": "tool-2", "toolName": "weather", "type": "tool-call", }, { "finishReason": "tool-calls", "providerMetadata": undefined, "response": { "headers": undefined, "id": "id-4", "modelId": "mock-model-id", "timestamp": 2025-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, }, { "finishReason": "tool-calls", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, "type": "finish", }, ] `); }); it('should preserve additional metadata in the response', async () => { const mockModel = new MockLanguageModelV2({ async doGenerate() { return { content: [{ type: 'text', text: 'This is a test response' }], finishReason: 'stop', usage: testUsage, providerMetadata: { custom: { key: 'value' } }, warnings: [], }; }, }); const result = streamText({ model: wrapLanguageModel({ model: mockModel, middleware: simulateStreamingMiddleware(), }), ...DEFAULT_SETTINGs, }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "0", "type": "text-start", }, { "id": "0", "providerMetadata": undefined, "text": "This is a test response", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "custom": { "key": "value", }, }, "response": { "headers": undefined, "id": "id-5", "modelId": "mock-model-id", "timestamp": 2025-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, }, { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokens": 5, "outputTokens": 10, "reasoningTokens": 3, "totalTokens": 18, }, "type": "finish", }, ] `); }); it('should handle empty text response', async () => { const mockModel = new MockLanguageModelV2({ async doGenerate() { return { content: [{ type: 'text', text: '' }], finishReason: 'stop', usage: testUsage, warnings: [], }; }, }); const result = streamText({ model: wrapLanguageModel({ model: mockModel, middleware: simulateStreamingMiddleware(), }), ...DEFAULT_SETTINGs, }); expect( await convertAsyncIterableToArray(result.fullStream), ).toMatchSnapshot(); }); it('should pass through warnings from the model', async () => { const mockModel = new MockLanguageModelV2({ async doGenerate() { return { content: [{ type: 'text', text: 'This is a test response' }], finishReason: 'stop', usage: testUsage, warnings: [ { type: 'other', message: 'Test warning', code: 'test_warning' }, ], }; }, }); const result = streamText({ model: wrapLanguageModel({ model: mockModel, middleware: simulateStreamingMiddleware(), }), ...DEFAULT_SETTINGs, }); result.consumeStream(); expect(await result.warnings).toMatchInlineSnapshot(` [ { "code": "test_warning", "message": "Test warning", "type": "other", }, ] `); }); }); --- File: /ai/packages/ai/src/middleware/simulate-streaming-middleware.ts --- import type { LanguageModelV2Middleware, LanguageModelV2StreamPart, } from '@ai-sdk/provider'; /** * Simulates streaming chunks with the response from a generate call. */ export function simulateStreamingMiddleware(): LanguageModelV2Middleware { return { middlewareVersion: 'v2', wrapStream: async ({ doGenerate }) => { const result = await doGenerate(); let id = 0; const simulatedStream = new ReadableStream<LanguageModelV2StreamPart>({ start(controller) { controller.enqueue({ type: 'stream-start', warnings: result.warnings, }); controller.enqueue({ type: 'response-metadata', ...result.response }); for (const part of result.content) { switch (part.type) { case 'text': { if (part.text.length > 0) { controller.enqueue({ type: 'text-start', id: String(id) }); controller.enqueue({ type: 'text-delta', id: String(id), delta: part.text, }); controller.enqueue({ type: 'text-end', id: String(id) }); id++; } break; } case 'reasoning': { controller.enqueue({ type: 'reasoning-start', id: String(id), providerMetadata: part.providerMetadata, }); controller.enqueue({ type: 'reasoning-delta', id: String(id), delta: part.text, }); controller.enqueue({ type: 'reasoning-end', id: String(id) }); id++; break; } default: { controller.enqueue(part); break; } } } controller.enqueue({ type: 'finish', finishReason: result.finishReason, usage: result.usage, providerMetadata: result.providerMetadata, }); controller.close(); }, }); return { stream: simulatedStream, request: result.request, response: result.response, }; }, }; } --- File: /ai/packages/ai/src/middleware/wrap-language-model.test.ts --- import { LanguageModelV2, LanguageModelV2CallOptions } from '@ai-sdk/provider'; import { wrapLanguageModel } from '../middleware/wrap-language-model'; import { MockLanguageModelV2 } from '../test/mock-language-model-v2'; describe('wrapLanguageModel', () => { describe('model property', () => { it('should pass through by default', () => { const wrappedModel = wrapLanguageModel({ model: new MockLanguageModelV2({ modelId: 'test-model', }), middleware: { middlewareVersion: 'v2', }, }); expect(wrappedModel.modelId).toBe('test-model'); }); it('should use middleware overrideModelId if provided', () => { const wrappedModel = wrapLanguageModel({ model: new MockLanguageModelV2({ modelId: 'test-model', }), middleware: { middlewareVersion: 'v2', overrideModelId: ({ model }) => 'override-model', }, }); expect(wrappedModel.modelId).toBe('override-model'); }); it('should use modelId parameter if provided', () => { const wrappedModel = wrapLanguageModel({ model: new MockLanguageModelV2({ modelId: 'test-model', }), middleware: { middlewareVersion: 'v2', }, modelId: 'override-model', }); expect(wrappedModel.modelId).toBe('override-model'); }); }); describe('provider property', () => { it('should pass through by default', () => { const wrappedModel = wrapLanguageModel({ model: new MockLanguageModelV2({ provider: 'test-provider', }), middleware: { middlewareVersion: 'v2', }, }); expect(wrappedModel.provider).toBe('test-provider'); }); it('should use middleware overrideProvider if provided', () => { const wrappedModel = wrapLanguageModel({ model: new MockLanguageModelV2({ provider: 'test-provider', }), middleware: { middlewareVersion: 'v2', overrideProvider: ({ model }) => 'override-provider', }, }); expect(wrappedModel.provider).toBe('override-provider'); }); it('should use providerId parameter if provided', () => { const wrappedModel = wrapLanguageModel({ model: new MockLanguageModelV2({ provider: 'test-provider', }), middleware: { middlewareVersion: 'v2', }, providerId: 'override-provider', }); expect(wrappedModel.provider).toBe('override-provider'); }); }); describe('supportedUrls property', () => { it('should pass through by default', async () => { const supportedUrls = { 'original/*': [/^https:\/\/.*$/], }; const wrappedModel = wrapLanguageModel({ model: new MockLanguageModelV2({ supportedUrls }), middleware: { middlewareVersion: 'v2', }, }); expect(await wrappedModel.supportedUrls).toStrictEqual(supportedUrls); }); it('should use middleware overrideSupportedUrls if provided', () => { const wrappedModel = wrapLanguageModel({ model: new MockLanguageModelV2({ supportedUrls: { 'original/*': [/^https:\/\/.*$/], }, }), middleware: { middlewareVersion: 'v2', overrideSupportedUrls: ({ model }) => ({ 'override/*': [/^https:\/\/.*$/], }), }, }); expect(wrappedModel.supportedUrls).toStrictEqual({ 'override/*': [/^https:\/\/.*$/], }); }); }); it('should call transformParams middleware for doGenerate', async () => { const mockModel = new MockLanguageModelV2({ doGenerate: [], }); const transformParams = vi.fn().mockImplementation(({ params }) => ({ ...params, transformed: true, })); const wrappedModel = wrapLanguageModel({ model: mockModel, middleware: { middlewareVersion: 'v2', transformParams, }, }); const params: LanguageModelV2CallOptions = { prompt: [{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }], }; await wrappedModel.doGenerate(params); expect(transformParams).toHaveBeenCalledWith({ params, type: 'generate', model: expect.any(Object), }); expect(mockModel.doGenerateCalls[0]).toStrictEqual({ ...params, transformed: true, }); }); it('should call wrapGenerate middleware', async () => { const mockModel = new MockLanguageModelV2({ doGenerate: vi.fn().mockResolvedValue('mock result'), }); const wrapGenerate = vi .fn() .mockImplementation(({ doGenerate }) => doGenerate()); const wrappedModel = wrapLanguageModel({ model: mockModel, middleware: { middlewareVersion: 'v2', wrapGenerate, }, }); const params: LanguageModelV2CallOptions = { prompt: [{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }], }; await wrappedModel.doGenerate(params); expect(wrapGenerate).toHaveBeenCalledWith({ doGenerate: expect.any(Function), doStream: expect.any(Function), params, model: mockModel, }); }); it('should call transformParams middleware for doStream', async () => { const mockModel = new MockLanguageModelV2({ doStream: [], }); const transformParams = vi.fn().mockImplementation(({ params }) => ({ ...params, transformed: true, })); const wrappedModel = wrapLanguageModel({ model: mockModel, middleware: { middlewareVersion: 'v2', transformParams, }, }); const params: LanguageModelV2CallOptions = { prompt: [{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }], }; await wrappedModel.doStream(params); expect(transformParams).toHaveBeenCalledWith({ params, type: 'stream', model: expect.any(Object), }); expect(mockModel.doStreamCalls[0]).toStrictEqual({ ...params, transformed: true, }); }); it('should call wrapStream middleware', async () => { const mockModel = new MockLanguageModelV2({ doStream: vi.fn().mockResolvedValue('mock stream'), }); const wrapStream = vi.fn().mockImplementation(({ doStream }) => doStream()); const wrappedModel = wrapLanguageModel({ model: mockModel, middleware: { middlewareVersion: 'v2', wrapStream, }, }); const params: LanguageModelV2CallOptions = { prompt: [{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }], }; await wrappedModel.doStream(params); expect(wrapStream).toHaveBeenCalledWith({ doGenerate: expect.any(Function), doStream: expect.any(Function), params, model: mockModel, }); }); it('should support models that use "this" context in supportedUrls', async () => { let supportedUrlsCalled = false; class MockLanguageModelWithImageSupport implements LanguageModelV2 { readonly specificationVersion = 'v2'; readonly provider = 'test-provider'; readonly modelId = 'test-model'; readonly doGenerate: LanguageModelV2['doGenerate'] = vi.fn(); readonly doStream: LanguageModelV2['doStream'] = vi.fn(); readonly value = { 'image/*': [/^https:\/\/.*$/], }; get supportedUrls() { supportedUrlsCalled = true; // Reference 'this' to verify context return this.value; } } const model = new MockLanguageModelWithImageSupport(); const wrappedModel = wrapLanguageModel({ model, middleware: { middlewareVersion: 'v2' }, }); expect(await wrappedModel.supportedUrls).toStrictEqual(model.value); expect(supportedUrlsCalled).toBe(true); }); describe('multiple middlewares', () => { it('should call multiple transformParams middlewares in sequence for doGenerate', async () => { const mockModel = new MockLanguageModelV2({ doGenerate: [], }); const transformParams1 = vi.fn().mockImplementation(({ params }) => ({ ...params, transformationStep1: true, })); const transformParams2 = vi.fn().mockImplementation(({ params }) => ({ ...params, transformationStep2: true, })); const wrappedModel = wrapLanguageModel({ model: mockModel, middleware: [ { middlewareVersion: 'v2', transformParams: transformParams1, }, { middlewareVersion: 'v2', transformParams: transformParams2, }, ], }); const params: LanguageModelV2CallOptions = { prompt: [{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }], }; await wrappedModel.doGenerate(params); expect(transformParams1).toHaveBeenCalledWith({ params, type: 'generate', model: expect.any(Object), }); expect(transformParams2).toHaveBeenCalledWith({ params: { ...params, transformationStep1: true }, type: 'generate', model: expect.any(Object), }); expect(mockModel.doGenerateCalls[0]).toStrictEqual( expect.objectContaining({ transformationStep1: true, transformationStep2: true, }), ); }); it('should call multiple transformParams middlewares in sequence for doStream', async () => { const mockModel = new MockLanguageModelV2({ doStream: [], }); const transformParams1 = vi.fn().mockImplementation(({ params }) => ({ ...params, transformationStep1: true, })); const transformParams2 = vi.fn().mockImplementation(({ params }) => ({ ...params, transformationStep2: true, })); const wrappedModel = wrapLanguageModel({ model: mockModel, middleware: [ { middlewareVersion: 'v2', transformParams: transformParams1, }, { middlewareVersion: 'v2', transformParams: transformParams2, }, ], }); const params: LanguageModelV2CallOptions = { prompt: [{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }], }; await wrappedModel.doStream(params); expect(transformParams1).toHaveBeenCalledWith({ params, type: 'stream', model: expect.any(Object), }); expect(transformParams2).toHaveBeenCalledWith({ params: expect.objectContaining({ transformationStep1: true }), type: 'stream', model: mockModel, }); expect(mockModel.doStreamCalls[0]).toStrictEqual( expect.objectContaining({ transformationStep1: true, transformationStep2: true, }), ); }); it('should chain multiple wrapGenerate middlewares in the correct order', async () => { const mockModel = new MockLanguageModelV2({ doGenerate: vi.fn().mockResolvedValue('final generate result'), }); const wrapGenerate1 = vi .fn() .mockImplementation(async ({ doGenerate, params, model }) => { const result = await doGenerate(); return `wrapGenerate1(${result})`; }); const wrapGenerate2 = vi .fn() .mockImplementation(async ({ doGenerate, params, model }) => { const result = await doGenerate(); return `wrapGenerate2(${result})`; }); const wrappedModel = wrapLanguageModel({ model: mockModel, middleware: [ { middlewareVersion: 'v2', wrapGenerate: wrapGenerate1, }, { middlewareVersion: 'v2', wrapGenerate: wrapGenerate2, }, ], }); const params: LanguageModelV2CallOptions = { prompt: [{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }], }; const result = await wrappedModel.doGenerate(params); // The middlewares should wrap in order, applying wrapGenerate2 last expect(result).toBe( 'wrapGenerate1(wrapGenerate2(final generate result))', ); expect(wrapGenerate1).toHaveBeenCalled(); expect(wrapGenerate2).toHaveBeenCalled(); }); it('should chain multiple wrapStream middlewares in the correct order', async () => { const mockModel = new MockLanguageModelV2({ doStream: vi.fn().mockResolvedValue('final stream result'), }); const wrapStream1 = vi .fn() .mockImplementation(async ({ doStream, params, model }) => { const result = await doStream(); return `wrapStream1(${result})`; }); const wrapStream2 = vi .fn() .mockImplementation(async ({ doStream, params, model }) => { const result = await doStream(); return `wrapStream2(${result})`; }); const wrappedModel = wrapLanguageModel({ model: mockModel, middleware: [ { middlewareVersion: 'v2', wrapStream: wrapStream1, }, { middlewareVersion: 'v2', wrapStream: wrapStream2, }, ], }); const params: LanguageModelV2CallOptions = { prompt: [{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }], }; const result = await wrappedModel.doStream(params); // The middlewares should wrap in order, applying wrapStream2 last expect(result).toBe('wrapStream1(wrapStream2(final stream result))'); expect(wrapStream1).toHaveBeenCalled(); expect(wrapStream2).toHaveBeenCalled(); }); }); }); --- File: /ai/packages/ai/src/middleware/wrap-language-model.ts --- import { LanguageModelV2, LanguageModelV2CallOptions, LanguageModelV2Middleware, } from '@ai-sdk/provider'; import { asArray } from '../util/as-array'; /** * Wraps a LanguageModelV2 instance with middleware functionality. * This function allows you to apply middleware to transform parameters, * wrap generate operations, and wrap stream operations of a language model. * * @param options - Configuration options for wrapping the language model. * @param options.model - The original LanguageModelV2 instance to be wrapped. * @param options.middleware - The middleware to be applied to the language model. When multiple middlewares are provided, the first middleware will transform the input first, and the last middleware will be wrapped directly around the model. * @param options.modelId - Optional custom model ID to override the original model's ID. * @param options.providerId - Optional custom provider ID to override the original model's provider ID. * @returns A new LanguageModelV2 instance with middleware applied. */ export const wrapLanguageModel = ({ model, middleware: middlewareArg, modelId, providerId, }: { model: LanguageModelV2; middleware: LanguageModelV2Middleware | LanguageModelV2Middleware[]; modelId?: string; providerId?: string; }): LanguageModelV2 => { return asArray(middlewareArg) .reverse() .reduce((wrappedModel, middleware) => { return doWrap({ model: wrappedModel, middleware, modelId, providerId }); }, model); }; const doWrap = ({ model, middleware: { transformParams, wrapGenerate, wrapStream, overrideProvider, overrideModelId, overrideSupportedUrls, }, modelId, providerId, }: { model: LanguageModelV2; middleware: LanguageModelV2Middleware; modelId?: string; providerId?: string; }): LanguageModelV2 => { async function doTransform({ params, type, }: { params: LanguageModelV2CallOptions; type: 'generate' | 'stream'; }) { return transformParams ? await transformParams({ params, type, model }) : params; } return { specificationVersion: 'v2', provider: providerId ?? overrideProvider?.({ model }) ?? model.provider, modelId: modelId ?? overrideModelId?.({ model }) ?? model.modelId, supportedUrls: overrideSupportedUrls?.({ model }) ?? model.supportedUrls, async doGenerate( params: LanguageModelV2CallOptions, ): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>> { const transformedParams = await doTransform({ params, type: 'generate' }); const doGenerate = async () => model.doGenerate(transformedParams); const doStream = async () => model.doStream(transformedParams); return wrapGenerate ? wrapGenerate({ doGenerate, doStream, params: transformedParams, model, }) : doGenerate(); }, async doStream( params: LanguageModelV2CallOptions, ): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>> { const transformedParams = await doTransform({ params, type: 'stream' }); const doGenerate = async () => model.doGenerate(transformedParams); const doStream = async () => model.doStream(transformedParams); return wrapStream ? wrapStream({ doGenerate, doStream, params: transformedParams, model }) : doStream(); }, }; }; --- File: /ai/packages/ai/src/middleware/wrap-provider.test.ts --- import { MockLanguageModelV2 } from '../test/mock-language-model-v2'; import { MockProviderV2 } from '../test/mock-provider-v2'; import { wrapProvider } from './wrap-provider'; describe('wrapProvider', () => { it('should wrap all language models in the provider', () => { const model1 = new MockLanguageModelV2({ modelId: 'model-1' }); const model2 = new MockLanguageModelV2({ modelId: 'model-2' }); const model3 = new MockLanguageModelV2({ modelId: 'model-3' }); const provider = new MockProviderV2({ languageModels: { 'model-1': model1, 'model-2': model2, 'model-3': model3, }, }); const overrideModelId = vi .fn() .mockImplementation(({ model }) => `override-${model.modelId}`); const wrappedProvider = wrapProvider({ provider, languageModelMiddleware: { middlewareVersion: 'v2', overrideModelId, }, }); expect(wrappedProvider.languageModel('model-1').modelId).toBe( 'override-model-1', ); expect(wrappedProvider.languageModel('model-2').modelId).toBe( 'override-model-2', ); expect(wrappedProvider.languageModel('model-3').modelId).toBe( 'override-model-3', ); expect(overrideModelId).toHaveBeenCalledTimes(3); expect(overrideModelId).toHaveBeenCalledWith({ model: model1 }); expect(overrideModelId).toHaveBeenCalledWith({ model: model2 }); expect(overrideModelId).toHaveBeenCalledWith({ model: model3 }); }); }); --- File: /ai/packages/ai/src/middleware/wrap-provider.ts --- import type { LanguageModelV2Middleware, ProviderV2 } from '@ai-sdk/provider'; import { wrapLanguageModel } from './wrap-language-model'; /** * Wraps a ProviderV2 instance with middleware functionality. * This function allows you to apply middleware to all language models * from the provider, enabling you to transform parameters, wrap generate * operations, and wrap stream operations for every language model. * * @param options - Configuration options for wrapping the provider. * @param options.provider - The original ProviderV2 instance to be wrapped. * @param options.languageModelMiddleware - The middleware to be applied to all language models from the provider. When multiple middlewares are provided, the first middleware will transform the input first, and the last middleware will be wrapped directly around the model. * @returns A new ProviderV2 instance with middleware applied to all language models. */ export function wrapProvider({ provider, languageModelMiddleware, }: { provider: ProviderV2; languageModelMiddleware: | LanguageModelV2Middleware | LanguageModelV2Middleware[]; }): ProviderV2 { const wrappedProvider = { languageModel(modelId: string) { let model = provider.languageModel(modelId); model = wrapLanguageModel({ model, middleware: languageModelMiddleware, }); return model; }, textEmbeddingModel: provider.textEmbeddingModel, imageModel: provider.imageModel, transcriptionModel: provider.transcriptionModel, speechModel: provider.speechModel, }; return wrappedProvider; } --- File: /ai/packages/ai/src/model/resolve-model.test.ts --- import { customProvider } from '../registry/custom-provider'; import { MockEmbeddingModelV2 } from '../test/mock-embedding-model-v2'; import { MockLanguageModelV2 } from '../test/mock-language-model-v2'; import { resolveEmbeddingModel, resolveLanguageModel } from './resolve-model'; describe('resolveLanguageModel', () => { describe('when a language model v2 is provided', () => { it('should return the language model v2', () => { const resolvedModel = resolveLanguageModel( new MockLanguageModelV2({ provider: 'test-provider', modelId: 'test-model-id', }), ); expect(resolvedModel.provider).toBe('test-provider'); expect(resolvedModel.modelId).toBe('test-model-id'); }); }); describe('when a string is provided and the global default provider is not set', () => { it('should return a gateway language model', () => { const resolvedModel = resolveLanguageModel('test-model-id'); expect(resolvedModel.provider).toBe('gateway'); expect(resolvedModel.modelId).toBe('test-model-id'); }); }); describe('when a string is provided and the global default provider is set', () => { beforeEach(() => { globalThis.AI_SDK_DEFAULT_PROVIDER = customProvider({ languageModels: { 'test-model-id': new MockLanguageModelV2({ provider: 'global-test-provider', modelId: 'actual-test-model-id', }), }, }); }); afterEach(() => { delete globalThis.AI_SDK_DEFAULT_PROVIDER; }); it('should return a language model from the global default provider', () => { const resolvedModel = resolveLanguageModel('test-model-id'); expect(resolvedModel.provider).toBe('global-test-provider'); expect(resolvedModel.modelId).toBe('actual-test-model-id'); }); }); }); describe('resolveEmbeddingModel', () => { describe('when a embedding model v2 is provided', () => { it('should return the embedding model v2', () => { const resolvedModel = resolveEmbeddingModel( new MockEmbeddingModelV2({ provider: 'test-provider', modelId: 'test-model-id', }), ); expect(resolvedModel.provider).toBe('test-provider'); expect(resolvedModel.modelId).toBe('test-model-id'); }); }); describe('when a string is provided and the global default provider is not set', () => { it('should return a gateway embedding model', () => { const resolvedModel = resolveEmbeddingModel('test-model-id'); expect(resolvedModel.provider).toBe('gateway'); expect(resolvedModel.modelId).toBe('test-model-id'); }); }); describe('when a string is provided and the global default provider is set', () => { beforeEach(() => { globalThis.AI_SDK_DEFAULT_PROVIDER = customProvider({ textEmbeddingModels: { 'test-model-id': new MockEmbeddingModelV2({ provider: 'global-test-provider', modelId: 'actual-test-model-id', }), }, }); }); afterEach(() => { delete globalThis.AI_SDK_DEFAULT_PROVIDER; }); it('should return a embedding model from the global default provider', () => { const resolvedModel = resolveEmbeddingModel('test-model-id'); expect(resolvedModel.provider).toBe('global-test-provider'); expect(resolvedModel.modelId).toBe('actual-test-model-id'); }); }); }); --- File: /ai/packages/ai/src/model/resolve-model.ts --- import { gateway } from '@ai-sdk/gateway'; import { EmbeddingModelV2, LanguageModelV2, ProviderV2, } from '@ai-sdk/provider'; import { UnsupportedModelVersionError } from '../error'; import { EmbeddingModel } from '../types/embedding-model'; import { LanguageModel } from '../types/language-model'; export function resolveLanguageModel(model: LanguageModel): LanguageModelV2 { if (typeof model !== 'string') { if (model.specificationVersion !== 'v2') { throw new UnsupportedModelVersionError({ version: model.specificationVersion, provider: model.provider, modelId: model.modelId, }); } return model; } return getGlobalProvider().languageModel(model); } export function resolveEmbeddingModel<VALUE = string>( model: EmbeddingModel<VALUE>, ): EmbeddingModelV2<VALUE> { if (typeof model !== 'string') { if (model.specificationVersion !== 'v2') { throw new UnsupportedModelVersionError({ version: model.specificationVersion, provider: model.provider, modelId: model.modelId, }); } return model; } // TODO AI SDK 6: figure out how to cleanly support different generic types return getGlobalProvider().textEmbeddingModel( model, ) as EmbeddingModelV2<VALUE>; } function getGlobalProvider(): ProviderV2 { return globalThis.AI_SDK_DEFAULT_PROVIDER ?? gateway; } --- File: /ai/packages/ai/src/prompt/call-settings.ts --- export type CallSettings = { /** Maximum number of tokens to generate. */ maxOutputTokens?: number; /** Temperature setting. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both. */ temperature?: number; /** Nucleus sampling. This is a number between 0 and 1. E.g. 0.1 would mean that only tokens with the top 10% probability mass are considered. It is recommended to set either `temperature` or `topP`, but not both. */ topP?: number; /** Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature. */ topK?: number; /** Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The presence penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty. */ presencePenalty?: number; /** Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The frequency penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty. */ frequencyPenalty?: number; /** Stop sequences. If set, the model will stop generating text when one of the stop sequences is generated. Providers may have limits on the number of stop sequences. */ stopSequences?: string[]; /** The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results. */ seed?: number; /** Maximum number of retries. Set to 0 to disable retries. @default 2 */ maxRetries?: number; /** Abort signal. */ abortSignal?: AbortSignal; /** Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. */ headers?: Record<string, string | undefined>; }; --- File: /ai/packages/ai/src/prompt/content-part.ts --- import { LanguageModelV2ToolResultOutput } from '@ai-sdk/provider'; import { FilePart, ImagePart, ProviderOptions, ReasoningPart, TextPart, ToolResultPart, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { jsonValueSchema } from '../types/json-value'; import { providerMetadataSchema } from '../types/provider-metadata'; import { dataContentSchema } from './data-content'; /** @internal */ export const textPartSchema: z.ZodType<TextPart> = z.object({ type: z.literal('text'), text: z.string(), providerOptions: providerMetadataSchema.optional(), }); /** @internal */ export const imagePartSchema: z.ZodType<ImagePart> = z.object({ type: z.literal('image'), image: z.union([dataContentSchema, z.instanceof(URL)]), mediaType: z.string().optional(), providerOptions: providerMetadataSchema.optional(), }); /** @internal */ export const filePartSchema: z.ZodType<FilePart> = z.object({ type: z.literal('file'), data: z.union([dataContentSchema, z.instanceof(URL)]), filename: z.string().optional(), mediaType: z.string(), providerOptions: providerMetadataSchema.optional(), }); /** @internal */ export const reasoningPartSchema: z.ZodType<ReasoningPart> = z.object({ type: z.literal('reasoning'), text: z.string(), providerOptions: providerMetadataSchema.optional(), }); /** Tool call content part of a prompt. It contains a tool call (usually generated by the AI model). */ export interface ToolCallPart { type: 'tool-call'; /** ID of the tool call. This ID is used to match the tool call with the tool result. */ toolCallId: string; /** Name of the tool that is being called. */ toolName: string; /** Arguments of the tool call. This is a JSON-serializable object that matches the tool's input schema. */ input: unknown; /** Additional provider-specific metadata. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; } /** @internal */ export const toolCallPartSchema: z.ZodType<ToolCallPart> = z.object({ type: z.literal('tool-call'), toolCallId: z.string(), toolName: z.string(), input: z.unknown(), providerOptions: providerMetadataSchema.optional(), providerExecuted: z.boolean().optional(), }) as z.ZodType<ToolCallPart>; // necessary bc input is optional on Zod type /** @internal */ export const outputSchema: z.ZodType<LanguageModelV2ToolResultOutput> = z.discriminatedUnion('type', [ z.object({ type: z.literal('text'), value: z.string(), }), z.object({ type: z.literal('json'), value: jsonValueSchema, }), z.object({ type: z.literal('error-text'), value: z.string(), }), z.object({ type: z.literal('error-json'), value: jsonValueSchema, }), z.object({ type: z.literal('content'), value: z.array( z.union([ z.object({ type: z.literal('text'), text: z.string(), }), z.object({ type: z.literal('media'), data: z.string(), mediaType: z.string(), }), ]), ), }), ]); /** @internal */ export const toolResultPartSchema: z.ZodType<ToolResultPart> = z.object({ type: z.literal('tool-result'), toolCallId: z.string(), toolName: z.string(), output: outputSchema, providerOptions: providerMetadataSchema.optional(), }) as z.ZodType<ToolResultPart>; // necessary bc result is optional on Zod type --- File: /ai/packages/ai/src/prompt/convert-to-language-model-prompt.test.ts --- import { convertToLanguageModelMessage, convertToLanguageModelPrompt, } from './convert-to-language-model-prompt'; describe('convertToLanguageModelPrompt', () => { describe('user message', () => { describe('image parts', () => { it('should download images for user image parts with URLs when model does not support image URLs', async () => { const result = await convertToLanguageModelPrompt({ prompt: { messages: [ { role: 'user', content: [ { type: 'image', image: new URL('https://example.com/image.png'), }, ], }, ], }, supportedUrls: {}, downloadImplementation: async ({ url }) => { expect(url).toEqual(new URL('https://example.com/image.png')); return { data: new Uint8Array([0, 1, 2, 3]), mediaType: 'image/png', }; }, }); expect(result).toEqual([ { role: 'user', content: [ { type: 'file', mediaType: 'image/png', data: new Uint8Array([0, 1, 2, 3]), }, ], }, ]); }); it('should download images for user image parts with string URLs when model does not support image URLs', async () => { const result = await convertToLanguageModelPrompt({ prompt: { messages: [ { role: 'user', content: [ { type: 'image', image: 'https://example.com/image.png', }, ], }, ], }, supportedUrls: {}, downloadImplementation: async ({ url }) => { expect(url).toEqual(new URL('https://example.com/image.png')); return { data: new Uint8Array([0, 1, 2, 3]), mediaType: 'image/png', }; }, }); expect(result).toEqual([ { role: 'user', content: [ { type: 'file', mediaType: 'image/png', data: new Uint8Array([0, 1, 2, 3]), }, ], }, ]); }); }); describe('file parts', () => { it('should pass through URLs when the model supports a particular URL', async () => { const result = await convertToLanguageModelPrompt({ prompt: { messages: [ { role: 'user', content: [ { type: 'file', data: new URL('https://example.com/document.pdf'), mediaType: 'application/pdf', }, ], }, ], }, supportedUrls: { '*': [/^https:\/\/.*$/], }, }); expect(result).toEqual([ { role: 'user', content: [ { type: 'file', data: new URL('https://example.com/document.pdf'), mediaType: 'application/pdf', }, ], }, ]); }); it('should download the URL as an asset when the model does not support a URL', async () => { const result = await convertToLanguageModelPrompt({ prompt: { messages: [ { role: 'user', content: [ { type: 'file', data: new URL('https://example.com/document.pdf'), mediaType: 'application/pdf', }, ], }, ], }, supportedUrls: { // PDF is not supported, but image/* is 'image/*': [/^https:\/\/.*$/], }, downloadImplementation: async ({ url }) => { expect(url).toEqual(new URL('https://example.com/document.pdf')); return { data: new Uint8Array([0, 1, 2, 3]), mediaType: 'application/pdf', }; }, }); expect(result).toEqual([ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data: new Uint8Array([0, 1, 2, 3]), }, ], }, ]); }); it('should handle file parts with base64 string data', async () => { const base64Data = 'SGVsbG8sIFdvcmxkIQ=='; // "Hello, World!" in base64 const result = await convertToLanguageModelPrompt({ prompt: { messages: [ { role: 'user', content: [ { type: 'file', data: base64Data, mediaType: 'text/plain', }, ], }, ], }, supportedUrls: { 'image/*': [/^https:\/\/.*$/], }, }); expect(result).toEqual([ { role: 'user', content: [ { type: 'file', data: base64Data, mediaType: 'text/plain', }, ], }, ]); }); it('should handle file parts with Uint8Array data', async () => { const uint8Data = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" in ASCII const result = await convertToLanguageModelPrompt({ prompt: { messages: [ { role: 'user', content: [ { type: 'file', data: uint8Data, mediaType: 'text/plain', }, ], }, ], }, supportedUrls: { 'image/*': [/^https:\/\/.*$/], }, }); expect(result).toEqual([ { role: 'user', content: [ { type: 'file', data: new Uint8Array([72, 101, 108, 108, 111]), mediaType: 'text/plain', }, ], }, ]); }); it('should download files for user file parts with URL objects when model does not support downloads', async () => { const result = await convertToLanguageModelPrompt({ prompt: { messages: [ { role: 'user', content: [ { type: 'file', data: new URL('https://example.com/document.pdf'), mediaType: 'application/pdf', }, ], }, ], }, supportedUrls: {}, downloadImplementation: async ({ url }) => { expect(url).toEqual(new URL('https://example.com/document.pdf')); return { data: new Uint8Array([0, 1, 2, 3]), mediaType: 'application/pdf', }; }, }); expect(result).toEqual([ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data: new Uint8Array([0, 1, 2, 3]), }, ], }, ]); }); it('should download files for user file parts with string URLs when model does not support downloads', async () => { const result = await convertToLanguageModelPrompt({ prompt: { messages: [ { role: 'user', content: [ { type: 'file', data: 'https://example.com/document.pdf', mediaType: 'application/pdf', }, ], }, ], }, supportedUrls: {}, downloadImplementation: async ({ url }) => { expect(url).toEqual(new URL('https://example.com/document.pdf')); return { data: new Uint8Array([0, 1, 2, 3]), mediaType: 'application/pdf', }; }, }); expect(result).toEqual([ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data: new Uint8Array([0, 1, 2, 3]), }, ], }, ]); }); it('should download files for user file parts with string URLs when model does not support the particular URL', async () => { const result = await convertToLanguageModelPrompt({ prompt: { messages: [ { role: 'user', content: [ { type: 'file', data: 'https://example.com/document.pdf', mediaType: 'application/pdf', }, ], }, ], }, supportedUrls: { 'application/pdf': [ // everything except https://example.com/document.pdf /^(?!https:\/\/example\.com\/document\.pdf$).*$/, ], }, downloadImplementation: async ({ url }) => { expect(url).toEqual(new URL('https://example.com/document.pdf')); return { data: new Uint8Array([0, 1, 2, 3]), mediaType: 'application/pdf', }; }, }); expect(result).toEqual([ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data: new Uint8Array([0, 1, 2, 3]), }, ], }, ]); }); it('does not download URLs for user file parts for URL objects when model does support the URL', async () => { const result = await convertToLanguageModelPrompt({ prompt: { messages: [ { role: 'user', content: [ { type: 'file', data: new URL('https://example.com/document.pdf'), mediaType: 'application/pdf', }, ], }, ], }, supportedUrls: { 'application/pdf': [ // match exactly https://example.com/document.pdf /^https:\/\/example\.com\/document\.pdf$/, ], }, }); expect(result).toEqual([ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data: new URL('https://example.com/document.pdf'), }, ], }, ]); }); it('it should default to downloading the URL when the model does not provider a supportsUrl function', async () => { const result = await convertToLanguageModelPrompt({ prompt: { messages: [ { role: 'user', content: [ { type: 'file', data: 'https://example.com/document.pdf', mediaType: 'application/pdf', }, ], }, ], }, supportedUrls: {}, downloadImplementation: async ({ url }) => { expect(url).toEqual(new URL('https://example.com/document.pdf')); return { data: new Uint8Array([0, 1, 2, 3]), mediaType: 'application/pdf', }; }, }); expect(result).toEqual([ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data: new Uint8Array([0, 1, 2, 3]), }, ], }, ]); }); it('should handle file parts with filename', async () => { const result = await convertToLanguageModelPrompt({ prompt: { messages: [ { role: 'user', content: [ { type: 'file', data: 'SGVsbG8sIFdvcmxkIQ==', // "Hello, World!" in base64 mediaType: 'text/plain', filename: 'hello.txt', }, ], }, ], }, supportedUrls: { 'image/*': [/^https:\/\/.*$/], }, }); expect(result).toEqual([ { role: 'user', content: [ { type: 'file', data: 'SGVsbG8sIFdvcmxkIQ==', mediaType: 'text/plain', filename: 'hello.txt', }, ], }, ]); }); it('should preserve filename when downloading file from URL', async () => { const result = await convertToLanguageModelPrompt({ prompt: { messages: [ { role: 'user', content: [ { type: 'file', data: new URL('https://example.com/document.pdf'), mediaType: 'application/pdf', filename: 'important-document.pdf', }, ], }, ], }, supportedUrls: {}, downloadImplementation: async ({ url }) => { expect(url).toEqual(new URL('https://example.com/document.pdf')); return { data: new Uint8Array([0, 1, 2, 3]), mediaType: 'application/pdf', }; }, }); expect(result).toEqual([ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data: new Uint8Array([0, 1, 2, 3]), filename: 'important-document.pdf', }, ], }, ]); }); it('should prioritize user-provided mediaType over downloaded file mediaType', async () => { const result = await convertToLanguageModelPrompt({ prompt: { messages: [ { role: 'user', content: [ { type: 'file', data: new URL('https://example.com/image.jpg'), mediaType: 'image/jpeg', }, ], }, ], }, supportedUrls: {}, downloadImplementation: async ({ url }) => { expect(url).toEqual(new URL('https://example.com/image.jpg')); return { data: new Uint8Array([0, 1, 2, 3]), mediaType: 'application/octet-stream', }; }, }); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "data": Uint8Array [ 0, 1, 2, 3, ], "filename": undefined, "mediaType": "image/jpeg", "providerOptions": undefined, "type": "file", }, ], "providerOptions": undefined, "role": "user", }, ] `); }); it('should use downloaded file mediaType as fallback when user provides generic mediaType', async () => { const result = await convertToLanguageModelPrompt({ prompt: { messages: [ { role: 'user', content: [ { type: 'file', data: new URL('https://example.com/document.txt'), mediaType: 'application/octet-stream', }, ], }, ], }, supportedUrls: {}, downloadImplementation: async ({ url }) => { expect(url).toEqual(new URL('https://example.com/document.txt')); return { data: new Uint8Array([72, 101, 108, 108, 111]), mediaType: 'text/plain', }; }, }); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "data": Uint8Array [ 72, 101, 108, 108, 111, ], "filename": undefined, "mediaType": "application/octet-stream", "providerOptions": undefined, "type": "file", }, ], "providerOptions": undefined, "role": "user", }, ] `); }); }); describe('provider options', async () => { it('should add provider options to messages', async () => { const result = await convertToLanguageModelPrompt({ prompt: { messages: [ { role: 'user', content: [ { type: 'text', text: 'hello, world!', }, ], providerOptions: { 'test-provider': { 'key-a': 'test-value-1', 'key-b': 'test-value-2', }, }, }, ], }, supportedUrls: {}, }); expect(result).toEqual([ { role: 'user', content: [ { type: 'text', text: 'hello, world!', providerMetadata: undefined, }, ], providerOptions: { 'test-provider': { 'key-a': 'test-value-1', 'key-b': 'test-value-2', }, }, }, ]); }); }); }); }); describe('convertToLanguageModelMessage', () => { describe('user message', () => { describe('text parts', () => { it('should filter out empty text parts', async () => { const result = convertToLanguageModelMessage({ message: { role: 'user', content: [{ type: 'text', text: '' }] }, downloadedAssets: {}, }); expect(result).toEqual({ role: 'user', content: [], }); }); it('should pass through non-empty text parts', async () => { const result = convertToLanguageModelMessage({ message: { role: 'user', content: [{ type: 'text', text: 'hello, world!' }], }, downloadedAssets: {}, }); expect(result).toEqual({ role: 'user', content: [{ type: 'text', text: 'hello, world!' }], }); }); }); describe('image parts', () => { it('should convert image string https url to URL object', async () => { const result = convertToLanguageModelMessage({ message: { role: 'user', content: [ { type: 'image', image: 'https://example.com/image.jpg', }, ], }, downloadedAssets: {}, }); expect(result).toEqual({ role: 'user', content: [ { type: 'file', data: new URL('https://example.com/image.jpg'), mediaType: 'image/*', // wildcard since we don't know the exact type }, ], }); }); it('should convert image string data url to base64 content', async () => { const result = convertToLanguageModelMessage({ message: { role: 'user', content: [ { type: 'image', image: 'data:image/jpg;base64,/9j/3Q==', }, ], }, downloadedAssets: {}, }); expect(result).toEqual({ role: 'user', content: [ { type: 'file', data: '/9j/3Q==', mediaType: 'image/jpeg', }, ], }); }); it('should prefer detected mediaType', async () => { const result = convertToLanguageModelMessage({ message: { role: 'user', content: [ { type: 'image', // incorrect mediaType: image: 'data:image/png;base64,/9j/3Q==', }, ], }, downloadedAssets: {}, }); expect(result).toEqual({ role: 'user', content: [ { type: 'file', data: '/9j/3Q==', mediaType: 'image/jpeg', }, ], }); }); }); describe('file parts', () => { it('should convert file string https url to URL object', async () => { const result = convertToLanguageModelMessage({ message: { role: 'user', content: [ { type: 'file', data: 'https://example.com/image.jpg', mediaType: 'image/jpg', }, ], }, downloadedAssets: {}, }); expect(result).toEqual({ role: 'user', content: [ { type: 'file', data: new URL('https://example.com/image.jpg'), mediaType: 'image/jpg', }, ], }); }); it('should convert file string data url to base64 content', async () => { const result = convertToLanguageModelMessage({ message: { role: 'user', content: [ { type: 'file', data: 'data:image/jpg;base64,dGVzdA==', mediaType: 'image/jpg', }, ], }, downloadedAssets: {}, }); expect(result).toEqual({ role: 'user', content: [ { type: 'file', data: 'dGVzdA==', mediaType: 'image/jpg', }, ], }); }); }); }); describe('assistant message', () => { describe('text parts', () => { it('should ignore empty text parts', async () => { const result = convertToLanguageModelMessage({ message: { role: 'assistant', content: [ { type: 'text', text: '', }, { type: 'tool-call', toolName: 'toolName', toolCallId: 'toolCallId', input: {}, }, ], }, downloadedAssets: {}, }); expect(result).toEqual({ role: 'assistant', content: [ { type: 'tool-call', input: {}, toolCallId: 'toolCallId', toolName: 'toolName', }, ], }); }); }); describe('reasoning parts', () => { it('should pass through provider options', () => { const result = convertToLanguageModelMessage({ message: { role: 'assistant', content: [ { type: 'reasoning', text: 'hello, world!', providerOptions: { 'test-provider': { 'key-a': 'test-value-1', 'key-b': 'test-value-2', }, }, }, ], }, downloadedAssets: {}, }); expect(result).toEqual({ role: 'assistant', content: [ { type: 'reasoning', text: 'hello, world!', providerOptions: { 'test-provider': { 'key-a': 'test-value-1', 'key-b': 'test-value-2', }, }, }, ], }); }); it('should support a mix of reasoning, redacted reasoning, and text parts', () => { const result = convertToLanguageModelMessage({ message: { role: 'assistant', content: [ { type: 'reasoning', text: `I'm thinking`, }, { type: 'reasoning', text: 'redacted-reasoning-data', providerOptions: { 'test-provider': { redacted: true }, }, }, { type: 'reasoning', text: 'more thinking', }, { type: 'text', text: 'hello, world!', }, ], }, downloadedAssets: {}, }); expect(result).toEqual({ role: 'assistant', content: [ { type: 'reasoning', text: `I'm thinking`, }, { type: 'reasoning', text: 'redacted-reasoning-data', providerOptions: { 'test-provider': { redacted: true }, }, }, { type: 'reasoning', text: 'more thinking', }, { type: 'text', text: 'hello, world!', }, ], }); }); }); describe('tool call parts', () => { it('should pass through provider options', () => { const result = convertToLanguageModelMessage({ message: { role: 'assistant', content: [ { type: 'tool-call', toolName: 'toolName', toolCallId: 'toolCallId', input: {}, providerOptions: { 'test-provider': { 'key-a': 'test-value-1', 'key-b': 'test-value-2', }, }, }, ], }, downloadedAssets: {}, }); expect(result).toEqual({ role: 'assistant', content: [ { type: 'tool-call', input: {}, toolCallId: 'toolCallId', toolName: 'toolName', providerOptions: { 'test-provider': { 'key-a': 'test-value-1', 'key-b': 'test-value-2', }, }, }, ], }); }); it('should include providerExecuted flag', () => { const result = convertToLanguageModelMessage({ message: { role: 'assistant', content: [ { type: 'tool-call', toolName: 'toolName', toolCallId: 'toolCallId', input: {}, providerExecuted: true, }, ], }, downloadedAssets: {}, }); expect(result).toMatchInlineSnapshot(` { "content": [ { "input": {}, "providerExecuted": true, "providerOptions": undefined, "toolCallId": "toolCallId", "toolName": "toolName", "type": "tool-call", }, ], "providerOptions": undefined, "role": "assistant", } `); }); }); describe('tool result parts', () => { it('should include providerExecuted flag', () => { const result = convertToLanguageModelMessage({ message: { role: 'assistant', content: [ { type: 'tool-result', toolCallId: 'toolCallId', toolName: 'toolName', output: { type: 'json', value: { some: 'result' } }, providerOptions: { 'test-provider': { 'key-a': 'test-value-1', 'key-b': 'test-value-2', }, }, }, ], }, downloadedAssets: {}, }); expect(result).toMatchInlineSnapshot(` { "content": [ { "output": { "type": "json", "value": { "some": "result", }, }, "providerOptions": { "test-provider": { "key-a": "test-value-1", "key-b": "test-value-2", }, }, "toolCallId": "toolCallId", "toolName": "toolName", "type": "tool-result", }, ], "providerOptions": undefined, "role": "assistant", } `); }); }); describe('file parts', () => { it('should convert file data correctly', async () => { const result = convertToLanguageModelMessage({ message: { role: 'assistant', content: [ { type: 'file', data: 'dGVzdA==', // "test" in base64 mediaType: 'application/pdf', }, ], }, downloadedAssets: {}, }); expect(result).toEqual({ role: 'assistant', content: [ { type: 'file', data: 'dGVzdA==', mediaType: 'application/pdf', }, ], }); }); it('should preserve filename when present', async () => { const result = convertToLanguageModelMessage({ message: { role: 'assistant', content: [ { type: 'file', data: 'dGVzdA==', mediaType: 'application/pdf', filename: 'test-document.pdf', }, ], }, downloadedAssets: {}, }); expect(result).toEqual({ role: 'assistant', content: [ { type: 'file', data: 'dGVzdA==', mediaType: 'application/pdf', filename: 'test-document.pdf', }, ], }); }); it('should handle provider options', async () => { const result = convertToLanguageModelMessage({ message: { role: 'assistant', content: [ { type: 'file', data: 'dGVzdA==', mediaType: 'application/pdf', providerOptions: { 'test-provider': { 'key-a': 'test-value-1', 'key-b': 'test-value-2', }, }, }, ], }, downloadedAssets: {}, }); expect(result).toEqual({ role: 'assistant', content: [ { type: 'file', data: 'dGVzdA==', mediaType: 'application/pdf', providerOptions: { 'test-provider': { 'key-a': 'test-value-1', 'key-b': 'test-value-2', }, }, }, ], }); }); }); }); describe('tool message', () => { it('should convert basic tool result message', () => { const result = convertToLanguageModelMessage({ message: { role: 'tool', content: [ { type: 'tool-result', toolName: 'toolName', toolCallId: 'toolCallId', output: { type: 'json', value: { some: 'result' } }, }, ], }, downloadedAssets: {}, }); expect(result).toMatchInlineSnapshot(` { "content": [ { "output": { "type": "json", "value": { "some": "result", }, }, "providerOptions": undefined, "toolCallId": "toolCallId", "toolName": "toolName", "type": "tool-result", }, ], "providerOptions": undefined, "role": "tool", } `); }); it('should convert tool result with provider metadata', () => { const result = convertToLanguageModelMessage({ message: { role: 'tool', content: [ { type: 'tool-result', toolName: 'toolName', toolCallId: 'toolCallId', output: { type: 'json', value: { some: 'result' } }, providerOptions: { 'test-provider': { 'key-a': 'test-value-1', 'key-b': 'test-value-2', }, }, }, ], }, downloadedAssets: {}, }); expect(result).toMatchInlineSnapshot(` { "content": [ { "output": { "type": "json", "value": { "some": "result", }, }, "providerOptions": { "test-provider": { "key-a": "test-value-1", "key-b": "test-value-2", }, }, "toolCallId": "toolCallId", "toolName": "toolName", "type": "tool-result", }, ], "providerOptions": undefined, "role": "tool", } `); }); it('should include error flag', () => { const result = convertToLanguageModelMessage({ message: { role: 'tool', content: [ { type: 'tool-result', toolName: 'toolName', toolCallId: 'toolCallId', output: { type: 'json', value: { some: 'result' } }, }, ], }, downloadedAssets: {}, }); expect(result).toMatchInlineSnapshot(` { "content": [ { "output": { "type": "json", "value": { "some": "result", }, }, "providerOptions": undefined, "toolCallId": "toolCallId", "toolName": "toolName", "type": "tool-result", }, ], "providerOptions": undefined, "role": "tool", } `); }); it('should include multipart content', () => { const result = convertToLanguageModelMessage({ message: { role: 'tool', content: [ { type: 'tool-result', toolName: 'toolName', toolCallId: 'toolCallId', output: { type: 'content', value: [ { type: 'media', data: 'dGVzdA==', mediaType: 'image/png' }, ], }, }, ], }, downloadedAssets: {}, }); expect(result).toMatchInlineSnapshot(` { "content": [ { "output": { "type": "content", "value": [ { "data": "dGVzdA==", "mediaType": "image/png", "type": "media", }, ], }, "providerOptions": undefined, "toolCallId": "toolCallId", "toolName": "toolName", "type": "tool-result", }, ], "providerOptions": undefined, "role": "tool", } `); }); }); }); --- File: /ai/packages/ai/src/prompt/convert-to-language-model-prompt.ts --- import { LanguageModelV2FilePart, LanguageModelV2Message, LanguageModelV2Prompt, LanguageModelV2TextPart, } from '@ai-sdk/provider'; import { DataContent, FilePart, ImagePart, isUrlSupported, ModelMessage, TextPart, } from '@ai-sdk/provider-utils'; import { detectMediaType, imageMediaTypeSignatures, } from '../util/detect-media-type'; import { download } from '../util/download'; import { convertToLanguageModelV2DataContent } from './data-content'; import { InvalidMessageRoleError } from './invalid-message-role-error'; import { StandardizedPrompt } from './standardize-prompt'; export async function convertToLanguageModelPrompt({ prompt, supportedUrls, downloadImplementation = download, }: { prompt: StandardizedPrompt; supportedUrls: Record<string, RegExp[]>; downloadImplementation?: typeof download; }): Promise<LanguageModelV2Prompt> { const downloadedAssets = await downloadAssets( prompt.messages, downloadImplementation, supportedUrls, ); return [ ...(prompt.system != null ? [{ role: 'system' as const, content: prompt.system }] : []), ...prompt.messages.map(message => convertToLanguageModelMessage({ message, downloadedAssets }), ), ]; } /** * Convert a ModelMessage to a LanguageModelV2Message. * * @param message The ModelMessage to convert. * @param downloadedAssets A map of URLs to their downloaded data. Only * available if the model does not support URLs, null otherwise. */ export function convertToLanguageModelMessage({ message, downloadedAssets, }: { message: ModelMessage; downloadedAssets: Record< string, { mediaType: string | undefined; data: Uint8Array } >; }): LanguageModelV2Message { const role = message.role; switch (role) { case 'system': { return { role: 'system', content: message.content, providerOptions: message.providerOptions, }; } case 'user': { if (typeof message.content === 'string') { return { role: 'user', content: [{ type: 'text', text: message.content }], providerOptions: message.providerOptions, }; } return { role: 'user', content: message.content .map(part => convertPartToLanguageModelPart(part, downloadedAssets)) // remove empty text parts: .filter(part => part.type !== 'text' || part.text !== ''), providerOptions: message.providerOptions, }; } case 'assistant': { if (typeof message.content === 'string') { return { role: 'assistant', content: [{ type: 'text', text: message.content }], providerOptions: message.providerOptions, }; } return { role: 'assistant', content: message.content .filter( // remove empty text parts: part => part.type !== 'text' || part.text !== '', ) .map(part => { const providerOptions = part.providerOptions; switch (part.type) { case 'file': { const { data, mediaType } = convertToLanguageModelV2DataContent( part.data, ); return { type: 'file', data, filename: part.filename, mediaType: mediaType ?? part.mediaType, providerOptions, }; } case 'reasoning': { return { type: 'reasoning', text: part.text, providerOptions, }; } case 'text': { return { type: 'text' as const, text: part.text, providerOptions, }; } case 'tool-call': { return { type: 'tool-call' as const, toolCallId: part.toolCallId, toolName: part.toolName, input: part.input, providerExecuted: part.providerExecuted, providerOptions, }; } case 'tool-result': { return { type: 'tool-result' as const, toolCallId: part.toolCallId, toolName: part.toolName, output: part.output, providerOptions, }; } } }), providerOptions: message.providerOptions, }; } case 'tool': { return { role: 'tool', content: message.content.map(part => ({ type: 'tool-result' as const, toolCallId: part.toolCallId, toolName: part.toolName, output: part.output, providerOptions: part.providerOptions, })), providerOptions: message.providerOptions, }; } default: { const _exhaustiveCheck: never = role; throw new InvalidMessageRoleError({ role: _exhaustiveCheck }); } } } /** * Downloads images and files from URLs in the messages. */ async function downloadAssets( messages: ModelMessage[], downloadImplementation: typeof download, supportedUrls: Record<string, RegExp[]>, ): Promise< Record<string, { mediaType: string | undefined; data: Uint8Array }> > { const urls = messages .filter(message => message.role === 'user') .map(message => message.content) .filter((content): content is Array<TextPart | ImagePart | FilePart> => Array.isArray(content), ) .flat() .filter( (part): part is ImagePart | FilePart => part.type === 'image' || part.type === 'file', ) .map(part => { const mediaType = part.mediaType ?? (part.type === 'image' ? 'image/*' : undefined); let data = part.type === 'image' ? part.image : part.data; if (typeof data === 'string') { try { data = new URL(data); } catch (ignored) {} } return { mediaType, data }; }) /** * Filter out URLs that the model supports natively, so we don't download them. */ .filter( (part): part is { mediaType: string; data: URL } => part.data instanceof URL && part.mediaType != null && !isUrlSupported({ url: part.data.toString(), mediaType: part.mediaType, supportedUrls, }), ) .map(part => part.data); // download in parallel: const downloadedImages = await Promise.all( urls.map(async url => ({ url, data: await downloadImplementation({ url }), })), ); return Object.fromEntries( downloadedImages.map(({ url, data }) => [url.toString(), data]), ); } /** * Convert part of a message to a LanguageModelV2Part. * @param part The part to convert. * @param downloadedAssets A map of URLs to their downloaded data. Only * available if the model does not support URLs, null otherwise. * * @returns The converted part. */ function convertPartToLanguageModelPart( part: TextPart | ImagePart | FilePart, downloadedAssets: Record< string, { mediaType: string | undefined; data: Uint8Array } >, ): LanguageModelV2TextPart | LanguageModelV2FilePart { if (part.type === 'text') { return { type: 'text', text: part.text, providerOptions: part.providerOptions, }; } let originalData: DataContent | URL; const type = part.type; switch (type) { case 'image': originalData = part.image; break; case 'file': originalData = part.data; break; default: throw new Error(`Unsupported part type: ${type}`); } const { data: convertedData, mediaType: convertedMediaType } = convertToLanguageModelV2DataContent(originalData); let mediaType: string | undefined = convertedMediaType ?? part.mediaType; let data: Uint8Array | string | URL = convertedData; // binary | base64 | url // If the content is a URL, we check if it was downloaded: if (data instanceof URL) { const downloadedFile = downloadedAssets[data.toString()]; if (downloadedFile) { data = downloadedFile.data; mediaType ??= downloadedFile.mediaType; } } // Now that we have the normalized data either as a URL or a Uint8Array, // we can create the LanguageModelV2Part. switch (type) { case 'image': { // When possible, try to detect the media type automatically // to deal with incorrect media type inputs. // When detection fails, use provided media type. if (data instanceof Uint8Array || typeof data === 'string') { mediaType = detectMediaType({ data, signatures: imageMediaTypeSignatures }) ?? mediaType; } return { type: 'file', mediaType: mediaType ?? 'image/*', // any image filename: undefined, data, providerOptions: part.providerOptions, }; } case 'file': { // We must have a mediaType for files, if not, throw an error. if (mediaType == null) { throw new Error(`Media type is missing for file part`); } return { type: 'file', mediaType, filename: part.filename, data, providerOptions: part.providerOptions, }; } } } --- File: /ai/packages/ai/src/prompt/create-tool-model-output.test.ts --- import { Tool } from '@ai-sdk/provider-utils'; import { createToolModelOutput } from './create-tool-model-output'; describe('createToolModelOutput', () => { describe('error cases', () => { it('should return error type with string value when isError is true and output is string', () => { const result = createToolModelOutput({ output: 'Error message', tool: undefined, errorMode: 'text', }); expect(result).toEqual({ type: 'error-text', value: 'Error message', }); }); it('should return error type with JSON stringified value when isError is true and output is not string', () => { const errorOutput = { error: 'Something went wrong', code: 500 }; const result = createToolModelOutput({ output: errorOutput, tool: undefined, errorMode: 'text', }); expect(result).toEqual({ type: 'error-text', value: JSON.stringify(errorOutput), }); }); it('should return error type with JSON stringified value for complex objects', () => { const complexError = { message: 'Complex error', details: { timestamp: '2023-01-01T00:00:00Z', stack: ['line1', 'line2'], }, }; const result = createToolModelOutput({ output: complexError, tool: undefined, errorMode: 'text', }); expect(result).toEqual({ type: 'error-text', value: JSON.stringify(complexError), }); }); }); describe('tool with toModelOutput', () => { it('should use tool.toModelOutput when available', () => { const mockTool: Tool = { toModelOutput: (output: any) => ({ type: 'text', value: `Custom output: ${output}`, }), }; const result = createToolModelOutput({ output: 'test output', tool: mockTool, errorMode: 'none', }); expect(result).toEqual({ type: 'text', value: 'Custom output: test output', }); }); it('should use tool.toModelOutput with complex output', () => { const mockTool: Tool = { toModelOutput: (output: any) => ({ type: 'json', value: { processed: output, timestamp: '2023-01-01' }, }), }; const complexOutput = { data: [1, 2, 3], status: 'success' }; const result = createToolModelOutput({ output: complexOutput, tool: mockTool, errorMode: 'none', }); expect(result).toEqual({ type: 'json', value: { processed: complexOutput, timestamp: '2023-01-01' }, }); }); it('should use tool.toModelOutput returning content type', () => { const mockTool: Tool = { toModelOutput: () => ({ type: 'content', value: [ { type: 'text', text: 'Here is the result:' }, { type: 'text', text: 'Additional information' }, ], }), }; const result = createToolModelOutput({ output: 'any output', tool: mockTool, errorMode: 'none', }); expect(result).toEqual({ type: 'content', value: [ { type: 'text', text: 'Here is the result:' }, { type: 'text', text: 'Additional information' }, ], }); }); }); describe('string output without toModelOutput', () => { it('should return text type for string output', () => { const result = createToolModelOutput({ output: 'Simple string output', tool: undefined, errorMode: 'none', }); expect(result).toEqual({ type: 'text', value: 'Simple string output', }); }); it('should return text type for string output even with tool that has no toModelOutput', () => { const toolWithoutToModelOutput: Tool = { description: 'A tool without toModelOutput', }; const result = createToolModelOutput({ output: 'String output', tool: toolWithoutToModelOutput, errorMode: 'none', }); expect(result).toEqual({ type: 'text', value: 'String output', }); }); it('should return text type for empty string', () => { const result = createToolModelOutput({ output: '', tool: undefined, errorMode: 'none', }); expect(result).toEqual({ type: 'text', value: '', }); }); }); describe('non-string output without toModelOutput', () => { it('should return json type for object output', () => { const objectOutput = { result: 'success', data: [1, 2, 3] }; const result = createToolModelOutput({ output: objectOutput, tool: undefined, errorMode: 'none', }); expect(result).toEqual({ type: 'json', value: objectOutput, }); }); it('should return json type for array output', () => { const arrayOutput = [1, 2, 3, 'test']; const result = createToolModelOutput({ output: arrayOutput, tool: undefined, errorMode: 'none', }); expect(result).toEqual({ type: 'json', value: arrayOutput, }); }); it('should return json type for number output', () => { const result = createToolModelOutput({ output: 42, tool: undefined, errorMode: 'none', }); expect(result).toEqual({ type: 'json', value: 42, }); }); it('should return json type for boolean output', () => { const result = createToolModelOutput({ output: true, tool: undefined, errorMode: 'none', }); expect(result).toEqual({ type: 'json', value: true, }); }); it('should return json type for null output', () => { const result = createToolModelOutput({ output: null, tool: undefined, errorMode: 'none', }); expect(result).toEqual({ type: 'json', value: null, }); }); it('should return json type for complex nested object', () => { const complexOutput = { user: { id: 123, name: 'John Doe', preferences: { theme: 'dark', notifications: true, }, }, metadata: { timestamp: '2023-01-01T00:00:00Z', version: '1.0.0', }, items: [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }, ], }; const result = createToolModelOutput({ output: complexOutput, tool: undefined, errorMode: 'none', }); expect(result).toEqual({ type: 'json', value: complexOutput, }); }); }); describe('edge cases', () => { it('should prioritize isError over tool.toModelOutput', () => { const mockTool: Tool = { toModelOutput: () => ({ type: 'text', value: 'This should not be called', }), }; const result = createToolModelOutput({ output: 'Error occurred', tool: mockTool, errorMode: 'text', }); expect(result).toEqual({ type: 'error-text', value: 'Error occurred', }); }); it('should handle undefined output in error text case', () => { const result = createToolModelOutput({ output: undefined, tool: undefined, errorMode: 'text', }); expect(result).toEqual({ type: 'error-text', value: 'unknown error', }); }); it('should use null for undefined output in error json case', () => { const result = createToolModelOutput({ output: undefined, tool: undefined, errorMode: 'json', }); expect(result).toEqual({ type: 'error-json', value: null, }); }); it('should use null for undefined output in non-error case', () => { const result = createToolModelOutput({ output: undefined, tool: undefined, errorMode: 'none', }); expect(result).toEqual({ type: 'json', value: null, }); }); }); }); --- File: /ai/packages/ai/src/prompt/create-tool-model-output.ts --- import { getErrorMessage, JSONValue, LanguageModelV2ToolResultOutput, } from '@ai-sdk/provider'; import { Tool } from '@ai-sdk/provider-utils'; export function createToolModelOutput({ output, tool, errorMode, }: { output: unknown; tool: Tool | undefined; errorMode: 'none' | 'text' | 'json'; }): LanguageModelV2ToolResultOutput { if (errorMode === 'text') { return { type: 'error-text', value: getErrorMessage(output) }; } else if (errorMode === 'json') { return { type: 'error-json', value: toJSONValue(output) }; } if (tool?.toModelOutput) { return tool.toModelOutput(output); } return typeof output === 'string' ? { type: 'text', value: output } : { type: 'json', value: toJSONValue(output) }; } function toJSONValue(value: unknown): JSONValue { return value === undefined ? null : (value as JSONValue); } --- File: /ai/packages/ai/src/prompt/data-content.test.ts --- import { dataContentSchema } from './data-content'; describe('dataContentSchema', () => { it('should validate a Buffer', () => { const buffer = Buffer.from('Hello, world!'); const result = dataContentSchema.parse(buffer); expect(result).toEqual(buffer); }); it('should reject a non-matching object', () => { const nonMatchingObject = { foo: 'bar' }; expect(() => dataContentSchema.parse(nonMatchingObject)).toThrow(); }); }); --- File: /ai/packages/ai/src/prompt/data-content.ts --- import { AISDKError, LanguageModelV2DataContent } from '@ai-sdk/provider'; import { convertBase64ToUint8Array, convertUint8ArrayToBase64, DataContent, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { InvalidDataContentError } from './invalid-data-content-error'; import { splitDataUrl } from './split-data-url'; /** @internal */ export const dataContentSchema: z.ZodType<DataContent> = z.union([ z.string(), z.instanceof(Uint8Array), z.instanceof(ArrayBuffer), z.custom<Buffer>( // Buffer might not be available in some environments such as CloudFlare: (value: unknown): value is Buffer => globalThis.Buffer?.isBuffer(value) ?? false, { message: 'Must be a Buffer' }, ), ]); export function convertToLanguageModelV2DataContent( content: DataContent | URL, ): { data: LanguageModelV2DataContent; mediaType: string | undefined; } { // Buffer & Uint8Array: if (content instanceof Uint8Array) { return { data: content, mediaType: undefined }; } // ArrayBuffer needs conversion to Uint8Array (lightweight): if (content instanceof ArrayBuffer) { return { data: new Uint8Array(content), mediaType: undefined }; } // Attempt to create a URL from the data. If it fails, we can assume the data // is not a URL and likely some other sort of data. if (typeof content === 'string') { try { content = new URL(content); } catch (error) { // ignored } } // Extract data from data URL: if (content instanceof URL && content.protocol === 'data:') { const { mediaType: dataUrlMediaType, base64Content } = splitDataUrl( content.toString(), ); if (dataUrlMediaType == null || base64Content == null) { throw new AISDKError({ name: 'InvalidDataContentError', message: `Invalid data URL format in content ${content.toString()}`, }); } return { data: base64Content, mediaType: dataUrlMediaType }; } return { data: content, mediaType: undefined }; } /** Converts data content to a base64-encoded string. @param content - Data content to convert. @returns Base64-encoded string. */ export function convertDataContentToBase64String(content: DataContent): string { if (typeof content === 'string') { return content; } if (content instanceof ArrayBuffer) { return convertUint8ArrayToBase64(new Uint8Array(content)); } return convertUint8ArrayToBase64(content); } /** Converts data content to a Uint8Array. @param content - Data content to convert. @returns Uint8Array. */ export function convertDataContentToUint8Array( content: DataContent, ): Uint8Array { if (content instanceof Uint8Array) { return content; } if (typeof content === 'string') { try { return convertBase64ToUint8Array(content); } catch (error) { throw new InvalidDataContentError({ message: 'Invalid data content. Content string is not a base64-encoded media.', content, cause: error, }); } } if (content instanceof ArrayBuffer) { return new Uint8Array(content); } throw new InvalidDataContentError({ content }); } /** * Converts a Uint8Array to a string of text. * * @param uint8Array - The Uint8Array to convert. * @returns The converted string. */ export function convertUint8ArrayToText(uint8Array: Uint8Array): string { try { return new TextDecoder().decode(uint8Array); } catch (error) { throw new Error('Error decoding Uint8Array to text'); } } --- File: /ai/packages/ai/src/prompt/index.ts --- export type { CallSettings } from './call-settings'; export { assistantModelMessageSchema, coreAssistantMessageSchema, coreMessageSchema, coreSystemMessageSchema, coreToolMessageSchema, coreUserMessageSchema, modelMessageSchema, systemModelMessageSchema, toolModelMessageSchema, userModelMessageSchema, } from './message'; export type { CoreAssistantMessage, CoreMessage, CoreSystemMessage, CoreToolMessage, CoreUserMessage, } from './message'; export type { Prompt } from './prompt'; // re-export types from provider-utils export type { AssistantContent, AssistantModelMessage, DataContent, FilePart, ImagePart, ModelMessage, SystemModelMessage, TextPart, ToolCallPart, ToolContent, ToolModelMessage, ToolResultPart, UserContent, UserModelMessage, } from '@ai-sdk/provider-utils'; --- File: /ai/packages/ai/src/prompt/invalid-data-content-error.ts --- import { AISDKError } from '@ai-sdk/provider'; const name = 'AI_InvalidDataContentError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class InvalidDataContentError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly content: unknown; constructor({ content, cause, message = `Invalid data content. Expected a base64 string, Uint8Array, ArrayBuffer, or Buffer, but got ${typeof content}.`, }: { content: unknown; cause?: unknown; message?: string; }) { super({ name, message, cause }); this.content = content; } static isInstance(error: unknown): error is InvalidDataContentError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/ai/src/prompt/invalid-message-role-error.ts --- import { AISDKError } from '@ai-sdk/provider'; const name = 'AI_InvalidMessageRoleError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class InvalidMessageRoleError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly role: string; constructor({ role, message = `Invalid message role: '${role}'. Must be one of: "system", "user", "assistant", "tool".`, }: { role: string; message?: string; }) { super({ name, message }); this.role = role; } static isInstance(error: unknown): error is InvalidMessageRoleError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/ai/src/prompt/message-conversion-error.ts --- import { AISDKError } from '@ai-sdk/provider'; import { UIMessage } from '../ui/ui-messages'; const name = 'AI_MessageConversionError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class MessageConversionError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly originalMessage: Omit<UIMessage, 'id'>; constructor({ originalMessage, message, }: { originalMessage: Omit<UIMessage, 'id'>; message: string; }) { super({ name, message }); this.originalMessage = originalMessage; } static isInstance(error: unknown): error is MessageConversionError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/ai/src/prompt/message.ts --- import { AssistantModelMessage, ModelMessage, SystemModelMessage, ToolModelMessage, UserModelMessage, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { providerMetadataSchema } from '../types/provider-metadata'; import { filePartSchema, imagePartSchema, reasoningPartSchema, textPartSchema, toolCallPartSchema, toolResultPartSchema, } from './content-part'; /** @deprecated Use `SystemModelMessage` instead. */ // TODO remove in AI SDK 6 export type CoreSystemMessage = SystemModelMessage; export const systemModelMessageSchema: z.ZodType<SystemModelMessage> = z.object( { role: z.literal('system'), content: z.string(), providerOptions: providerMetadataSchema.optional(), }, ); /** @deprecated Use `systemModelMessageSchema` instead. */ // TODO remove in AI SDK 6 export const coreSystemMessageSchema = systemModelMessageSchema; /** @deprecated Use `UserModelMessage` instead. */ // TODO remove in AI SDK 6 export type CoreUserMessage = UserModelMessage; export const userModelMessageSchema: z.ZodType<UserModelMessage> = z.object({ role: z.literal('user'), content: z.union([ z.string(), z.array(z.union([textPartSchema, imagePartSchema, filePartSchema])), ]), providerOptions: providerMetadataSchema.optional(), }); /** @deprecated Use `userModelMessageSchema` instead. */ // TODO remove in AI SDK 6 export const coreUserMessageSchema = userModelMessageSchema; /** @deprecated Use `AssistantModelMessage` instead. */ // TODO remove in AI SDK 6 export type CoreAssistantMessage = AssistantModelMessage; export const assistantModelMessageSchema: z.ZodType<AssistantModelMessage> = z.object({ role: z.literal('assistant'), content: z.union([ z.string(), z.array( z.union([ textPartSchema, filePartSchema, reasoningPartSchema, toolCallPartSchema, toolResultPartSchema, ]), ), ]), providerOptions: providerMetadataSchema.optional(), }); /** @deprecated Use `assistantModelMessageSchema` instead. */ // TODO remove in AI SDK 6 export const coreAssistantMessageSchema = assistantModelMessageSchema; /** @deprecated Use `ToolModelMessage` instead. */ // TODO remove in AI SDK 6 export type CoreToolMessage = ToolModelMessage; export const toolModelMessageSchema: z.ZodType<ToolModelMessage> = z.object({ role: z.literal('tool'), content: z.array(toolResultPartSchema), providerOptions: providerMetadataSchema.optional(), }); /** @deprecated Use `toolModelMessageSchema` instead. */ // TODO remove in AI SDK 6 export const coreToolMessageSchema = toolModelMessageSchema; /** @deprecated Use `ModelMessage` instead. */ // TODO remove in AI SDK 6 export type CoreMessage = ModelMessage; export const modelMessageSchema: z.ZodType<ModelMessage> = z.union([ systemModelMessageSchema, userModelMessageSchema, assistantModelMessageSchema, toolModelMessageSchema, ]); /** @deprecated Use `modelMessageSchema` instead. */ // TODO remove in AI SDK 6 export const coreMessageSchema: z.ZodType<CoreMessage> = modelMessageSchema; --- File: /ai/packages/ai/src/prompt/prepare-call-settings.test.ts --- import { InvalidArgumentError } from '../error/invalid-argument-error'; import { prepareCallSettings } from './prepare-call-settings'; describe('prepareCallSettings', () => { describe('valid inputs', () => { it('should not throw an error for valid settings', () => { const validSettings = { maxOutputTokens: 100, temperature: 0.7, topP: 0.9, topK: 50, presencePenalty: 0.5, frequencyPenalty: 0.3, seed: 42, // stopSequences is not validated by validateCallSettings }; expect(() => prepareCallSettings(validSettings)).not.toThrow(); }); it('should allow undefined values for optional settings', () => { const validSettings = { maxOutputTokens: undefined, temperature: undefined, topP: undefined, topK: undefined, presencePenalty: undefined, frequencyPenalty: undefined, seed: undefined, }; expect(() => prepareCallSettings(validSettings)).not.toThrow(); }); }); describe('invalid inputs', () => { describe('maxOutputTokens', () => { it('should throw InvalidArgumentError if maxOutputTokens is not an integer', () => { expect(() => prepareCallSettings({ maxOutputTokens: 10.5 })).toThrow( new InvalidArgumentError({ parameter: 'maxOutputTokens', value: 10.5, message: 'maxOutputTokens must be an integer', }), ); }); it('should throw InvalidArgumentError if maxOutputTokens is less than 1', () => { expect(() => prepareCallSettings({ maxOutputTokens: 0 })).toThrow( new InvalidArgumentError({ parameter: 'maxOutputTokens', value: 0, message: 'maxOutputTokens must be >= 1', }), ); }); }); describe('temperature', () => { it('should throw InvalidArgumentError if temperature is not a number', () => { expect(() => prepareCallSettings({ temperature: 'invalid' as any }), ).toThrow( new InvalidArgumentError({ parameter: 'temperature', value: 'invalid', message: 'temperature must be a number', }), ); }); }); describe('topP', () => { it('should throw InvalidArgumentError if topP is not a number', () => { expect(() => prepareCallSettings({ topP: 'invalid' as any })).toThrow( new InvalidArgumentError({ parameter: 'topP', value: 'invalid', message: 'topP must be a number', }), ); }); }); describe('topK', () => { it('should throw InvalidArgumentError if topK is not a number', () => { expect(() => prepareCallSettings({ topK: 'invalid' as any })).toThrow( new InvalidArgumentError({ parameter: 'topK', value: 'invalid', message: 'topK must be a number', }), ); }); }); describe('presencePenalty', () => { it('should throw InvalidArgumentError if presencePenalty is not a number', () => { expect(() => prepareCallSettings({ presencePenalty: 'invalid' as any }), ).toThrow( new InvalidArgumentError({ parameter: 'presencePenalty', value: 'invalid', message: 'presencePenalty must be a number', }), ); }); }); describe('frequencyPenalty', () => { it('should throw InvalidArgumentError if frequencyPenalty is not a number', () => { expect(() => prepareCallSettings({ frequencyPenalty: 'invalid' as any }), ).toThrow( new InvalidArgumentError({ parameter: 'frequencyPenalty', value: 'invalid', message: 'frequencyPenalty must be a number', }), ); }); }); describe('seed', () => { it('should throw InvalidArgumentError if seed is not an integer', () => { expect(() => prepareCallSettings({ seed: 10.5 })).toThrow( new InvalidArgumentError({ parameter: 'seed', value: 10.5, message: 'seed must be an integer', }), ); }); }); }); it('should return a new object with limited values', () => { const settings = prepareCallSettings({ maxOutputTokens: 100, temperature: 0.7, random: 'invalid', } as any); expect(settings).toMatchInlineSnapshot(` { "frequencyPenalty": undefined, "maxOutputTokens": 100, "presencePenalty": undefined, "seed": undefined, "stopSequences": undefined, "temperature": 0.7, "topK": undefined, "topP": undefined, } `); }); }); --- File: /ai/packages/ai/src/prompt/prepare-call-settings.ts --- import { InvalidArgumentError } from '../error/invalid-argument-error'; import { CallSettings } from './call-settings'; /** * Validates call settings and returns a new object with limited values. */ export function prepareCallSettings({ maxOutputTokens, temperature, topP, topK, presencePenalty, frequencyPenalty, seed, stopSequences, }: Omit<CallSettings, 'abortSignal' | 'headers' | 'maxRetries'>): Omit< CallSettings, 'abortSignal' | 'headers' | 'maxRetries' > { if (maxOutputTokens != null) { if (!Number.isInteger(maxOutputTokens)) { throw new InvalidArgumentError({ parameter: 'maxOutputTokens', value: maxOutputTokens, message: 'maxOutputTokens must be an integer', }); } if (maxOutputTokens < 1) { throw new InvalidArgumentError({ parameter: 'maxOutputTokens', value: maxOutputTokens, message: 'maxOutputTokens must be >= 1', }); } } if (temperature != null) { if (typeof temperature !== 'number') { throw new InvalidArgumentError({ parameter: 'temperature', value: temperature, message: 'temperature must be a number', }); } } if (topP != null) { if (typeof topP !== 'number') { throw new InvalidArgumentError({ parameter: 'topP', value: topP, message: 'topP must be a number', }); } } if (topK != null) { if (typeof topK !== 'number') { throw new InvalidArgumentError({ parameter: 'topK', value: topK, message: 'topK must be a number', }); } } if (presencePenalty != null) { if (typeof presencePenalty !== 'number') { throw new InvalidArgumentError({ parameter: 'presencePenalty', value: presencePenalty, message: 'presencePenalty must be a number', }); } } if (frequencyPenalty != null) { if (typeof frequencyPenalty !== 'number') { throw new InvalidArgumentError({ parameter: 'frequencyPenalty', value: frequencyPenalty, message: 'frequencyPenalty must be a number', }); } } if (seed != null) { if (!Number.isInteger(seed)) { throw new InvalidArgumentError({ parameter: 'seed', value: seed, message: 'seed must be an integer', }); } } return { maxOutputTokens, temperature, topP, topK, presencePenalty, frequencyPenalty, stopSequences, seed, }; } --- File: /ai/packages/ai/src/prompt/prepare-tools-and-tool-choice.test.ts --- import { z } from 'zod/v4'; import { prepareToolsAndToolChoice } from './prepare-tools-and-tool-choice'; import { Tool, tool } from '@ai-sdk/provider-utils'; const mockTools = { tool1: tool({ description: 'Tool 1 description', inputSchema: z.object({}), }), tool2: tool({ description: 'Tool 2 description', inputSchema: z.object({ city: z.string() }), }), }; const mockProviderDefinedTool: Tool = { type: 'provider-defined', id: 'provider.tool-id', name: 'tool-id', args: { key: 'value' }, inputSchema: z.object({}), }; const mockToolsWithProviderDefined = { ...mockTools, providerTool: mockProviderDefinedTool, }; describe('prepareToolsAndToolChoice', () => { it('should return undefined for both tools and toolChoice when tools is not provided', () => { const result = prepareToolsAndToolChoice({ tools: undefined, toolChoice: undefined, activeTools: undefined, }); expect(result).toMatchInlineSnapshot(` { "toolChoice": undefined, "tools": undefined, } `); }); it('should return all tools when activeTools is not provided', () => { const result = prepareToolsAndToolChoice({ tools: mockTools, toolChoice: undefined, activeTools: undefined, }); expect(result).toMatchInlineSnapshot(` { "toolChoice": { "type": "auto", }, "tools": [ { "description": "Tool 1 description", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": {}, "type": "object", }, "name": "tool1", "providerOptions": undefined, "type": "function", }, { "description": "Tool 2 description", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "city": { "type": "string", }, }, "required": [ "city", ], "type": "object", }, "name": "tool2", "providerOptions": undefined, "type": "function", }, ], } `); }); it('should filter tools based on activeTools', () => { const result = prepareToolsAndToolChoice({ tools: mockTools, toolChoice: undefined, activeTools: ['tool1'], }); expect(result).toMatchInlineSnapshot(` { "toolChoice": { "type": "auto", }, "tools": [ { "description": "Tool 1 description", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": {}, "type": "object", }, "name": "tool1", "providerOptions": undefined, "type": "function", }, ], } `); }); it('should handle string toolChoice', () => { const result = prepareToolsAndToolChoice({ tools: mockTools, toolChoice: 'none', activeTools: undefined, }); expect(result).toMatchInlineSnapshot(` { "toolChoice": { "type": "none", }, "tools": [ { "description": "Tool 1 description", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": {}, "type": "object", }, "name": "tool1", "providerOptions": undefined, "type": "function", }, { "description": "Tool 2 description", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "city": { "type": "string", }, }, "required": [ "city", ], "type": "object", }, "name": "tool2", "providerOptions": undefined, "type": "function", }, ], } `); }); it('should handle object toolChoice', () => { const result = prepareToolsAndToolChoice({ tools: mockTools, toolChoice: { type: 'tool', toolName: 'tool2' }, activeTools: undefined, }); expect(result).toMatchInlineSnapshot(` { "toolChoice": { "toolName": "tool2", "type": "tool", }, "tools": [ { "description": "Tool 1 description", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": {}, "type": "object", }, "name": "tool1", "providerOptions": undefined, "type": "function", }, { "description": "Tool 2 description", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "city": { "type": "string", }, }, "required": [ "city", ], "type": "object", }, "name": "tool2", "providerOptions": undefined, "type": "function", }, ], } `); }); it('should correctly map tool properties', () => { const result = prepareToolsAndToolChoice({ tools: mockTools, toolChoice: undefined, activeTools: undefined, }); expect(result).toMatchInlineSnapshot(` { "toolChoice": { "type": "auto", }, "tools": [ { "description": "Tool 1 description", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": {}, "type": "object", }, "name": "tool1", "providerOptions": undefined, "type": "function", }, { "description": "Tool 2 description", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "city": { "type": "string", }, }, "required": [ "city", ], "type": "object", }, "name": "tool2", "providerOptions": undefined, "type": "function", }, ], } `); }); it('should handle provider-defined tool type', () => { const result = prepareToolsAndToolChoice({ tools: mockToolsWithProviderDefined, toolChoice: undefined, activeTools: undefined, }); expect(result).toMatchInlineSnapshot(` { "toolChoice": { "type": "auto", }, "tools": [ { "description": "Tool 1 description", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": {}, "type": "object", }, "name": "tool1", "providerOptions": undefined, "type": "function", }, { "description": "Tool 2 description", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "city": { "type": "string", }, }, "required": [ "city", ], "type": "object", }, "name": "tool2", "providerOptions": undefined, "type": "function", }, { "args": { "key": "value", }, "id": "provider.tool-id", "name": "providerTool", "type": "provider-defined", }, ], } `); }); it('should pass through provider options', () => { const result = prepareToolsAndToolChoice({ tools: { tool1: tool({ description: 'Tool 1 description', inputSchema: z.object({}), providerOptions: { aProvider: { aSetting: 'aValue', }, }, }), }, toolChoice: undefined, activeTools: undefined, }); expect(result).toMatchInlineSnapshot(` { "toolChoice": { "type": "auto", }, "tools": [ { "description": "Tool 1 description", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": {}, "type": "object", }, "name": "tool1", "providerOptions": { "aProvider": { "aSetting": "aValue", }, }, "type": "function", }, ], } `); }); }); --- File: /ai/packages/ai/src/prompt/prepare-tools-and-tool-choice.ts --- import { LanguageModelV2FunctionTool, LanguageModelV2ProviderDefinedTool, LanguageModelV2ToolChoice, } from '@ai-sdk/provider'; import { asSchema } from '@ai-sdk/provider-utils'; import { isNonEmptyObject } from '../util/is-non-empty-object'; import { ToolSet } from '../generate-text'; import { ToolChoice } from '../types/language-model'; export function prepareToolsAndToolChoice<TOOLS extends ToolSet>({ tools, toolChoice, activeTools, }: { tools: TOOLS | undefined; toolChoice: ToolChoice<TOOLS> | undefined; activeTools: Array<keyof TOOLS> | undefined; }): { tools: | Array<LanguageModelV2FunctionTool | LanguageModelV2ProviderDefinedTool> | undefined; toolChoice: LanguageModelV2ToolChoice | undefined; } { if (!isNonEmptyObject(tools)) { return { tools: undefined, toolChoice: undefined, }; } // when activeTools is provided, we only include the tools that are in the list: const filteredTools = activeTools != null ? Object.entries(tools).filter(([name]) => activeTools.includes(name as keyof TOOLS), ) : Object.entries(tools); return { tools: filteredTools.map(([name, tool]) => { const toolType = tool.type; switch (toolType) { case undefined: case 'dynamic': case 'function': return { type: 'function' as const, name, description: tool.description, inputSchema: asSchema(tool.inputSchema).jsonSchema, providerOptions: tool.providerOptions, }; case 'provider-defined': return { type: 'provider-defined' as const, name, id: tool.id, args: tool.args, }; default: { const exhaustiveCheck: never = toolType; throw new Error(`Unsupported tool type: ${exhaustiveCheck}`); } } }), toolChoice: toolChoice == null ? { type: 'auto' } : typeof toolChoice === 'string' ? { type: toolChoice } : { type: 'tool' as const, toolName: toolChoice.toolName as string }, }; } --- File: /ai/packages/ai/src/prompt/prompt.ts --- import { ModelMessage } from '@ai-sdk/provider-utils'; /** Prompt part of the AI function options. It contains a system message, a simple text prompt, or a list of messages. */ export type Prompt = { /** System message to include in the prompt. Can be used with `prompt` or `messages`. */ system?: string; /** A prompt. It can be either a text prompt or a list of messages. You can either use `prompt` or `messages` but not both. */ prompt?: string | Array<ModelMessage>; /** A list of messages. You can either use `prompt` or `messages` but not both. */ messages?: Array<ModelMessage>; }; --- File: /ai/packages/ai/src/prompt/split-data-url.ts --- export function splitDataUrl(dataUrl: string): { mediaType: string | undefined; base64Content: string | undefined; } { try { const [header, base64Content] = dataUrl.split(','); return { mediaType: header.split(';')[0].split(':')[1], base64Content, }; } catch (error) { return { mediaType: undefined, base64Content: undefined, }; } } --- File: /ai/packages/ai/src/prompt/standardize-prompt.test.ts --- import { InvalidPromptError } from '@ai-sdk/provider'; import { standardizePrompt } from './standardize-prompt'; describe('message prompt', () => { it('should throw InvalidPromptError when system message has parts', async () => { await expect(async () => { await standardizePrompt({ messages: [ { role: 'system', content: [{ type: 'text', text: 'test' }] as any, }, ], }); }).rejects.toThrow(InvalidPromptError); }); it('should throw InvalidPromptError when messages array is empty', async () => { await expect(async () => { await standardizePrompt({ messages: [], }); }).rejects.toThrow(InvalidPromptError); }); }); --- File: /ai/packages/ai/src/prompt/standardize-prompt.ts --- import { InvalidPromptError } from '@ai-sdk/provider'; import { ModelMessage, safeValidateTypes } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { modelMessageSchema } from './message'; import { Prompt } from './prompt'; export type StandardizedPrompt = { /** * System message. */ system?: string; /** * Messages. */ messages: ModelMessage[]; }; export async function standardizePrompt( prompt: Prompt, ): Promise<StandardizedPrompt> { if (prompt.prompt == null && prompt.messages == null) { throw new InvalidPromptError({ prompt, message: 'prompt or messages must be defined', }); } if (prompt.prompt != null && prompt.messages != null) { throw new InvalidPromptError({ prompt, message: 'prompt and messages cannot be defined at the same time', }); } // validate that system is a string if (prompt.system != null && typeof prompt.system !== 'string') { throw new InvalidPromptError({ prompt, message: 'system must be a string', }); } let messages: ModelMessage[]; if (prompt.prompt != null && typeof prompt.prompt === 'string') { messages = [{ role: 'user', content: prompt.prompt }]; } else if (prompt.prompt != null && Array.isArray(prompt.prompt)) { messages = prompt.prompt; } else if (prompt.messages != null) { messages = prompt.messages; } else { throw new InvalidPromptError({ prompt, message: 'prompt or messages must be defined', }); } if (messages.length === 0) { throw new InvalidPromptError({ prompt, message: 'messages must not be empty', }); } const validationResult = await safeValidateTypes({ value: messages, schema: z.array(modelMessageSchema), }); if (!validationResult.success) { throw new InvalidPromptError({ prompt, message: 'The messages must be a ModelMessage[]. ' + 'If you have passed a UIMessage[], you can use convertToModelMessages to convert them.', cause: validationResult.error, }); } return { messages, system: prompt.system, }; } --- File: /ai/packages/ai/src/prompt/wrap-gateway-error.ts --- import { GatewayAuthenticationError, GatewayModelNotFoundError, } from '@ai-sdk/gateway'; import { AISDKError } from '@ai-sdk/provider'; export function wrapGatewayError(error: unknown): unknown { if ( GatewayAuthenticationError.isInstance(error) || GatewayModelNotFoundError.isInstance(error) ) { return new AISDKError({ name: 'GatewayError', message: 'Vercel AI Gateway access failed. ' + 'If you want to use AI SDK providers directly, use the providers, e.g. @ai-sdk/openai, ' + 'or register a different global default provider.', cause: error, }); } return error; } --- File: /ai/packages/ai/src/registry/custom-provider.test.ts --- import { NoSuchModelError } from '@ai-sdk/provider'; import { describe, expect, it, vi } from 'vitest'; import { MockEmbeddingModelV2 } from '../test/mock-embedding-model-v2'; import { MockImageModelV2 } from '../test/mock-image-model-v2'; import { MockLanguageModelV2 } from '../test/mock-language-model-v2'; import { MockTranscriptionModelV2 } from '../test/mock-transcription-model-v2'; import { MockSpeechModelV2 } from '../test/mock-speech-model-v2'; import { customProvider } from './custom-provider'; const mockLanguageModel = new MockLanguageModelV2(); const mockEmbeddingModel = new MockEmbeddingModelV2(); const mockFallbackProvider = { languageModel: vi.fn(), textEmbeddingModel: vi.fn(), imageModel: vi.fn(), transcriptionModel: vi.fn(), speechModel: vi.fn(), }; describe('languageModel', () => { it('should return the language model if it exists', () => { const provider = customProvider({ languageModels: { 'test-model': mockLanguageModel }, }); expect(provider.languageModel('test-model')).toBe(mockLanguageModel); }); it('should use fallback provider if model not found and fallback exists', () => { mockFallbackProvider.languageModel.mockReturnValue(mockLanguageModel); const provider = customProvider({ fallbackProvider: mockFallbackProvider, }); expect(provider.languageModel('test-model')).toBe(mockLanguageModel); expect(mockFallbackProvider.languageModel).toHaveBeenCalledWith( 'test-model', ); }); it('should throw NoSuchModelError if model not found and no fallback', () => { const provider = customProvider({}); expect(() => provider.languageModel('test-model')).toThrow( NoSuchModelError, ); }); }); describe('textEmbeddingModel', () => { it('should return the embedding model if it exists', () => { const provider = customProvider({ textEmbeddingModels: { 'test-model': mockEmbeddingModel }, }); expect(provider.textEmbeddingModel('test-model')).toBe(mockEmbeddingModel); }); it('should use fallback provider if model not found and fallback exists', () => { mockFallbackProvider.textEmbeddingModel.mockReturnValue(mockEmbeddingModel); const provider = customProvider({ fallbackProvider: mockFallbackProvider, }); expect(provider.textEmbeddingModel('test-model')).toBe(mockEmbeddingModel); expect(mockFallbackProvider.textEmbeddingModel).toHaveBeenCalledWith( 'test-model', ); }); it('should throw NoSuchModelError if model not found and no fallback', () => { const provider = customProvider({}); expect(() => provider.textEmbeddingModel('test-model')).toThrow( NoSuchModelError, ); }); }); describe('imageModel', () => { const mockImageModel = new MockImageModelV2(); it('should return the image model if it exists', () => { const provider = customProvider({ imageModels: { 'test-model': mockImageModel }, }); expect(provider.imageModel('test-model')).toBe(mockImageModel); }); it('should use fallback provider if model not found and fallback exists', () => { mockFallbackProvider.imageModel = vi.fn().mockReturnValue(mockImageModel); const provider = customProvider({ fallbackProvider: mockFallbackProvider, }); expect(provider.imageModel('test-model')).toBe(mockImageModel); expect(mockFallbackProvider.imageModel).toHaveBeenCalledWith('test-model'); }); it('should throw NoSuchModelError if model not found and no fallback', () => { const provider = customProvider({}); expect(() => provider.imageModel('test-model')).toThrow(NoSuchModelError); }); }); describe('transcriptionModel', () => { const mockTranscriptionModel = new MockTranscriptionModelV2(); it('should return the transcription model if it exists', () => { const provider = customProvider({ transcriptionModels: { 'test-model': mockTranscriptionModel }, }); expect(provider.transcriptionModel('test-model')).toBe( mockTranscriptionModel, ); }); it('should use fallback provider if model not found and fallback exists', () => { mockFallbackProvider.transcriptionModel = vi .fn() .mockReturnValue(mockTranscriptionModel); const provider = customProvider({ fallbackProvider: mockFallbackProvider, }); expect(provider.transcriptionModel('test-model')).toBe( mockTranscriptionModel, ); expect(mockFallbackProvider.transcriptionModel).toHaveBeenCalledWith( 'test-model', ); }); it('should throw NoSuchModelError if model not found and no fallback', () => { const provider = customProvider({}); expect(() => provider.transcriptionModel('test-model')).toThrow( NoSuchModelError, ); }); }); describe('speechModel', () => { const mockSpeechModel = new MockSpeechModelV2(); it('should return the speech model if it exists', () => { const provider = customProvider({ speechModels: { 'test-model': mockSpeechModel }, }); expect(provider.speechModel('test-model')).toBe(mockSpeechModel); }); it('should use fallback provider if model not found and fallback exists', () => { mockFallbackProvider.speechModel = vi.fn().mockReturnValue(mockSpeechModel); const provider = customProvider({ fallbackProvider: mockFallbackProvider, }); expect(provider.speechModel('test-model')).toBe(mockSpeechModel); expect(mockFallbackProvider.speechModel).toHaveBeenCalledWith('test-model'); }); it('should throw NoSuchModelError if model not found and no fallback', () => { const provider = customProvider({}); expect(() => provider.speechModel('test-model')).toThrow(NoSuchModelError); }); }); --- File: /ai/packages/ai/src/registry/custom-provider.ts --- import { EmbeddingModelV2, ImageModelV2, LanguageModelV2, NoSuchModelError, ProviderV2, SpeechModelV2, TranscriptionModelV2, } from '@ai-sdk/provider'; /** * Creates a custom provider with specified language models, text embedding models, image models, transcription models, speech models, and an optional fallback provider. * * @param {Object} options - The options for creating the custom provider. * @param {Record<string, LanguageModel>} [options.languageModels] - A record of language models, where keys are model IDs and values are LanguageModel instances. * @param {Record<string, EmbeddingModel<string>>} [options.textEmbeddingModels] - A record of text embedding models, where keys are model IDs and values are EmbeddingModel<string> instances. * @param {Record<string, ImageModel>} [options.imageModels] - A record of image models, where keys are model IDs and values are ImageModel instances. * @param {Record<string, TranscriptionModel>} [options.transcriptionModels] - A record of transcription models, where keys are model IDs and values are TranscriptionModel instances. * @param {Record<string, SpeechModel>} [options.speechModels] - A record of speech models, where keys are model IDs and values are SpeechModel instances. * @param {Provider} [options.fallbackProvider] - An optional fallback provider to use when a requested model is not found in the custom provider. * @returns {Provider} A Provider object with languageModel, textEmbeddingModel, imageModel, transcriptionModel, and speechModel methods. * * @throws {NoSuchModelError} Throws when a requested model is not found and no fallback provider is available. */ export function customProvider< LANGUAGE_MODELS extends Record<string, LanguageModelV2>, EMBEDDING_MODELS extends Record<string, EmbeddingModelV2<string>>, IMAGE_MODELS extends Record<string, ImageModelV2>, TRANSCRIPTION_MODELS extends Record<string, TranscriptionModelV2>, SPEECH_MODELS extends Record<string, SpeechModelV2>, >({ languageModels, textEmbeddingModels, imageModels, transcriptionModels, speechModels, fallbackProvider, }: { languageModels?: LANGUAGE_MODELS; textEmbeddingModels?: EMBEDDING_MODELS; imageModels?: IMAGE_MODELS; transcriptionModels?: TRANSCRIPTION_MODELS; speechModels?: SPEECH_MODELS; fallbackProvider?: ProviderV2; }): ProviderV2 & { languageModel(modelId: ExtractModelId<LANGUAGE_MODELS>): LanguageModelV2; textEmbeddingModel( modelId: ExtractModelId<EMBEDDING_MODELS>, ): EmbeddingModelV2<string>; imageModel(modelId: ExtractModelId<IMAGE_MODELS>): ImageModelV2; transcriptionModel( modelId: ExtractModelId<TRANSCRIPTION_MODELS>, ): TranscriptionModelV2; speechModel(modelId: ExtractModelId<SPEECH_MODELS>): SpeechModelV2; } { return { languageModel(modelId: ExtractModelId<LANGUAGE_MODELS>): LanguageModelV2 { if (languageModels != null && modelId in languageModels) { return languageModels[modelId]; } if (fallbackProvider) { return fallbackProvider.languageModel(modelId); } throw new NoSuchModelError({ modelId, modelType: 'languageModel' }); }, textEmbeddingModel( modelId: ExtractModelId<EMBEDDING_MODELS>, ): EmbeddingModelV2<string> { if (textEmbeddingModels != null && modelId in textEmbeddingModels) { return textEmbeddingModels[modelId]; } if (fallbackProvider) { return fallbackProvider.textEmbeddingModel(modelId); } throw new NoSuchModelError({ modelId, modelType: 'textEmbeddingModel' }); }, imageModel(modelId: ExtractModelId<IMAGE_MODELS>): ImageModelV2 { if (imageModels != null && modelId in imageModels) { return imageModels[modelId]; } if (fallbackProvider?.imageModel) { return fallbackProvider.imageModel(modelId); } throw new NoSuchModelError({ modelId, modelType: 'imageModel' }); }, transcriptionModel( modelId: ExtractModelId<TRANSCRIPTION_MODELS>, ): TranscriptionModelV2 { if (transcriptionModels != null && modelId in transcriptionModels) { return transcriptionModels[modelId]; } if (fallbackProvider?.transcriptionModel) { return fallbackProvider.transcriptionModel(modelId); } throw new NoSuchModelError({ modelId, modelType: 'transcriptionModel' }); }, speechModel(modelId: ExtractModelId<SPEECH_MODELS>): SpeechModelV2 { if (speechModels != null && modelId in speechModels) { return speechModels[modelId]; } if (fallbackProvider?.speechModel) { return fallbackProvider.speechModel(modelId); } throw new NoSuchModelError({ modelId, modelType: 'speechModel' }); }, }; } /** * @deprecated Use `customProvider` instead. */ export const experimental_customProvider = customProvider; type ExtractModelId<MODELS extends Record<string, unknown>> = Extract< keyof MODELS, string >; --- File: /ai/packages/ai/src/registry/index.ts --- export { customProvider, experimental_customProvider } from './custom-provider'; export { NoSuchProviderError } from './no-such-provider-error'; export { createProviderRegistry, experimental_createProviderRegistry, } from './provider-registry'; export type { ProviderRegistryProvider } from './provider-registry'; --- File: /ai/packages/ai/src/registry/no-such-provider-error.ts --- import { AISDKError, NoSuchModelError } from '@ai-sdk/provider'; const name = 'AI_NoSuchProviderError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class NoSuchProviderError extends NoSuchModelError { private readonly [symbol] = true; // used in isInstance readonly providerId: string; readonly availableProviders: string[]; constructor({ modelId, modelType, providerId, availableProviders, message = `No such provider: ${providerId} (available providers: ${availableProviders.join()})`, }: { modelId: string; modelType: | 'languageModel' | 'textEmbeddingModel' | 'imageModel' | 'transcriptionModel' | 'speechModel'; providerId: string; availableProviders: string[]; message?: string; }) { super({ errorName: name, modelId, modelType, message }); this.providerId = providerId; this.availableProviders = availableProviders; } static isInstance(error: unknown): error is NoSuchProviderError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/ai/src/registry/provider-registry.test.ts --- import { NoSuchModelError } from '@ai-sdk/provider'; import { MockEmbeddingModelV2 } from '../test/mock-embedding-model-v2'; import { MockLanguageModelV2 } from '../test/mock-language-model-v2'; import { NoSuchProviderError } from './no-such-provider-error'; import { createProviderRegistry } from './provider-registry'; import { MockImageModelV2 } from '../test/mock-image-model-v2'; import { MockTranscriptionModelV2 } from '../test/mock-transcription-model-v2'; import { MockSpeechModelV2 } from '../test/mock-speech-model-v2'; import { MockProviderV2 } from '../test/mock-provider-v2'; describe('languageModel', () => { it('should return language model from provider', () => { const model = new MockLanguageModelV2(); const modelRegistry = createProviderRegistry({ provider: { languageModel: (id: string) => { expect(id).toEqual('model'); return model; }, textEmbeddingModel: (id: string) => { return null as any; }, imageModel: (id: string) => { return null as any; }, }, }); expect(modelRegistry.languageModel('provider:model')).toEqual(model); }); it('should return language model with additional colon from provider', () => { const model = new MockLanguageModelV2(); const modelRegistry = createProviderRegistry({ provider: { languageModel: id => { expect(id).toEqual('model:part2'); return model; }, textEmbeddingModel: () => { return null as any; }, imageModel: () => { return null as any; }, }, }); expect(modelRegistry.languageModel('provider:model:part2')).toEqual(model); }); it('should throw NoSuchProviderError if provider does not exist', () => { const registry = createProviderRegistry({}); // @ts-expect-error - should not accept arbitrary strings expect(() => registry.languageModel('provider:model:part2')).toThrowError( NoSuchProviderError, ); }); it('should throw NoSuchModelError if provider does not return a model', () => { const registry = createProviderRegistry({ provider: { languageModel: () => { return null as any; }, textEmbeddingModel: () => { return null as any; }, imageModel: () => { return null as any; }, transcriptionModel: () => { return null as any; }, speechModel: () => { return null as any; }, }, }); expect(() => registry.languageModel('provider:model')).toThrowError( NoSuchModelError, ); }); it("should throw NoSuchModelError if model id doesn't contain a colon", () => { const registry = createProviderRegistry({}); // @ts-expect-error - should not accept arbitrary strings expect(() => registry.languageModel('model')).toThrowError( NoSuchModelError, ); }); it('should support custom separator', () => { const model = new MockLanguageModelV2(); const modelRegistry = createProviderRegistry( { provider: { languageModel: id => { expect(id).toEqual('model'); return model; }, textEmbeddingModel: () => { return null as any; }, imageModel: () => { return null as any; }, transcriptionModel: () => { return null as any; }, speechModel: () => { return null as any; }, }, }, { separator: '|' }, ); expect(modelRegistry.languageModel('provider|model')).toEqual(model); }); it('should support custom separator with multiple characters', () => { const model = new MockLanguageModelV2(); const modelRegistry = createProviderRegistry( { provider: { languageModel: id => { expect(id).toEqual('model'); return model; }, textEmbeddingModel: () => { return null as any; }, imageModel: () => { return null as any; }, transcriptionModel: () => { return null as any; }, speechModel: () => { return null as any; }, }, }, { separator: ' > ' }, ); expect(modelRegistry.languageModel('provider > model')).toEqual(model); }); }); describe('textEmbeddingModel', () => { it('should return embedding model from provider using textEmbeddingModel', () => { const model = new MockEmbeddingModelV2<string>(); const modelRegistry = createProviderRegistry({ provider: { textEmbeddingModel: id => { expect(id).toEqual('model'); return model; }, languageModel: () => { return null as any; }, imageModel: () => { return null as any; }, transcriptionModel: () => { return null as any; }, speechModel: () => { return null as any; }, }, }); expect(modelRegistry.textEmbeddingModel('provider:model')).toEqual(model); }); it('should throw NoSuchProviderError if provider does not exist', () => { const registry = createProviderRegistry({}); // @ts-expect-error - should not accept arbitrary strings expect(() => registry.textEmbeddingModel('provider:model')).toThrowError( NoSuchProviderError, ); }); it('should throw NoSuchModelError if provider does not return a model', () => { const registry = createProviderRegistry({ provider: { textEmbeddingModel: () => { return null as any; }, languageModel: () => { return null as any; }, imageModel: () => { return null as any; }, }, }); expect(() => registry.languageModel('provider:model')).toThrowError( NoSuchModelError, ); }); it("should throw NoSuchModelError if model id doesn't contain a colon", () => { const registry = createProviderRegistry({}); // @ts-expect-error - should not accept arbitrary strings expect(() => registry.textEmbeddingModel('model')).toThrowError( NoSuchModelError, ); }); it('should support custom separator', () => { const model = new MockEmbeddingModelV2<string>(); const modelRegistry = createProviderRegistry( { provider: { textEmbeddingModel: id => { expect(id).toEqual('model'); return model; }, languageModel: () => { return null as any; }, imageModel: () => { return null as any; }, transcriptionModel: () => { return null as any; }, speechModel: () => { return null as any; }, }, }, { separator: '|' }, ); expect(modelRegistry.textEmbeddingModel('provider|model')).toEqual(model); }); }); describe('imageModel', () => { it('should return image model from provider', () => { const model = new MockImageModelV2(); const modelRegistry = createProviderRegistry({ provider: { imageModel: id => { expect(id).toEqual('model'); return model; }, languageModel: () => null as any, textEmbeddingModel: () => null as any, transcriptionModel: () => null as any, speechModel: () => null as any, }, }); expect(modelRegistry.imageModel('provider:model')).toEqual(model); }); it('should throw NoSuchProviderError if provider does not exist', () => { const registry = createProviderRegistry({}); // @ts-expect-error - should not accept arbitrary strings expect(() => registry.imageModel('provider:model')).toThrowError( NoSuchProviderError, ); }); it('should throw NoSuchModelError if provider does not return a model', () => { const registry = createProviderRegistry({ provider: { imageModel: () => null as any, languageModel: () => null as any, textEmbeddingModel: () => null as any, }, }); expect(() => registry.imageModel('provider:model')).toThrowError( NoSuchModelError, ); }); it("should throw NoSuchModelError if model id doesn't contain a colon", () => { const registry = createProviderRegistry({}); // @ts-expect-error - should not accept arbitrary strings expect(() => registry.imageModel('model')).toThrowError(NoSuchModelError); }); it('should support custom separator', () => { const model = new MockImageModelV2(); const modelRegistry = createProviderRegistry( { provider: { imageModel: id => { expect(id).toEqual('model'); return model; }, languageModel: () => null as any, textEmbeddingModel: () => null as any, }, }, { separator: '|' }, ); expect(modelRegistry.imageModel('provider|model')).toEqual(model); }); }); describe('transcriptionModel', () => { it('should return transcription model from provider', () => { const model = new MockTranscriptionModelV2(); const modelRegistry = createProviderRegistry({ provider: { transcriptionModel: id => { expect(id).toEqual('model'); return model; }, languageModel: () => null as any, textEmbeddingModel: () => null as any, imageModel: () => null as any, }, }); expect(modelRegistry.transcriptionModel('provider:model')).toEqual(model); }); it('should throw NoSuchProviderError if provider does not exist', () => { const registry = createProviderRegistry({}); // @ts-expect-error - should not accept arbitrary strings expect(() => registry.transcriptionModel('provider:model')).toThrowError( NoSuchProviderError, ); }); it('should throw NoSuchModelError if provider does not return a model', () => { const registry = createProviderRegistry({ provider: { transcriptionModel: () => null as any, languageModel: () => null as any, textEmbeddingModel: () => null as any, imageModel: () => null as any, }, }); expect(() => registry.transcriptionModel('provider:model')).toThrowError( NoSuchModelError, ); }); it("should throw NoSuchModelError if model id doesn't contain a colon", () => { const registry = createProviderRegistry({}); // @ts-expect-error - should not accept arbitrary strings expect(() => registry.transcriptionModel('model')).toThrowError( NoSuchModelError, ); }); }); describe('speechModel', () => { it('should return speech model from provider', () => { const model = new MockSpeechModelV2(); const modelRegistry = createProviderRegistry({ provider: { speechModel: id => { expect(id).toEqual('model'); return model; }, languageModel: () => null as any, textEmbeddingModel: () => null as any, imageModel: () => null as any, }, }); expect(modelRegistry.speechModel('provider:model')).toEqual(model); }); it('should throw NoSuchProviderError if provider does not exist', () => { const registry = createProviderRegistry({}); // @ts-expect-error - should not accept arbitrary strings expect(() => registry.speechModel('provider:model')).toThrowError( NoSuchProviderError, ); }); it('should throw NoSuchModelError if provider does not return a model', () => { const registry = createProviderRegistry({ provider: { speechModel: () => null as any, languageModel: () => null as any, textEmbeddingModel: () => null as any, imageModel: () => null as any, }, }); expect(() => registry.speechModel('provider:model')).toThrowError( NoSuchModelError, ); }); it("should throw NoSuchModelError if model id doesn't contain a colon", () => { const registry = createProviderRegistry({}); // @ts-expect-error - should not accept arbitrary strings expect(() => registry.speechModel('model')).toThrowError(NoSuchModelError); }); }); describe('middleware functionality', () => { it('should wrap all language models accessed through the provider registry', () => { const model1 = new MockLanguageModelV2({ modelId: 'model-1' }); const model2 = new MockLanguageModelV2({ modelId: 'model-2' }); const model3 = new MockLanguageModelV2({ modelId: 'model-3' }); const provider1 = new MockProviderV2({ languageModels: { 'model-1': model1, 'model-2': model2, }, }); const provider2 = new MockProviderV2({ languageModels: { 'model-3': model3, }, }); const overrideModelId = vi .fn() .mockImplementation(({ model }) => `override-${model.modelId}`); const registry = createProviderRegistry( { provider1, provider2, }, { languageModelMiddleware: { middlewareVersion: 'v2', overrideModelId, }, }, ); expect(registry.languageModel('provider1:model-1').modelId).toBe( 'override-model-1', ); expect(registry.languageModel('provider1:model-2').modelId).toBe( 'override-model-2', ); expect(registry.languageModel('provider2:model-3').modelId).toBe( 'override-model-3', ); expect(overrideModelId).toHaveBeenCalledTimes(3); expect(overrideModelId).toHaveBeenCalledWith({ model: model1 }); expect(overrideModelId).toHaveBeenCalledWith({ model: model2 }); expect(overrideModelId).toHaveBeenCalledWith({ model: model3 }); }); }); --- File: /ai/packages/ai/src/registry/provider-registry.ts --- import { EmbeddingModelV2, ImageModelV2, LanguageModelV2, LanguageModelV2Middleware, NoSuchModelError, ProviderV2, SpeechModelV2, TranscriptionModelV2, } from '@ai-sdk/provider'; import { NoSuchProviderError } from './no-such-provider-error'; import { wrapLanguageModel } from '../middleware/wrap-language-model'; type ExtractLiteralUnion<T> = T extends string ? string extends T ? never : T : never; export interface ProviderRegistryProvider< PROVIDERS extends Record<string, ProviderV2> = Record<string, ProviderV2>, SEPARATOR extends string = ':', > { languageModel<KEY extends keyof PROVIDERS>( id: KEY extends string ? `${KEY & string}${SEPARATOR}${ExtractLiteralUnion<Parameters<NonNullable<PROVIDERS[KEY]['languageModel']>>[0]>}` : never, ): LanguageModelV2; languageModel<KEY extends keyof PROVIDERS>( id: KEY extends string ? `${KEY & string}${SEPARATOR}${string}` : never, ): LanguageModelV2; textEmbeddingModel<KEY extends keyof PROVIDERS>( id: KEY extends string ? `${KEY & string}${SEPARATOR}${ExtractLiteralUnion<Parameters<NonNullable<PROVIDERS[KEY]['textEmbeddingModel']>>[0]>}` : never, ): EmbeddingModelV2<string>; textEmbeddingModel<KEY extends keyof PROVIDERS>( id: KEY extends string ? `${KEY & string}${SEPARATOR}${string}` : never, ): EmbeddingModelV2<string>; imageModel<KEY extends keyof PROVIDERS>( id: KEY extends string ? `${KEY & string}${SEPARATOR}${ExtractLiteralUnion<Parameters<NonNullable<PROVIDERS[KEY]['imageModel']>>[0]>}` : never, ): ImageModelV2; imageModel<KEY extends keyof PROVIDERS>( id: KEY extends string ? `${KEY & string}${SEPARATOR}${string}` : never, ): ImageModelV2; transcriptionModel<KEY extends keyof PROVIDERS>( id: KEY extends string ? `${KEY & string}${SEPARATOR}${ExtractLiteralUnion<Parameters<NonNullable<PROVIDERS[KEY]['transcriptionModel']>>[0]>}` : never, ): TranscriptionModelV2; transcriptionModel<KEY extends keyof PROVIDERS>( id: KEY extends string ? `${KEY & string}${SEPARATOR}${string}` : never, ): TranscriptionModelV2; speechModel<KEY extends keyof PROVIDERS>( id: KEY extends string ? `${KEY & string}${SEPARATOR}${ExtractLiteralUnion<Parameters<NonNullable<PROVIDERS[KEY]['speechModel']>>[0]>}` : never, ): SpeechModelV2; speechModel<KEY extends keyof PROVIDERS>( id: KEY extends string ? `${KEY & string}${SEPARATOR}${string}` : never, ): SpeechModelV2; } /** * Creates a registry for the given providers with optional middleware functionality. * This function allows you to register multiple providers and optionally apply middleware * to all language models from the registry, enabling you to transform parameters, wrap generate * operations, and wrap stream operations for every language model accessed through the registry. * * @param providers - A record of provider instances to be registered in the registry. * @param options - Configuration options for the provider registry. * @param options.separator - The separator used between provider ID and model ID in the combined identifier. Defaults to ':'. * @param options.languageModelMiddleware - Optional middleware to be applied to all language models from the registry. When multiple middlewares are provided, the first middleware will transform the input first, and the last middleware will be wrapped directly around the model. * @returns A new ProviderRegistryProvider instance that provides access to all registered providers with optional middleware applied to language models. */ export function createProviderRegistry< PROVIDERS extends Record<string, ProviderV2>, SEPARATOR extends string = ':', >( providers: PROVIDERS, { separator = ':' as SEPARATOR, languageModelMiddleware, }: { separator?: SEPARATOR; languageModelMiddleware?: | LanguageModelV2Middleware | LanguageModelV2Middleware[]; } = {}, ): ProviderRegistryProvider<PROVIDERS, SEPARATOR> { const registry = new DefaultProviderRegistry<PROVIDERS, SEPARATOR>({ separator, languageModelMiddleware, }); for (const [id, provider] of Object.entries(providers)) { registry.registerProvider({ id, provider } as { id: keyof PROVIDERS; provider: PROVIDERS[keyof PROVIDERS]; }); } return registry; } /** * @deprecated Use `createProviderRegistry` instead. */ export const experimental_createProviderRegistry = createProviderRegistry; class DefaultProviderRegistry< PROVIDERS extends Record<string, ProviderV2>, SEPARATOR extends string, > implements ProviderRegistryProvider<PROVIDERS, SEPARATOR> { private providers: PROVIDERS = {} as PROVIDERS; private separator: SEPARATOR; private languageModelMiddleware?: | LanguageModelV2Middleware | LanguageModelV2Middleware[]; constructor({ separator, languageModelMiddleware, }: { separator: SEPARATOR; languageModelMiddleware?: | LanguageModelV2Middleware | LanguageModelV2Middleware[]; }) { this.separator = separator; this.languageModelMiddleware = languageModelMiddleware; } registerProvider<K extends keyof PROVIDERS>({ id, provider, }: { id: K; provider: PROVIDERS[K]; }): void { this.providers[id] = provider; } private getProvider( id: string, modelType: | 'languageModel' | 'textEmbeddingModel' | 'imageModel' | 'transcriptionModel' | 'speechModel', ): ProviderV2 { const provider = this.providers[id as keyof PROVIDERS]; if (provider == null) { throw new NoSuchProviderError({ modelId: id, modelType, providerId: id, availableProviders: Object.keys(this.providers), }); } return provider; } private splitId( id: string, modelType: | 'languageModel' | 'textEmbeddingModel' | 'imageModel' | 'transcriptionModel' | 'speechModel', ): [string, string] { const index = id.indexOf(this.separator); if (index === -1) { throw new NoSuchModelError({ modelId: id, modelType, message: `Invalid ${modelType} id for registry: ${id} ` + `(must be in the format "providerId${this.separator}modelId")`, }); } return [id.slice(0, index), id.slice(index + this.separator.length)]; } languageModel<KEY extends keyof PROVIDERS>( id: `${KEY & string}${SEPARATOR}${string}`, ): LanguageModelV2 { const [providerId, modelId] = this.splitId(id, 'languageModel'); let model = this.getProvider(providerId, 'languageModel').languageModel?.( modelId, ); if (model == null) { throw new NoSuchModelError({ modelId: id, modelType: 'languageModel' }); } if (this.languageModelMiddleware != null) { model = wrapLanguageModel({ model, middleware: this.languageModelMiddleware, }); } return model; } textEmbeddingModel<KEY extends keyof PROVIDERS>( id: `${KEY & string}${SEPARATOR}${string}`, ): EmbeddingModelV2<string> { const [providerId, modelId] = this.splitId(id, 'textEmbeddingModel'); const provider = this.getProvider(providerId, 'textEmbeddingModel'); const model = provider.textEmbeddingModel?.(modelId); if (model == null) { throw new NoSuchModelError({ modelId: id, modelType: 'textEmbeddingModel', }); } return model; } imageModel<KEY extends keyof PROVIDERS>( id: `${KEY & string}${SEPARATOR}${string}`, ): ImageModelV2 { const [providerId, modelId] = this.splitId(id, 'imageModel'); const provider = this.getProvider(providerId, 'imageModel'); const model = provider.imageModel?.(modelId); if (model == null) { throw new NoSuchModelError({ modelId: id, modelType: 'imageModel' }); } return model; } transcriptionModel<KEY extends keyof PROVIDERS>( id: `${KEY & string}${SEPARATOR}${string}`, ): TranscriptionModelV2 { const [providerId, modelId] = this.splitId(id, 'transcriptionModel'); const provider = this.getProvider(providerId, 'transcriptionModel'); const model = provider.transcriptionModel?.(modelId); if (model == null) { throw new NoSuchModelError({ modelId: id, modelType: 'transcriptionModel', }); } return model; } speechModel<KEY extends keyof PROVIDERS>( id: `${KEY & string}${SEPARATOR}${string}`, ): SpeechModelV2 { const [providerId, modelId] = this.splitId(id, 'speechModel'); const provider = this.getProvider(providerId, 'speechModel'); const model = provider.speechModel?.(modelId); if (model == null) { throw new NoSuchModelError({ modelId: id, modelType: 'speechModel' }); } return model; } } --- File: /ai/packages/ai/src/telemetry/assemble-operation-name.ts --- import { TelemetrySettings } from './telemetry-settings'; export function assembleOperationName({ operationId, telemetry, }: { operationId: string; telemetry?: TelemetrySettings; }) { return { // standardized operation and resource name: 'operation.name': `${operationId}${ telemetry?.functionId != null ? ` ${telemetry.functionId}` : '' }`, 'resource.name': telemetry?.functionId, // detailed, AI SDK specific data: 'ai.operationId': operationId, 'ai.telemetry.functionId': telemetry?.functionId, }; } --- File: /ai/packages/ai/src/telemetry/get-base-telemetry-attributes.ts --- import { Attributes } from '@opentelemetry/api'; import { CallSettings } from '../prompt/call-settings'; import { TelemetrySettings } from './telemetry-settings'; export function getBaseTelemetryAttributes({ model, settings, telemetry, headers, }: { model: { modelId: string; provider: string }; settings: Omit<CallSettings, 'abortSignal' | 'headers' | 'temperature'>; telemetry: TelemetrySettings | undefined; headers: Record<string, string | undefined> | undefined; }): Attributes { return { 'ai.model.provider': model.provider, 'ai.model.id': model.modelId, // settings: ...Object.entries(settings).reduce((attributes, [key, value]) => { attributes[`ai.settings.${key}`] = value; return attributes; }, {} as Attributes), // add metadata as attributes: ...Object.entries(telemetry?.metadata ?? {}).reduce( (attributes, [key, value]) => { attributes[`ai.telemetry.metadata.${key}`] = value; return attributes; }, {} as Attributes, ), // request headers ...Object.entries(headers ?? {}).reduce((attributes, [key, value]) => { if (value !== undefined) { attributes[`ai.request.headers.${key}`] = value; } return attributes; }, {} as Attributes), }; } --- File: /ai/packages/ai/src/telemetry/get-tracer.ts --- import { Tracer, trace } from '@opentelemetry/api'; import { noopTracer } from './noop-tracer'; export function getTracer({ isEnabled = false, tracer, }: { isEnabled?: boolean; tracer?: Tracer; } = {}): Tracer { if (!isEnabled) { return noopTracer; } if (tracer) { return tracer; } return trace.getTracer('ai'); } --- File: /ai/packages/ai/src/telemetry/noop-tracer.ts --- import { Span, SpanContext, Tracer } from '@opentelemetry/api'; /** * Tracer implementation that does nothing (null object). */ export const noopTracer: Tracer = { startSpan(): Span { return noopSpan; }, startActiveSpan<F extends (span: Span) => unknown>( name: unknown, arg1: unknown, arg2?: unknown, arg3?: F, ): ReturnType<any> { if (typeof arg1 === 'function') { return arg1(noopSpan); } if (typeof arg2 === 'function') { return arg2(noopSpan); } if (typeof arg3 === 'function') { return arg3(noopSpan); } }, }; const noopSpan: Span = { spanContext() { return noopSpanContext; }, setAttribute() { return this; }, setAttributes() { return this; }, addEvent() { return this; }, addLink() { return this; }, addLinks() { return this; }, setStatus() { return this; }, updateName() { return this; }, end() { return this; }, isRecording() { return false; }, recordException() { return this; }, }; const noopSpanContext: SpanContext = { traceId: '', spanId: '', traceFlags: 0, }; --- File: /ai/packages/ai/src/telemetry/record-span.ts --- import { Attributes, Span, Tracer, SpanStatusCode } from '@opentelemetry/api'; export function recordSpan<T>({ name, tracer, attributes, fn, endWhenDone = true, }: { name: string; tracer: Tracer; attributes: Attributes; fn: (span: Span) => Promise<T>; endWhenDone?: boolean; }) { return tracer.startActiveSpan(name, { attributes }, async span => { try { const result = await fn(span); if (endWhenDone) { span.end(); } return result; } catch (error) { try { recordErrorOnSpan(span, error); } finally { // always stop the span when there is an error: span.end(); } throw error; } }); } /** * Record an error on a span. If the error is an instance of Error, an exception event will be recorded on the span, otherwise * the span will be set to an error status. * * @param span - The span to record the error on. * @param error - The error to record on the span. */ export function recordErrorOnSpan(span: Span, error: unknown) { if (error instanceof Error) { span.recordException({ name: error.name, message: error.message, stack: error.stack, }); span.setStatus({ code: SpanStatusCode.ERROR, message: error.message, }); } else { span.setStatus({ code: SpanStatusCode.ERROR }); } } --- File: /ai/packages/ai/src/telemetry/select-telemetry-attributes.ts --- import type { Attributes, AttributeValue } from '@opentelemetry/api'; import type { TelemetrySettings } from './telemetry-settings'; export function selectTelemetryAttributes({ telemetry, attributes, }: { telemetry?: TelemetrySettings; attributes: { [attributeKey: string]: | AttributeValue | { input: () => AttributeValue | undefined } | { output: () => AttributeValue | undefined } | undefined; }; }): Attributes { // when telemetry is disabled, return an empty object to avoid serialization overhead: if (telemetry?.isEnabled !== true) { return {}; } return Object.entries(attributes).reduce((attributes, [key, value]) => { if (value == null) { return attributes; } // input value, check if it should be recorded: if ( typeof value === 'object' && 'input' in value && typeof value.input === 'function' ) { // default to true: if (telemetry?.recordInputs === false) { return attributes; } const result = value.input(); return result == null ? attributes : { ...attributes, [key]: result }; } // output value, check if it should be recorded: if ( typeof value === 'object' && 'output' in value && typeof value.output === 'function' ) { // default to true: if (telemetry?.recordOutputs === false) { return attributes; } const result = value.output(); return result == null ? attributes : { ...attributes, [key]: result }; } // value is an attribute value already: return { ...attributes, [key]: value }; }, {}); } --- File: /ai/packages/ai/src/telemetry/select-temetry-attributes.test.ts --- import { selectTelemetryAttributes } from './select-telemetry-attributes'; it('should return an empty object when telemetry is disabled', () => { const result = selectTelemetryAttributes({ telemetry: { isEnabled: false }, attributes: { key: 'value' }, }); expect(result).toEqual({}); }); it('should return an empty object when telemetry enablement is undefined', () => { const result = selectTelemetryAttributes({ telemetry: { isEnabled: undefined }, attributes: { key: 'value' }, }); expect(result).toEqual({}); }); it('should return attributes with simple values', () => { const result = selectTelemetryAttributes({ telemetry: { isEnabled: true }, attributes: { string: 'value', number: 42, boolean: true }, }); expect(result).toEqual({ string: 'value', number: 42, boolean: true }); }); it('should handle input functions when recordInputs is true', () => { const result = selectTelemetryAttributes({ telemetry: { isEnabled: true, recordInputs: true }, attributes: { input: { input: () => 'input value' }, other: 'other value', }, }); expect(result).toEqual({ input: 'input value', other: 'other value' }); }); it('should not include input functions when recordInputs is false', () => { const result = selectTelemetryAttributes({ telemetry: { isEnabled: true, recordInputs: false }, attributes: { input: { input: () => 'input value' }, other: 'other value', }, }); expect(result).toEqual({ other: 'other value' }); }); it('should handle output functions when recordOutputs is true', () => { const result = selectTelemetryAttributes({ telemetry: { isEnabled: true, recordOutputs: true }, attributes: { output: { output: () => 'output value' }, other: 'other value', }, }); expect(result).toEqual({ output: 'output value', other: 'other value' }); }); it('should not include output functions when recordOutputs is false', () => { const result = selectTelemetryAttributes({ telemetry: { isEnabled: true, recordOutputs: false }, attributes: { output: { output: () => 'output value' }, other: 'other value', }, }); expect(result).toEqual({ other: 'other value' }); }); it('should ignore undefined values', () => { const result = selectTelemetryAttributes({ telemetry: { isEnabled: true }, attributes: { defined: 'value', undefined: undefined, }, }); expect(result).toEqual({ defined: 'value' }); }); it('should ignore input and output functions that return undefined', () => { const result = selectTelemetryAttributes({ telemetry: { isEnabled: true }, attributes: { input: { input: () => undefined }, output: { output: () => undefined }, other: 'value', }, }); expect(result).toEqual({ other: 'value' }); }); it('should handle mixed attribute types correctly', () => { const result = selectTelemetryAttributes({ telemetry: { isEnabled: true }, attributes: { simple: 'value', input: { input: () => 'input value' }, output: { output: () => 'output value' }, undefined: undefined, // Invalid null input null: null as any, input_null: { input: () => null as any }, }, }); expect(result).toEqual({ simple: 'value', input: 'input value', output: 'output value', }); }); --- File: /ai/packages/ai/src/telemetry/stringify-for-telemetry.test.ts --- import { stringifyForTelemetry } from './stringify-for-telemetry'; import { LanguageModelV2Prompt } from '@ai-sdk/provider'; describe('stringifyForTelemetry', () => { it('should stringify a prompt with text parts', () => { const prompt: LanguageModelV2Prompt = [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: [{ type: 'text', text: 'Hello!' }], }, ]; const result = stringifyForTelemetry(prompt); expect(result).toMatchInlineSnapshot( `"[{"role":"system","content":"You are a helpful assistant."},{"role":"user","content":[{"type":"text","text":"Hello!"}]}]"`, ); }); it('should convert Uint8Array images to base64 strings', () => { const result = stringifyForTelemetry([ { role: 'user', content: [ { type: 'file', data: new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0xff, 0xff]), mediaType: 'image/png', }, ], }, ]); expect(result).toMatchInlineSnapshot( `"[{"role":"user","content":[{"type":"file","data":"iVBOR///","mediaType":"image/png"}]}]"`, ); }); it('should preserve the file name and provider options', () => { const result = stringifyForTelemetry([ { role: 'user', content: [ { type: 'file', filename: 'image.png', data: new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0xff, 0xff]), mediaType: 'image/png', providerOptions: { anthropic: { key: 'value', }, }, }, ], }, ]); expect(result).toMatchInlineSnapshot( `"[{"role":"user","content":[{"type":"file","filename":"image.png","data":"iVBOR///","mediaType":"image/png","providerOptions":{"anthropic":{"key":"value"}}}]}]"`, ); }); it('should keep URL images as is', () => { const result = stringifyForTelemetry([ { role: 'user', content: [ { type: 'text', text: 'Check this image:' }, { type: 'file', data: new URL('https://example.com/image.jpg'), mediaType: 'image/jpeg', }, ], }, ]); expect(result).toMatchInlineSnapshot( `"[{"role":"user","content":[{"type":"text","text":"Check this image:"},{"type":"file","data":"https://example.com/image.jpg","mediaType":"image/jpeg"}]}]"`, ); }); it('should handle a mixed prompt with various content types', () => { const result = stringifyForTelemetry([ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: [ { type: 'file', data: new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0xff, 0xff]), mediaType: 'image/png', }, { type: 'file', data: new URL('https://example.com/image.jpg'), mediaType: 'image/jpeg', }, ], }, { role: 'assistant', content: [{ type: 'text', text: 'I see the images!' }], }, ]); expect(result).toMatchInlineSnapshot( `"[{"role":"system","content":"You are a helpful assistant."},{"role":"user","content":[{"type":"file","data":"iVBOR///","mediaType":"image/png"},{"type":"file","data":"https://example.com/image.jpg","mediaType":"image/jpeg"}]},{"role":"assistant","content":[{"type":"text","text":"I see the images!"}]}]"`, ); }); }); --- File: /ai/packages/ai/src/telemetry/stringify-for-telemetry.ts --- import { LanguageModelV2Message, LanguageModelV2Prompt, } from '@ai-sdk/provider'; import { convertDataContentToBase64String } from '../prompt/data-content'; /** * Helper utility to serialize prompt content for OpenTelemetry tracing. * It is initially created because normalized LanguageModelV1Prompt carries * images as Uint8Arrays, on which JSON.stringify acts weirdly, converting * them to objects with stringified indices as keys, e.g. {"0": 42, "1": 69 }. */ export function stringifyForTelemetry(prompt: LanguageModelV2Prompt): string { return JSON.stringify( prompt.map((message: LanguageModelV2Message) => ({ ...message, content: typeof message.content === 'string' ? message.content : message.content.map(part => part.type === 'file' ? { ...part, data: part.data instanceof Uint8Array ? convertDataContentToBase64String(part.data) : part.data, } : part, ), })), ); } --- File: /ai/packages/ai/src/telemetry/telemetry-settings.ts --- import { AttributeValue, Tracer } from '@opentelemetry/api'; /** * Telemetry configuration. */ // This is meant to be both flexible for custom app requirements (metadata) // and extensible for standardization (example: functionId, more to come). export type TelemetrySettings = { /** * Enable or disable telemetry. Disabled by default while experimental. */ isEnabled?: boolean; /** * Enable or disable input recording. Enabled by default. * * You might want to disable input recording to avoid recording sensitive * information, to reduce data transfers, or to increase performance. */ recordInputs?: boolean; /** * Enable or disable output recording. Enabled by default. * * You might want to disable output recording to avoid recording sensitive * information, to reduce data transfers, or to increase performance. */ recordOutputs?: boolean; /** * Identifier for this function. Used to group telemetry data by function. */ functionId?: string; /** * Additional information to include in the telemetry data. */ metadata?: Record<string, AttributeValue>; /** * A custom tracer to use for the telemetry data. */ tracer?: Tracer; }; --- File: /ai/packages/ai/src/test/mock-embedding-model-v2.ts --- import { EmbeddingModelV2 } from '@ai-sdk/provider'; import { Embedding } from '../types'; import { EmbeddingModelUsage } from '../types/usage'; import { notImplemented } from './not-implemented'; export class MockEmbeddingModelV2<VALUE> implements EmbeddingModelV2<VALUE> { readonly specificationVersion = 'v2'; readonly provider: EmbeddingModelV2<VALUE>['provider']; readonly modelId: EmbeddingModelV2<VALUE>['modelId']; readonly maxEmbeddingsPerCall: EmbeddingModelV2<VALUE>['maxEmbeddingsPerCall']; readonly supportsParallelCalls: EmbeddingModelV2<VALUE>['supportsParallelCalls']; doEmbed: EmbeddingModelV2<VALUE>['doEmbed']; constructor({ provider = 'mock-provider', modelId = 'mock-model-id', maxEmbeddingsPerCall = 1, supportsParallelCalls = false, doEmbed = notImplemented, }: { provider?: EmbeddingModelV2<VALUE>['provider']; modelId?: EmbeddingModelV2<VALUE>['modelId']; maxEmbeddingsPerCall?: | EmbeddingModelV2<VALUE>['maxEmbeddingsPerCall'] | null; supportsParallelCalls?: EmbeddingModelV2<VALUE>['supportsParallelCalls']; doEmbed?: EmbeddingModelV2<VALUE>['doEmbed']; } = {}) { this.provider = provider; this.modelId = modelId; this.maxEmbeddingsPerCall = maxEmbeddingsPerCall ?? undefined; this.supportsParallelCalls = supportsParallelCalls; this.doEmbed = doEmbed; } } export function mockEmbed<VALUE>( expectedValues: Array<VALUE>, embeddings: Array<Embedding>, usage?: EmbeddingModelUsage, response: Awaited< ReturnType<EmbeddingModelV2<VALUE>['doEmbed']> >['response'] = { headers: {}, body: {} }, providerMetadata?: Awaited< ReturnType<EmbeddingModelV2<VALUE>['doEmbed']> >['providerMetadata'], ): EmbeddingModelV2<VALUE>['doEmbed'] { return async ({ values }) => { assert.deepStrictEqual(expectedValues, values); return { embeddings, usage, response, providerMetadata }; }; } --- File: /ai/packages/ai/src/test/mock-image-model-v2.ts --- import { ImageModelV2 } from '@ai-sdk/provider'; import { notImplemented } from './not-implemented'; export class MockImageModelV2 implements ImageModelV2 { readonly specificationVersion = 'v2'; readonly provider: ImageModelV2['provider']; readonly modelId: ImageModelV2['modelId']; readonly maxImagesPerCall: ImageModelV2['maxImagesPerCall']; doGenerate: ImageModelV2['doGenerate']; constructor({ provider = 'mock-provider', modelId = 'mock-model-id', maxImagesPerCall = 1, doGenerate = notImplemented, }: { provider?: ImageModelV2['provider']; modelId?: ImageModelV2['modelId']; maxImagesPerCall?: ImageModelV2['maxImagesPerCall']; doGenerate?: ImageModelV2['doGenerate']; } = {}) { this.provider = provider; this.modelId = modelId; this.maxImagesPerCall = maxImagesPerCall; this.doGenerate = doGenerate; } } --- File: /ai/packages/ai/src/test/mock-language-model-v2.ts --- import { LanguageModelV2 } from '@ai-sdk/provider'; import { notImplemented } from './not-implemented'; export class MockLanguageModelV2 implements LanguageModelV2 { readonly specificationVersion = 'v2'; private _supportedUrls: () => LanguageModelV2['supportedUrls']; readonly provider: LanguageModelV2['provider']; readonly modelId: LanguageModelV2['modelId']; doGenerate: LanguageModelV2['doGenerate']; doStream: LanguageModelV2['doStream']; doGenerateCalls: Parameters<LanguageModelV2['doGenerate']>[0][] = []; doStreamCalls: Parameters<LanguageModelV2['doStream']>[0][] = []; constructor({ provider = 'mock-provider', modelId = 'mock-model-id', supportedUrls = {}, doGenerate = notImplemented, doStream = notImplemented, }: { provider?: LanguageModelV2['provider']; modelId?: LanguageModelV2['modelId']; supportedUrls?: | LanguageModelV2['supportedUrls'] | (() => LanguageModelV2['supportedUrls']); doGenerate?: | LanguageModelV2['doGenerate'] | Awaited<ReturnType<LanguageModelV2['doGenerate']>> | Awaited<ReturnType<LanguageModelV2['doGenerate']>>[]; doStream?: | LanguageModelV2['doStream'] | Awaited<ReturnType<LanguageModelV2['doStream']>> | Awaited<ReturnType<LanguageModelV2['doStream']>>[]; } = {}) { this.provider = provider; this.modelId = modelId; this.doGenerate = async options => { this.doGenerateCalls.push(options); if (typeof doGenerate === 'function') { return doGenerate(options); } else if (Array.isArray(doGenerate)) { return doGenerate[this.doGenerateCalls.length]; } else { return doGenerate; } }; this.doStream = async options => { this.doStreamCalls.push(options); if (typeof doStream === 'function') { return doStream(options); } else if (Array.isArray(doStream)) { return doStream[this.doStreamCalls.length]; } else { return doStream; } }; this._supportedUrls = typeof supportedUrls === 'function' ? supportedUrls : async () => supportedUrls; } get supportedUrls() { return this._supportedUrls(); } } --- File: /ai/packages/ai/src/test/mock-provider-v2.ts --- import { EmbeddingModelV2, ImageModelV2, LanguageModelV2, NoSuchModelError, ProviderV2, SpeechModelV2, TranscriptionModelV2, } from '@ai-sdk/provider'; export class MockProviderV2 implements ProviderV2 { languageModel: ProviderV2['languageModel']; textEmbeddingModel: ProviderV2['textEmbeddingModel']; imageModel: ProviderV2['imageModel']; transcriptionModel: ProviderV2['transcriptionModel']; speechModel: ProviderV2['speechModel']; constructor({ languageModels, embeddingModels, imageModels, transcriptionModels, speechModels, }: { languageModels?: Record<string, LanguageModelV2>; embeddingModels?: Record<string, EmbeddingModelV2<string>>; imageModels?: Record<string, ImageModelV2>; transcriptionModels?: Record<string, TranscriptionModelV2>; speechModels?: Record<string, SpeechModelV2>; } = {}) { this.languageModel = (modelId: string) => { if (!languageModels?.[modelId]) { throw new NoSuchModelError({ modelId, modelType: 'languageModel' }); } return languageModels[modelId]; }; this.textEmbeddingModel = (modelId: string) => { if (!embeddingModels?.[modelId]) { throw new NoSuchModelError({ modelId, modelType: 'textEmbeddingModel', }); } return embeddingModels[modelId]; }; this.imageModel = (modelId: string) => { if (!imageModels?.[modelId]) { throw new NoSuchModelError({ modelId, modelType: 'imageModel' }); } return imageModels[modelId]; }; this.transcriptionModel = (modelId: string) => { if (!transcriptionModels?.[modelId]) { throw new NoSuchModelError({ modelId, modelType: 'transcriptionModel', }); } return transcriptionModels[modelId]; }; this.speechModel = (modelId: string) => { if (!speechModels?.[modelId]) { throw new NoSuchModelError({ modelId, modelType: 'speechModel' }); } return speechModels[modelId]; }; } } --- File: /ai/packages/ai/src/test/mock-server-response.ts --- import { ServerResponse } from 'node:http'; class MockServerResponse { writtenChunks: any[] = []; headers = {}; statusCode = 0; statusMessage = ''; ended = false; write(chunk: any): void { this.writtenChunks.push(chunk); } end(): void { // You might want to mark the response as ended to simulate the real behavior this.ended = true; } writeHead( statusCode: number, statusMessage: string, headers: Record<string, string>, ): void { this.statusCode = statusCode; this.statusMessage = statusMessage; this.headers = headers; } get body() { // Combine all written chunks into a single string return this.writtenChunks.join(''); } /** * Get the decoded chunks as strings. */ getDecodedChunks() { const decoder = new TextDecoder(); return this.writtenChunks.map(chunk => decoder.decode(chunk)); } /** * Wait for the stream to finish writing to the mock response. */ async waitForEnd() { await new Promise(resolve => { const checkIfEnded = () => { if (this.ended) { resolve(undefined); } else { setImmediate(checkIfEnded); } }; checkIfEnded(); }); } } export function createMockServerResponse(): ServerResponse & MockServerResponse { return new MockServerResponse() as ServerResponse & MockServerResponse; } --- File: /ai/packages/ai/src/test/mock-speech-model-v2.ts --- import { SpeechModelV2 } from '@ai-sdk/provider'; import { notImplemented } from './not-implemented'; export class MockSpeechModelV2 implements SpeechModelV2 { readonly specificationVersion = 'v2'; readonly provider: SpeechModelV2['provider']; readonly modelId: SpeechModelV2['modelId']; doGenerate: SpeechModelV2['doGenerate']; constructor({ provider = 'mock-provider', modelId = 'mock-model-id', doGenerate = notImplemented, }: { provider?: SpeechModelV2['provider']; modelId?: SpeechModelV2['modelId']; doGenerate?: SpeechModelV2['doGenerate']; } = {}) { this.provider = provider; this.modelId = modelId; this.doGenerate = doGenerate; } } --- File: /ai/packages/ai/src/test/mock-tracer.ts --- import { AttributeValue, Attributes, Context, Exception, Span, SpanContext, SpanOptions, SpanStatus, TimeInput, Tracer, } from '@opentelemetry/api'; export class MockTracer implements Tracer { spans: MockSpan[] = []; get jsonSpans() { return this.spans.map(span => ({ name: span.name, attributes: span.attributes, events: span.events, ...(span.status && { status: span.status }), })); } startSpan(name: string, options?: SpanOptions, context?: Context): Span { const span = new MockSpan({ name, options, context, }); this.spans.push(span); return span; } startActiveSpan<F extends (span: Span) => unknown>( name: string, arg1: unknown, arg2?: unknown, arg3?: F, ): ReturnType<any> { if (typeof arg1 === 'function') { const span = new MockSpan({ name, }); this.spans.push(span); return arg1(span); } if (typeof arg2 === 'function') { const span = new MockSpan({ name, options: arg1 as SpanOptions, }); this.spans.push(span); return arg2(span); } if (typeof arg3 === 'function') { const span = new MockSpan({ name, options: arg1 as SpanOptions, context: arg2 as Context, }); this.spans.push(span); return arg3(span); } } } class MockSpan implements Span { name: string; context?: Context; options?: SpanOptions; attributes: Attributes; events: Array<{ name: string; attributes: Attributes | undefined; time?: [number, number]; }> = []; status?: SpanStatus; readonly _spanContext: SpanContext = new MockSpanContext(); constructor({ name, options, context, }: { name: string; options?: SpanOptions; context?: Context; }) { this.name = name; this.context = context; this.options = options; this.attributes = options?.attributes ?? {}; } spanContext(): SpanContext { return this._spanContext; } setAttribute(key: string, value: AttributeValue): this { this.attributes = { ...this.attributes, [key]: value }; return this; } setAttributes(attributes: Attributes): this { this.attributes = { ...this.attributes, ...attributes }; return this; } addEvent(name: string, attributes?: Attributes): this { this.events.push({ name, attributes }); return this; } addLink() { return this; } addLinks() { return this; } setStatus(status: SpanStatus) { this.status = status; return this; } updateName() { return this; } end() { return this; } isRecording() { return false; } recordException(exception: Exception, time?: TimeInput) { const error = typeof exception === 'string' ? new Error(exception) : exception; this.events.push({ name: 'exception', attributes: { 'exception.type': error.constructor?.name || 'Error', 'exception.name': error.name || 'Error', 'exception.message': error.message || '', 'exception.stack': error.stack || '', }, time: Array.isArray(time) ? time : [0, 0], }); } } class MockSpanContext implements SpanContext { traceId = 'test-trace-id'; spanId = 'test-span-id'; traceFlags = 0; } --- File: /ai/packages/ai/src/test/mock-transcription-model-v2.ts --- import { TranscriptionModelV2 } from '@ai-sdk/provider'; import { notImplemented } from './not-implemented'; export class MockTranscriptionModelV2 implements TranscriptionModelV2 { readonly specificationVersion = 'v2'; readonly provider: TranscriptionModelV2['provider']; readonly modelId: TranscriptionModelV2['modelId']; doGenerate: TranscriptionModelV2['doGenerate']; constructor({ provider = 'mock-provider', modelId = 'mock-model-id', doGenerate = notImplemented, }: { provider?: TranscriptionModelV2['provider']; modelId?: TranscriptionModelV2['modelId']; doGenerate?: TranscriptionModelV2['doGenerate']; } = {}) { this.provider = provider; this.modelId = modelId; this.doGenerate = doGenerate; } } --- File: /ai/packages/ai/src/test/mock-values.ts --- export function mockValues<T>(...values: T[]): () => T { let counter = 0; return () => values[counter++] ?? values[values.length - 1]; } --- File: /ai/packages/ai/src/test/not-implemented.ts --- export function notImplemented(): never { throw new Error('Not implemented'); } --- File: /ai/packages/ai/src/text-stream/create-text-stream-response.test.ts --- import { convertArrayToReadableStream, convertReadableStreamToArray, } from '@ai-sdk/provider-utils/test'; import { createTextStreamResponse } from './create-text-stream-response'; describe('createTextStreamResponse', () => { it('should create a Response with correct headers and encoded stream', async () => { const response = createTextStreamResponse({ status: 200, statusText: 'OK', headers: { 'Custom-Header': 'test', }, textStream: convertArrayToReadableStream(['test-data']), }); // Verify response properties expect(response).toBeInstanceOf(Response); expect(response.status).toBe(200); expect(response.statusText).toBe('OK'); // Verify headers expect(response.headers.get('Content-Type')).toBe( 'text/plain; charset=utf-8', ); expect(response.headers.get('Custom-Header')).toBe('test'); // Verify encoded stream content const decoder = new TextDecoder(); const encodedStream = response.body!; const chunks = await convertReadableStreamToArray(encodedStream); const decodedChunks = chunks.map(chunk => decoder.decode(chunk)); expect(decodedChunks).toEqual(['test-data']); }); }); --- File: /ai/packages/ai/src/text-stream/create-text-stream-response.ts --- import { prepareHeaders } from '../util/prepare-headers'; export function createTextStreamResponse({ status, statusText, headers, textStream, }: ResponseInit & { textStream: ReadableStream<string>; }): Response { return new Response(textStream.pipeThrough(new TextEncoderStream()), { status: status ?? 200, statusText, headers: prepareHeaders(headers, { 'content-type': 'text/plain; charset=utf-8', }), }); } --- File: /ai/packages/ai/src/text-stream/index.ts --- export { createTextStreamResponse } from './create-text-stream-response'; export { pipeTextStreamToResponse } from './pipe-text-stream-to-response'; --- File: /ai/packages/ai/src/text-stream/pipe-text-stream-to-response.test.ts --- import { convertArrayToReadableStream } from '@ai-sdk/provider-utils/test'; import { createMockServerResponse } from '../test/mock-server-response'; import { pipeTextStreamToResponse } from './pipe-text-stream-to-response'; describe('pipeTextStreamToResponse', () => { it('should write to ServerResponse with correct headers and encoded stream', async () => { const mockResponse = createMockServerResponse(); pipeTextStreamToResponse({ response: mockResponse, status: 200, statusText: 'OK', headers: { 'Custom-Header': 'test', }, textStream: convertArrayToReadableStream(['test-data']), }); // Wait for the stream to finish writing await mockResponse.waitForEnd(); // Verify response properties expect(mockResponse.statusCode).toBe(200); expect(mockResponse.statusMessage).toBe('OK'); // Verify headers expect(mockResponse.headers).toMatchInlineSnapshot(` { "content-type": "text/plain; charset=utf-8", "custom-header": "test", } `); // Verify written data using decoded chunks expect(mockResponse.getDecodedChunks()).toStrictEqual(['test-data']); }); }); --- File: /ai/packages/ai/src/text-stream/pipe-text-stream-to-response.ts --- import { ServerResponse } from 'node:http'; import { prepareHeaders } from '../util/prepare-headers'; import { writeToServerResponse } from '../util/write-to-server-response'; export function pipeTextStreamToResponse({ response, status, statusText, headers, textStream, }: { response: ServerResponse; textStream: ReadableStream<string>; } & ResponseInit): void { writeToServerResponse({ response, status, statusText, headers: Object.fromEntries( prepareHeaders(headers, { 'content-type': 'text/plain; charset=utf-8', }).entries(), ), stream: textStream.pipeThrough(new TextEncoderStream()), }); } --- File: /ai/packages/ai/src/tool/mcp/json-rpc-message.ts --- import { z } from 'zod/v4'; import { BaseParamsSchema, RequestSchema, ResultSchema } from './types'; const JSONRPC_VERSION = '2.0'; const JSONRPCRequestSchema = z .object({ jsonrpc: z.literal(JSONRPC_VERSION), id: z.union([z.string(), z.number().int()]), }) .merge(RequestSchema) .strict(); export type JSONRPCRequest = z.infer<typeof JSONRPCRequestSchema>; const JSONRPCResponseSchema = z .object({ jsonrpc: z.literal(JSONRPC_VERSION), id: z.union([z.string(), z.number().int()]), result: ResultSchema, }) .strict(); export type JSONRPCResponse = z.infer<typeof JSONRPCResponseSchema>; const JSONRPCErrorSchema = z .object({ jsonrpc: z.literal(JSONRPC_VERSION), id: z.union([z.string(), z.number().int()]), error: z.object({ code: z.number().int(), message: z.string(), data: z.optional(z.unknown()), }), }) .strict(); export type JSONRPCError = z.infer<typeof JSONRPCErrorSchema>; const JSONRPCNotificationSchema = z .object({ jsonrpc: z.literal(JSONRPC_VERSION), }) .merge( z.object({ method: z.string(), params: z.optional(BaseParamsSchema), }), ) .strict(); export type JSONRPCNotification = z.infer<typeof JSONRPCNotificationSchema>; export const JSONRPCMessageSchema = z.union([ JSONRPCRequestSchema, JSONRPCNotificationSchema, JSONRPCResponseSchema, JSONRPCErrorSchema, ]); export type JSONRPCMessage = z.infer<typeof JSONRPCMessageSchema>; --- File: /ai/packages/ai/src/tool/mcp/mcp-client.test.ts --- import { z } from 'zod/v4'; import { MCPClientError } from '../../error/mcp-client-error'; import { createMCPClient } from './mcp-client'; import { MockMCPTransport } from './mock-mcp-transport'; import { CallToolResult } from './types'; const createMockTransport = vi.fn(config => new MockMCPTransport(config)); vi.mock('./mcp-transport.ts', async importOriginal => { const actual = await importOriginal<typeof import('./mcp-transport')>(); return { ...actual, createMcpTransport: vi.fn(config => { return createMockTransport(config); }), }; }); describe('MCPClient', () => { let client: Awaited<ReturnType<typeof createMCPClient>>; beforeEach(async () => { createMockTransport.mockClear(); createMockTransport.mockImplementation(() => new MockMCPTransport()); }); afterEach(async () => { await client?.close(); }); it('should return AI SDK compatible tool set', async () => { client = await createMCPClient({ transport: { type: 'sse', url: 'https://example.com/sse' }, }); const tools = await client.tools(); expect(tools).toHaveProperty('mock-tool'); const tool = tools['mock-tool']; expect(tool).toHaveProperty('inputSchema'); expect(tool.inputSchema).toMatchObject({ jsonSchema: { type: 'object', properties: { foo: { type: 'string' }, }, }, }); expect(tool).toHaveProperty('type'); expect(tool.type).toBe('dynamic'); const toolCall = tool.execute; expect(toolCall).toBeDefined(); expect( await toolCall( { foo: 'bar' }, { messages: [], toolCallId: '1', }, ), ).toMatchInlineSnapshot(` { "content": [ { "text": "Mock tool call result", "type": "text", }, ], "isError": false, } `); }); it('should return typed AI SDK compatible tool set when schemas are provided', async () => { const mockTransport = new MockMCPTransport({ overrideTools: [ { name: 'mock-tool-only-input-schema', description: 'A mock tool for testing custom transports', inputSchema: { type: 'object', properties: { foo: { type: 'string' }, }, }, }, ], }); client = await createMCPClient({ transport: mockTransport, }); const tools = await client.tools({ schemas: { 'mock-tool-only-input-schema': { inputSchema: z.object({ foo: z.string(), }), }, }, }); expect(tools).toHaveProperty('mock-tool-only-input-schema'); const tool = tools['mock-tool-only-input-schema']; type ToolParams = Parameters<typeof tool.execute>[0]; expectTypeOf<ToolParams>().toEqualTypeOf<{ foo: string }>(); const result = await tool.execute( { foo: 'bar' }, { messages: [], toolCallId: '1', }, ); expectTypeOf<typeof result>().toEqualTypeOf<CallToolResult>(); }); it('should not return user-defined tool if it is nonexistent', async () => { client = await createMCPClient({ transport: { type: 'sse', url: 'https://example.com/sse' }, }); const tools = await client.tools({ schemas: { 'nonexistent-tool': { inputSchema: z.object({ bar: z.string() }), }, }, }); expect(tools).not.toHaveProperty('nonexistent-tool'); }); it('should error when calling tool with misconfigured parameters', async () => { createMockTransport.mockImplementation( () => new MockMCPTransport({ failOnInvalidToolParams: true, }), ); client = await createMCPClient({ transport: { type: 'sse', url: 'https://example.com/sse' }, }); const tools = await client.tools({ schemas: { 'mock-tool': { inputSchema: z.object({ bar: z.string() }), }, }, }); const toolCall = tools['mock-tool'].execute; await expect( toolCall({ bar: 'bar' }, { messages: [], toolCallId: '1' }), ).rejects.toThrow(MCPClientError); }); it('should throw if the server does not support any tools', async () => { createMockTransport.mockImplementation( () => new MockMCPTransport({ overrideTools: [], }), ); client = await createMCPClient({ transport: { type: 'sse', url: 'https://example.com/sse' }, }); await expect(client.tools()).rejects.toThrow(MCPClientError); }); it('should throw if server sends invalid initialize result', async () => { createMockTransport.mockImplementation( () => new MockMCPTransport({ initializeResult: {}, }), ); await expect( createMCPClient({ transport: { type: 'sse', url: 'https://example.com/sse' }, }), ).rejects.toThrowError(MCPClientError); }); it('should throw if server sends invalid protocol version', async () => { createMockTransport.mockImplementation( () => new MockMCPTransport({ initializeResult: { protocolVersion: 'foo', serverInfo: { name: 'mock-mcp-server', version: '1.0.0', }, capabilities: {}, }, }), ); await expect( createMCPClient({ transport: { type: 'sse', url: 'https://example.com/sse' }, }), ).rejects.toThrowError(MCPClientError); }); it('should close transport when client is closed', async () => { const mockTransport = new MockMCPTransport(); const closeSpy = vi.spyOn(mockTransport, 'close'); createMockTransport.mockImplementation(() => mockTransport); const client = await createMCPClient({ transport: { type: 'sse', url: 'https://example.com/sse' }, }); await client.close(); expect(closeSpy).toHaveBeenCalled(); }); it('should throw Abort Error if tool call request is aborted', async () => { client = await createMCPClient({ transport: { type: 'sse', url: 'https://example.com/sse' }, }); const tools = await client.tools(); const tool = tools['mock-tool']; const abortController = new AbortController(); abortController.abort(); await expect( tool.execute( { foo: 'bar' }, { messages: [], toolCallId: '1', abortSignal: abortController.signal, }, ), ).rejects.toSatisfy( error => error instanceof Error && error.name === 'AbortError', ); }); it('should use onUncaughtError callback if provided', async () => { const onUncaughtError = vi.fn(); const mockTransport = new MockMCPTransport({ sendError: true, }); createMockTransport.mockImplementation(() => mockTransport); client = await createMCPClient({ transport: { type: 'sse', url: 'https://example.com/sse' }, onUncaughtError, }); expect(onUncaughtError).toHaveBeenCalled(); }); it('should support custom transports', async () => { const mockTransport = new MockMCPTransport(); client = await createMCPClient({ transport: mockTransport, }); const tools = await client.tools({ schemas: { 'mock-tool': { inputSchema: z.object({ foo: z.string(), }), }, }, }); expect(tools).toHaveProperty('mock-tool'); const tool = tools['mock-tool']; type ToolParams = Parameters<typeof tool.execute>[0]; expectTypeOf<ToolParams>().toEqualTypeOf<{ foo: string }>(); const result = await tool.execute( { foo: 'bar' }, { messages: [], toolCallId: '1', }, ); expectTypeOf<typeof result>().toEqualTypeOf<CallToolResult>(); }); it('should throw if transport is missing required methods', async () => { // Because isCustomMcpTransport will return false, the client will fallback to createMcpTransport, but it will throw because the transport is invalid: const invalidTransport = { start: vi.fn(), close: vi.fn(), }; // @ts-expect-error - invalid transport createMockTransport.mockImplementation(() => invalidTransport); await expect( // @ts-expect-error - invalid transport createMCPClient({ transport: invalidTransport }), ).rejects.toThrow(); }); it('should support zero-argument tools', async () => { client = await createMCPClient({ transport: { type: 'sse', url: 'https://example.com/sse' }, }); const tools = await client.tools(); const tool = tools['mock-tool-no-args']; expect(tool).toHaveProperty('inputSchema'); expect(tool.inputSchema).toMatchObject({ jsonSchema: { type: 'object', properties: {}, additionalProperties: false, }, }); const result = await tool.execute({}, { messages: [], toolCallId: '1' }); expect(result).toMatchInlineSnapshot(` { "content": [ { "text": "Mock tool call result", "type": "text", }, ], "isError": false, } `); }); }); --- File: /ai/packages/ai/src/tool/mcp/mcp-client.ts --- import { JSONSchema7 } from '@ai-sdk/provider'; import { dynamicTool, jsonSchema, Tool, tool, ToolCallOptions, } from '@ai-sdk/provider-utils'; import { z, ZodType } from 'zod/v4'; import { MCPClientError } from '../../error/mcp-client-error'; import { JSONRPCError, JSONRPCNotification, JSONRPCRequest, JSONRPCResponse, } from './json-rpc-message'; import { createMcpTransport, isCustomMcpTransport, MCPTransport, MCPTransportConfig, } from './mcp-transport'; import { CallToolResult, CallToolResultSchema, Configuration as ClientConfiguration, InitializeResultSchema, LATEST_PROTOCOL_VERSION, ListToolsResult, ListToolsResultSchema, McpToolSet, Notification, PaginatedRequest, Request, RequestOptions, ServerCapabilities, SUPPORTED_PROTOCOL_VERSIONS, ToolSchemas, } from './types'; const CLIENT_VERSION = '1.0.0'; export interface MCPClientConfig { /** Transport configuration for connecting to the MCP server */ transport: MCPTransportConfig | MCPTransport; /** Optional callback for uncaught errors */ onUncaughtError?: (error: unknown) => void; /** Optional client name, defaults to 'ai-sdk-mcp-client' */ name?: string; } export async function createMCPClient( config: MCPClientConfig, ): Promise<MCPClient> { const client = new DefaultMCPClient(config); await client.init(); return client; } export interface MCPClient { tools<TOOL_SCHEMAS extends ToolSchemas = 'automatic'>(options?: { schemas?: TOOL_SCHEMAS; }): Promise<McpToolSet<TOOL_SCHEMAS>>; close: () => Promise<void>; } /** * A lightweight MCP Client implementation * * The primary purpose of this client is tool conversion between MCP<>AI SDK * but can later be extended to support other MCP features * * Tool parameters are automatically inferred from the server's JSON schema * if not explicitly provided in the tools configuration * * This client is meant to be used to communicate with a single server. To communicate and fetch tools across multiple servers, it's recommended to create a new client instance per server. * * Not supported: * - Client options (e.g. sampling, roots) as they are not needed for tool conversion * - Accepting notifications * - Session management (when passing a sessionId to an instance of the Streamable HTTP transport) * - Resumable SSE streams */ class DefaultMCPClient implements MCPClient { private transport: MCPTransport; private onUncaughtError?: (error: unknown) => void; private clientInfo: ClientConfiguration; private requestMessageId = 0; private responseHandlers: Map< number, (response: JSONRPCResponse | Error) => void > = new Map(); private serverCapabilities: ServerCapabilities = {}; private isClosed = true; constructor({ transport: transportConfig, name = 'ai-sdk-mcp-client', onUncaughtError, }: MCPClientConfig) { this.onUncaughtError = onUncaughtError; if (isCustomMcpTransport(transportConfig)) { this.transport = transportConfig; } else { this.transport = createMcpTransport(transportConfig); } this.transport.onclose = () => this.onClose(); this.transport.onerror = (error: Error) => this.onError(error); this.transport.onmessage = message => { if ('method' in message) { // This lightweight client implementation does not support // receiving notifications or requests from server. // If we get an unsupported message, we can safely ignore it and pass to the onError handler: this.onError( new MCPClientError({ message: 'Unsupported message type', }), ); return; } this.onResponse(message); }; this.clientInfo = { name, version: CLIENT_VERSION, }; } async init(): Promise<this> { try { await this.transport.start(); this.isClosed = false; const result = await this.request({ request: { method: 'initialize', params: { protocolVersion: LATEST_PROTOCOL_VERSION, capabilities: {}, clientInfo: this.clientInfo, }, }, resultSchema: InitializeResultSchema, }); if (result === undefined) { throw new MCPClientError({ message: 'Server sent invalid initialize result', }); } if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) { throw new MCPClientError({ message: `Server's protocol version is not supported: ${result.protocolVersion}`, }); } this.serverCapabilities = result.capabilities; // Complete initialization handshake: await this.notification({ method: 'notifications/initialized', }); return this; } catch (error) { await this.close(); throw error; } } async close(): Promise<void> { if (this.isClosed) return; await this.transport?.close(); this.onClose(); } private assertCapability(method: string): void { switch (method) { case 'initialize': break; case 'tools/list': case 'tools/call': if (!this.serverCapabilities.tools) { throw new MCPClientError({ message: `Server does not support tools`, }); } break; default: throw new MCPClientError({ message: `Unsupported method: ${method}`, }); } } private async request<T extends ZodType<object>>({ request, resultSchema, options, }: { request: Request; resultSchema: T; options?: RequestOptions; }): Promise<z.infer<T>> { return new Promise((resolve, reject) => { if (this.isClosed) { return reject( new MCPClientError({ message: 'Attempted to send a request from a closed client', }), ); } this.assertCapability(request.method); const signal = options?.signal; signal?.throwIfAborted(); const messageId = this.requestMessageId++; const jsonrpcRequest: JSONRPCRequest = { ...request, jsonrpc: '2.0', id: messageId, }; const cleanup = () => { this.responseHandlers.delete(messageId); }; this.responseHandlers.set(messageId, response => { if (signal?.aborted) { return reject( new MCPClientError({ message: 'Request was aborted', cause: signal.reason, }), ); } if (response instanceof Error) { return reject(response); } try { const result = resultSchema.parse(response.result); resolve(result); } catch (error) { const parseError = new MCPClientError({ message: 'Failed to parse server response', cause: error, }); reject(parseError); } }); this.transport.send(jsonrpcRequest).catch(error => { cleanup(); reject(error); }); }); } private async listTools({ params, options, }: { params?: PaginatedRequest['params']; options?: RequestOptions; } = {}): Promise<ListToolsResult> { try { return this.request({ request: { method: 'tools/list', params }, resultSchema: ListToolsResultSchema, options, }); } catch (error) { throw error; } } private async callTool({ name, args, options, }: { name: string; args: Record<string, unknown>; options?: ToolCallOptions; }): Promise<CallToolResult> { try { return this.request({ request: { method: 'tools/call', params: { name, arguments: args } }, resultSchema: CallToolResultSchema, options: { signal: options?.abortSignal, }, }); } catch (error) { throw error; } } private async notification(notification: Notification): Promise<void> { const jsonrpcNotification: JSONRPCNotification = { ...notification, jsonrpc: '2.0', }; await this.transport.send(jsonrpcNotification); } /** * Returns a set of AI SDK tools from the MCP server * @returns A record of tool names to their implementations */ async tools<TOOL_SCHEMAS extends ToolSchemas = 'automatic'>({ schemas = 'automatic', }: { schemas?: TOOL_SCHEMAS; } = {}): Promise<McpToolSet<TOOL_SCHEMAS>> { const tools: Record<string, Tool> = {}; try { const listToolsResult = await this.listTools(); for (const { name, description, inputSchema } of listToolsResult.tools) { if (schemas !== 'automatic' && !(name in schemas)) { continue; } const self = this; const execute = async ( args: any, options: ToolCallOptions, ): Promise<CallToolResult> => { options?.abortSignal?.throwIfAborted(); return self.callTool({ name, args, options }); }; const toolWithExecute = schemas === 'automatic' ? dynamicTool({ description, inputSchema: jsonSchema({ ...inputSchema, properties: inputSchema.properties ?? {}, additionalProperties: false, } as JSONSchema7), execute, }) : tool({ description, inputSchema: schemas[name].inputSchema, execute, }); tools[name] = toolWithExecute; } return tools as McpToolSet<TOOL_SCHEMAS>; } catch (error) { throw error; } } private onClose(): void { if (this.isClosed) return; this.isClosed = true; const error = new MCPClientError({ message: 'Connection closed', }); for (const handler of this.responseHandlers.values()) { handler(error); } this.responseHandlers.clear(); } private onError(error: unknown): void { if (this.onUncaughtError) { this.onUncaughtError(error); } } private onResponse(response: JSONRPCResponse | JSONRPCError): void { const messageId = Number(response.id); const handler = this.responseHandlers.get(messageId); if (handler === undefined) { throw new MCPClientError({ message: `Protocol error: Received a response for an unknown message ID: ${JSON.stringify( response, )}`, }); } this.responseHandlers.delete(messageId); handler( 'result' in response ? response : new MCPClientError({ message: response.error.message, cause: response.error, }), ); } } --- File: /ai/packages/ai/src/tool/mcp/mcp-sse-transport.test.ts --- import { createTestServer, TestResponseController, } from '@ai-sdk/provider-utils/test'; import { MCPClientError } from '../../error/mcp-client-error'; import { SseMCPTransport } from './mcp-sse-transport'; describe('SseMCPTransport', () => { const server = createTestServer({ 'http://localhost:3000/sse': {}, 'http://localhost:3000/messages': { response: { type: 'json-value', body: { ok: true, message: 'Created', code: 201, }, }, }, 'http://localhost:3333/sse': {}, }); let transport: SseMCPTransport; beforeEach(() => { transport = new SseMCPTransport({ url: 'http://localhost:3000/sse', }); }); it('should establish connection and receive endpoint', async () => { const controller = new TestResponseController(); server.urls['http://localhost:3000/sse'].response = { type: 'controlled-stream', controller, }; const connectPromise = transport.start(); controller.write( 'event: endpoint\ndata: http://localhost:3000/messages\n\n', ); await connectPromise; await transport.close(); expect(server.calls).toHaveLength(1); expect(server.calls[0].requestMethod).toBe('GET'); expect(server.calls[0].requestUrl).toBe('http://localhost:3000/sse'); expect(server.calls[0].requestHeaders).toEqual({ accept: 'text/event-stream', }); }); it('should throw if server returns non-200 status', async () => { server.urls['http://localhost:3000/sse'].response = { type: 'error', status: 500, body: 'Internal Server Error', }; await expect(transport.start()).rejects.toThrow(); }); it('should handle valid JSON-RPC messages', async () => { const controller = new TestResponseController(); server.urls['http://localhost:3000/sse'].response = { type: 'controlled-stream', controller, }; const messagePromise = new Promise(resolve => { transport.onmessage = msg => resolve(msg); }); const connectPromise = transport.start(); controller.write( 'event: endpoint\ndata: http://localhost:3000/messages\n\n', ); await connectPromise; const testMessage = { jsonrpc: '2.0' as const, method: 'test', params: { foo: 'bar' }, id: '1', }; controller.write( `event: message\ndata: ${JSON.stringify(testMessage)}\n\n`, ); expect(await messagePromise).toEqual(testMessage); await transport.close(); }); it('should handle invalid JSON-RPC messages', async () => { const controller = new TestResponseController(); server.urls['http://localhost:3000/sse'].response = { type: 'controlled-stream', controller, }; const errorPromise = new Promise<unknown>(resolve => { transport.onerror = err => resolve(err); }); const connectPromise = transport.start(); controller.write( 'event: endpoint\ndata: http://localhost:3000/messages\n\n', ); await connectPromise; const invalidMessage = { foo: 'bar' }; controller.write( `event: message\ndata: ${JSON.stringify(invalidMessage)}\n\n`, ); const error = await errorPromise; expect(error).toBeInstanceOf(MCPClientError); expect((error as Error).message).toContain('Failed to parse message'); await transport.close(); }); it('should send messages as POST requests', async () => { const controller = new TestResponseController(); server.urls['http://localhost:3000/sse'].response = { type: 'controlled-stream', controller, }; const connectPromise = transport.start(); controller.write( 'event: endpoint\ndata: http://localhost:3000/messages\n\n', ); await connectPromise; const message = { jsonrpc: '2.0' as const, method: 'test', params: { foo: 'bar' }, id: '1', }; await transport.send(message); expect(server.calls).toHaveLength(2); expect(server.calls[1].requestMethod).toBe('POST'); expect(server.calls[1].requestUrl).toBe('http://localhost:3000/messages'); expect(await server.calls[1].requestBodyJson).toEqual(message); await transport.close(); }); it('should handle POST request errors', async () => { const controller = new TestResponseController(); server.urls['http://localhost:3000/sse'].response = { type: 'controlled-stream', controller, }; server.urls['http://localhost:3000/messages'].response = { type: 'error', status: 500, body: 'Internal Server Error', }; const errorPromise = new Promise<unknown>(resolve => { transport.onerror = err => resolve(err); }); const connectPromise = transport.start(); controller.write( 'event: endpoint\ndata: http://localhost:3000/messages\n\n', ); await connectPromise; const message = { jsonrpc: '2.0' as const, method: 'test', params: { foo: 'bar' }, id: '1', }; await transport.send(message); const error = await errorPromise; expect(error).toBeInstanceOf(MCPClientError); expect((error as Error).message).toContain('Error: POSTing to endpoint'); expect(transport['connected']).toBe(true); await transport.close(); }); it('should handle invalid endpoint URLs', async () => { server.urls['http://localhost:3333/sse'].response = { type: 'error', status: 500, body: 'Internal Server Error', }; transport = new SseMCPTransport({ url: 'http://localhost:3333/sse', }); const errorPromise = new Promise<unknown>(resolve => { transport.onerror = err => resolve(err); }); const connectPromise = transport.start(); await expect(connectPromise).rejects.toThrow(); const error = await errorPromise; expect((error as Error).message).toContain( 'MCP SSE Transport Error: 500 Internal Server Error', ); }); it('should send custom headers with all requests', async () => { const controller = new TestResponseController(); server.urls['http://localhost:3000/sse'].response = { type: 'controlled-stream', controller, }; const customHeaders = { authorization: 'Bearer test-token', 'x-custom-header': 'test-value', }; transport = new SseMCPTransport({ url: 'http://localhost:3000/sse', headers: customHeaders, }); const connectPromise = transport.start(); controller.write( 'event: endpoint\ndata: http://localhost:3000/messages\n\n', ); await connectPromise; const message = { jsonrpc: '2.0' as const, method: 'test', params: { foo: 'bar' }, id: '1', }; await transport.send(message); // Verify SSE connection headers expect(server.calls[0].requestHeaders).toEqual({ accept: 'text/event-stream', ...customHeaders, }); // Verify POST request headers expect(server.calls[1].requestHeaders).toEqual({ 'content-type': 'application/json', ...customHeaders, }); await transport.close(); }); }); --- File: /ai/packages/ai/src/tool/mcp/mcp-sse-transport.ts --- import { EventSourceParserStream } from '@ai-sdk/provider-utils'; import { MCPClientError } from '../../error/mcp-client-error'; import { JSONRPCMessage, JSONRPCMessageSchema } from './json-rpc-message'; import { MCPTransport } from './mcp-transport'; export class SseMCPTransport implements MCPTransport { private endpoint?: URL; private abortController?: AbortController; private url: URL; private connected = false; private sseConnection?: { close: () => void; }; private headers?: Record<string, string>; onclose?: () => void; onerror?: (error: unknown) => void; onmessage?: (message: JSONRPCMessage) => void; constructor({ url, headers, }: { url: string; headers?: Record<string, string>; }) { this.url = new URL(url); this.headers = headers; } async start(): Promise<void> { return new Promise<void>((resolve, reject) => { if (this.connected) { return resolve(); } this.abortController = new AbortController(); const establishConnection = async () => { try { const headers = new Headers(this.headers); headers.set('Accept', 'text/event-stream'); const response = await fetch(this.url.href, { headers, signal: this.abortController?.signal, }); if (!response.ok || !response.body) { const error = new MCPClientError({ message: `MCP SSE Transport Error: ${response.status} ${response.statusText}`, }); this.onerror?.(error); return reject(error); } const stream = response.body .pipeThrough(new TextDecoderStream()) .pipeThrough(new EventSourceParserStream()); const reader = stream.getReader(); const processEvents = async () => { try { while (true) { const { done, value } = await reader.read(); if (done) { if (this.connected) { this.connected = false; throw new MCPClientError({ message: 'MCP SSE Transport Error: Connection closed unexpectedly', }); } return; } const { event, data } = value; if (event === 'endpoint') { this.endpoint = new URL(data, this.url); if (this.endpoint.origin !== this.url.origin) { throw new MCPClientError({ message: `MCP SSE Transport Error: Endpoint origin does not match connection origin: ${this.endpoint.origin}`, }); } this.connected = true; resolve(); } else if (event === 'message') { try { const message = JSONRPCMessageSchema.parse( JSON.parse(data), ); this.onmessage?.(message); } catch (error) { const e = new MCPClientError({ message: 'MCP SSE Transport Error: Failed to parse message', cause: error, }); this.onerror?.(e); // We do not throw here so we continue processing events after reporting the error } } } } catch (error) { if (error instanceof Error && error.name === 'AbortError') { return; } this.onerror?.(error); reject(error); } }; this.sseConnection = { close: () => reader.cancel(), }; processEvents(); } catch (error) { if (error instanceof Error && error.name === 'AbortError') { return; } this.onerror?.(error); reject(error); } }; establishConnection(); }); } async close(): Promise<void> { this.connected = false; this.sseConnection?.close(); this.abortController?.abort(); this.onclose?.(); } async send(message: JSONRPCMessage): Promise<void> { if (!this.endpoint || !this.connected) { throw new MCPClientError({ message: 'MCP SSE Transport Error: Not connected', }); } try { const headers = new Headers(this.headers); headers.set('Content-Type', 'application/json'); const init = { method: 'POST', headers, body: JSON.stringify(message), signal: this.abortController?.signal, }; const response = await fetch(this.endpoint, init); if (!response.ok) { const text = await response.text().catch(() => null); const error = new MCPClientError({ message: `MCP SSE Transport Error: POSTing to endpoint (HTTP ${response.status}): ${text}`, }); this.onerror?.(error); return; } } catch (error) { this.onerror?.(error); return; } } } export function deserializeMessage(line: string): JSONRPCMessage { return JSONRPCMessageSchema.parse(JSON.parse(line)); } --- File: /ai/packages/ai/src/tool/mcp/mcp-transport.ts --- import { MCPClientError } from '../../error/mcp-client-error'; import { JSONRPCMessage } from './json-rpc-message'; import { SseMCPTransport } from './mcp-sse-transport'; /** * Transport interface for MCP (Model Context Protocol) communication. * Maps to the `Transport` interface in the MCP spec. */ export interface MCPTransport { /** * Initialize and start the transport */ start(): Promise<void>; /** * Send a JSON-RPC message through the transport * @param message The JSON-RPC message to send */ send(message: JSONRPCMessage): Promise<void>; /** * Clean up and close the transport */ close(): Promise<void>; /** * Event handler for transport closure */ onclose?: () => void; /** * Event handler for transport errors */ onerror?: (error: Error) => void; /** * Event handler for received messages */ onmessage?: (message: JSONRPCMessage) => void; } export type MCPTransportConfig = { type: 'sse'; /** * The URL of the MCP server. */ url: string; /** * Additional HTTP headers to be sent with requests. */ headers?: Record<string, string>; }; export function createMcpTransport(config: MCPTransportConfig): MCPTransport { if (config.type !== 'sse') { throw new MCPClientError({ message: 'Unsupported or invalid transport configuration. If you are using a custom transport, make sure it implements the MCPTransport interface.', }); } return new SseMCPTransport(config); } export function isCustomMcpTransport( transport: MCPTransportConfig | MCPTransport, ): transport is MCPTransport { return ( 'start' in transport && typeof transport.start === 'function' && 'send' in transport && typeof transport.send === 'function' && 'close' in transport && typeof transport.close === 'function' ); } --- File: /ai/packages/ai/src/tool/mcp/mock-mcp-transport.ts --- import { delay } from '@ai-sdk/provider-utils'; import { JSONRPCMessage } from './json-rpc-message'; import { MCPTransport } from './mcp-transport'; import { MCPTool } from './types'; const DEFAULT_TOOLS: MCPTool[] = [ { name: 'mock-tool', description: 'A mock tool for testing', inputSchema: { type: 'object', properties: { foo: { type: 'string' }, }, }, }, { name: 'mock-tool-no-args', description: 'A mock tool for testing', inputSchema: { type: 'object', }, }, ]; export class MockMCPTransport implements MCPTransport { private tools; private failOnInvalidToolParams; private initializeResult; private sendError; onmessage?: (message: JSONRPCMessage) => void; onclose?: () => void; onerror?: (error: Error) => void; constructor({ overrideTools = DEFAULT_TOOLS, failOnInvalidToolParams = false, initializeResult, sendError = false, }: { overrideTools?: MCPTool[]; failOnInvalidToolParams?: boolean; initializeResult?: Record<string, unknown>; sendError?: boolean; } = {}) { this.tools = overrideTools; this.failOnInvalidToolParams = failOnInvalidToolParams; this.initializeResult = initializeResult; this.sendError = sendError; } async start(): Promise<void> { if (this.sendError) { this.onerror?.({ name: 'UnknownError', message: 'Unknown error', }); } } async send(message: JSONRPCMessage): Promise<void> { // Mock server response implementation - extend as necessary: if ('method' in message && 'id' in message) { if (message.method === 'initialize') { await delay(10); this.onmessage?.({ jsonrpc: '2.0', id: message.id, result: this.initializeResult || { protocolVersion: '2025-06-18', serverInfo: { name: 'mock-mcp-server', version: '1.0.0', }, capabilities: { ...(this.tools.length > 0 ? { tools: {} } : {}), }, }, }); } if (message.method === 'tools/list') { await delay(10); if (this.tools.length === 0) { this.onmessage?.({ jsonrpc: '2.0', id: message.id, error: { code: -32000, message: 'Method not supported', }, }); return; } this.onmessage?.({ jsonrpc: '2.0', id: message.id, result: { tools: this.tools, }, }); } if (message.method === 'tools/call') { await delay(10); const toolName = message.params?.name; const tool = this.tools.find(t => t.name === toolName); if (!tool) { this.onmessage?.({ jsonrpc: '2.0', id: message.id, error: { code: -32601, message: `Tool ${toolName} not found`, }, }); return; } if (this.failOnInvalidToolParams) { this.onmessage?.({ jsonrpc: '2.0', id: message.id, error: { code: -32602, message: `Invalid tool inputSchema: ${JSON.stringify( message.params?.arguments, )}`, }, }); return; } this.onmessage?.({ jsonrpc: '2.0', id: message.id, result: { content: [ { type: 'text', text: `Mock tool call result`, }, ], }, }); } } } async close(): Promise<void> { this.onclose?.(); } } --- File: /ai/packages/ai/src/tool/mcp/types.ts --- import { z } from 'zod/v4'; import { JSONObject } from '@ai-sdk/provider'; import { FlexibleSchema, Tool } from '@ai-sdk/provider-utils'; export const LATEST_PROTOCOL_VERSION = '2025-06-18'; export const SUPPORTED_PROTOCOL_VERSIONS = [ LATEST_PROTOCOL_VERSION, '2025-03-26', '2024-11-05', ]; export type ToolSchemas = | Record<string, { inputSchema: FlexibleSchema<JSONObject | unknown> }> | 'automatic' | undefined; export type McpToolSet<TOOL_SCHEMAS extends ToolSchemas = 'automatic'> = TOOL_SCHEMAS extends Record<string, { inputSchema: FlexibleSchema<any> }> ? { [K in keyof TOOL_SCHEMAS]: TOOL_SCHEMAS[K] extends { inputSchema: FlexibleSchema<infer INPUT>; } ? Tool<INPUT, CallToolResult> & Required<Pick<Tool<INPUT, CallToolResult>, 'execute'>> : never; } : McpToolSet<Record<string, { inputSchema: FlexibleSchema<unknown> }>>; const ClientOrServerImplementationSchema = z.looseObject({ name: z.string(), version: z.string(), }); export type Configuration = z.infer<typeof ClientOrServerImplementationSchema>; export const BaseParamsSchema = z.looseObject({ _meta: z.optional(z.object({}).loose()), }); type BaseParams = z.infer<typeof BaseParamsSchema>; export const ResultSchema = BaseParamsSchema; export const RequestSchema = z.object({ method: z.string(), params: z.optional(BaseParamsSchema), }); export type Request = z.infer<typeof RequestSchema>; export type RequestOptions = { signal?: AbortSignal; timeout?: number; maxTotalTimeout?: number; }; export type Notification = z.infer<typeof RequestSchema>; const ServerCapabilitiesSchema = z.looseObject({ experimental: z.optional(z.object({}).loose()), logging: z.optional(z.object({}).loose()), prompts: z.optional( z.looseObject({ listChanged: z.optional(z.boolean()), }), ), resources: z.optional( z.looseObject({ subscribe: z.optional(z.boolean()), listChanged: z.optional(z.boolean()), }), ), tools: z.optional( z.looseObject({ listChanged: z.optional(z.boolean()), }), ), }); export type ServerCapabilities = z.infer<typeof ServerCapabilitiesSchema>; export const InitializeResultSchema = ResultSchema.extend({ protocolVersion: z.string(), capabilities: ServerCapabilitiesSchema, serverInfo: ClientOrServerImplementationSchema, instructions: z.optional(z.string()), }); export type InitializeResult = z.infer<typeof InitializeResultSchema>; export type PaginatedRequest = Request & { params?: BaseParams & { cursor?: string; }; }; const PaginatedResultSchema = ResultSchema.extend({ nextCursor: z.optional(z.string()), }); const ToolSchema = z .object({ name: z.string(), description: z.optional(z.string()), inputSchema: z .object({ type: z.literal('object'), properties: z.optional(z.object({}).loose()), }) .loose(), }) .loose(); export type MCPTool = z.infer<typeof ToolSchema>; export const ListToolsResultSchema = PaginatedResultSchema.extend({ tools: z.array(ToolSchema), }); export type ListToolsResult = z.infer<typeof ListToolsResultSchema>; const TextContentSchema = z .object({ type: z.literal('text'), text: z.string(), }) .loose(); const ImageContentSchema = z .object({ type: z.literal('image'), data: z.base64(), mimeType: z.string(), }) .loose(); const ResourceContentsSchema = z .object({ /** * The URI of this resource. */ uri: z.string(), /** * The MIME type of this resource, if known. */ mimeType: z.optional(z.string()), }) .loose(); const TextResourceContentsSchema = ResourceContentsSchema.extend({ text: z.string(), }); const BlobResourceContentsSchema = ResourceContentsSchema.extend({ blob: z.base64(), }); const EmbeddedResourceSchema = z .object({ type: z.literal('resource'), resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema]), }) .loose(); export const CallToolResultSchema = ResultSchema.extend({ content: z.array( z.union([TextContentSchema, ImageContentSchema, EmbeddedResourceSchema]), ), isError: z.boolean().default(false).optional(), }).or( ResultSchema.extend({ toolResult: z.unknown(), }), ); export type CallToolResult = z.infer<typeof CallToolResultSchema>; --- File: /ai/packages/ai/src/tool/index.ts --- export type { JSONRPCError, JSONRPCMessage, JSONRPCNotification, JSONRPCRequest, JSONRPCResponse, } from './mcp/json-rpc-message'; export { createMCPClient as experimental_createMCPClient, type MCPClientConfig as experimental_MCPClientConfig, type MCPClient as experimental_MCPClient, } from './mcp/mcp-client'; export type { MCPTransport } from './mcp/mcp-transport'; --- File: /ai/packages/ai/src/transcribe/index.ts --- export { transcribe as experimental_transcribe } from './transcribe'; export type { TranscriptionResult as Experimental_TranscriptionResult } from './transcribe-result'; --- File: /ai/packages/ai/src/transcribe/transcribe-result.ts --- import { JSONValue } from '@ai-sdk/provider'; import { TranscriptionWarning } from '../types/transcription-model'; import { TranscriptionModelResponseMetadata } from '../types/transcription-model-response-metadata'; /** The result of a `transcribe` call. It contains the transcript and additional information. */ export interface TranscriptionResult { /** * The complete transcribed text from the audio. */ readonly text: string; /** * Array of transcript segments with timing information. * Each segment represents a portion of the transcribed text with start and end times. */ readonly segments: Array<{ /** * The text content of this segment. */ readonly text: string; /** * The start time of this segment in seconds. */ readonly startSecond: number; /** * The end time of this segment in seconds. */ readonly endSecond: number; }>; /** * The detected language of the audio content, as an ISO-639-1 code (e.g., 'en' for English). * May be undefined if the language couldn't be detected. */ readonly language: string | undefined; /** * The total duration of the audio file in seconds. * May be undefined if the duration couldn't be determined. */ readonly durationInSeconds: number | undefined; /** Warnings for the call, e.g. unsupported settings. */ readonly warnings: Array<TranscriptionWarning>; /** Response metadata from the provider. There may be multiple responses if we made multiple calls to the model. */ readonly responses: Array<TranscriptionModelResponseMetadata>; /** Provider metadata from the provider. */ readonly providerMetadata: Record<string, Record<string, JSONValue>>; } --- File: /ai/packages/ai/src/transcribe/transcribe.test.ts --- import { JSONValue, TranscriptionModelV2, TranscriptionModelV2CallWarning, } from '@ai-sdk/provider'; import { MockTranscriptionModelV2 } from '../test/mock-transcription-model-v2'; import { transcribe } from './transcribe'; const audioData = new Uint8Array([1, 2, 3, 4]); // Sample audio data const testDate = new Date(2024, 0, 1); const sampleTranscript = { text: 'This is a sample transcript.', segments: [ { startSecond: 0, endSecond: 2.5, text: 'This is a', }, { startSecond: 2.5, endSecond: 4.0, text: 'sample transcript.', }, ], language: 'en', durationInSeconds: 4.0, }; const createMockResponse = (options: { text: string; segments: Array<{ text: string; startSecond: number; endSecond: number; }>; language?: string; durationInSeconds?: number; warnings?: TranscriptionModelV2CallWarning[]; timestamp?: Date; modelId?: string; headers?: Record<string, string>; providerMetadata?: Record<string, Record<string, JSONValue>>; }) => ({ text: options.text, segments: options.segments, language: options.language, durationInSeconds: options.durationInSeconds, warnings: options.warnings ?? [], response: { timestamp: options.timestamp ?? new Date(), modelId: options.modelId ?? 'test-model-id', headers: options.headers ?? {}, }, providerMetadata: options.providerMetadata ?? {}, }); describe('transcribe', () => { it('should send args to doGenerate', async () => { const abortController = new AbortController(); const abortSignal = abortController.signal; let capturedArgs!: Parameters<TranscriptionModelV2['doGenerate']>[0]; await transcribe({ model: new MockTranscriptionModelV2({ doGenerate: async args => { capturedArgs = args; return createMockResponse({ ...sampleTranscript, }); }, }), audio: audioData, headers: { 'custom-request-header': 'request-header-value' }, abortSignal, }); expect(capturedArgs).toStrictEqual({ audio: audioData, mediaType: 'audio/wav', headers: { 'custom-request-header': 'request-header-value' }, abortSignal, providerOptions: {}, }); }); it('should return warnings', async () => { const result = await transcribe({ model: new MockTranscriptionModelV2({ doGenerate: async () => createMockResponse({ ...sampleTranscript, warnings: [ { type: 'other', message: 'Setting is not supported', }, ], providerMetadata: { 'test-provider': { 'test-key': 'test-value', }, }, }), }), audio: audioData, }); expect(result.warnings).toStrictEqual([ { type: 'other', message: 'Setting is not supported', }, ]); }); it('should return the transcript', async () => { const result = await transcribe({ model: new MockTranscriptionModelV2({ doGenerate: async () => createMockResponse({ ...sampleTranscript, }), }), audio: audioData, }); expect(result).toEqual({ ...sampleTranscript, warnings: [], responses: [ { timestamp: expect.any(Date), modelId: 'test-model-id', headers: {}, }, ], providerMetadata: {}, }); }); describe('error handling', () => { it('should throw NoTranscriptGeneratedError when no transcript is returned', async () => { await expect( transcribe({ model: new MockTranscriptionModelV2({ doGenerate: async () => createMockResponse({ text: '', segments: [], language: 'en', durationInSeconds: 0, timestamp: testDate, }), }), audio: audioData, }), ).rejects.toMatchObject({ name: 'AI_NoTranscriptGeneratedError', message: 'No transcript generated.', responses: [ { timestamp: testDate, modelId: expect.any(String), }, ], }); }); it('should include response headers in error when no transcript generated', async () => { await expect( transcribe({ model: new MockTranscriptionModelV2({ doGenerate: async () => createMockResponse({ text: '', segments: [], language: 'en', durationInSeconds: 0, timestamp: testDate, headers: { 'custom-response-header': 'response-header-value', }, }), }), audio: audioData, }), ).rejects.toMatchObject({ name: 'AI_NoTranscriptGeneratedError', message: 'No transcript generated.', responses: [ { timestamp: testDate, modelId: expect.any(String), headers: { 'custom-response-header': 'response-header-value', }, }, ], }); }); }); it('should return response metadata', async () => { const testHeaders = { 'x-test': 'value' }; const result = await transcribe({ model: new MockTranscriptionModelV2({ doGenerate: async () => createMockResponse({ ...sampleTranscript, timestamp: testDate, modelId: 'test-model', headers: testHeaders, }), }), audio: audioData, }); expect(result.responses).toStrictEqual([ { timestamp: testDate, modelId: 'test-model', headers: testHeaders, }, ]); }); }); --- File: /ai/packages/ai/src/transcribe/transcribe.ts --- import { JSONValue, TranscriptionModelV2 } from '@ai-sdk/provider'; import { ProviderOptions } from '@ai-sdk/provider-utils'; import { NoTranscriptGeneratedError } from '../error/no-transcript-generated-error'; import { audioMediaTypeSignatures, detectMediaType, } from '../util/detect-media-type'; import { download } from '../util/download'; import { prepareRetries } from '../util/prepare-retries'; import { UnsupportedModelVersionError } from '../error/unsupported-model-version-error'; import { DataContent } from '../prompt'; import { convertDataContentToUint8Array } from '../prompt/data-content'; import { TranscriptionWarning } from '../types/transcription-model'; import { TranscriptionModelResponseMetadata } from '../types/transcription-model-response-metadata'; import { TranscriptionResult } from './transcribe-result'; /** Generates transcripts using a transcription model. @param model - The transcription model to use. @param audio - The audio data to transcribe as DataContent (string | Uint8Array | ArrayBuffer | Buffer) or a URL. @param providerOptions - Additional provider-specific options that are passed through to the provider as body parameters. @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2. @param abortSignal - An optional abort signal that can be used to cancel the call. @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. @returns A result object that contains the generated transcript. */ export async function transcribe({ model, audio, providerOptions = {}, maxRetries: maxRetriesArg, abortSignal, headers, }: { /** The transcription model to use. */ model: TranscriptionModelV2; /** The audio data to transcribe. */ audio: DataContent | URL; /** Additional provider-specific options that are passed through to the provider as body parameters. The outer record is keyed by the provider name, and the inner record is keyed by the provider-specific metadata key. ```ts { "openai": { "temperature": 0 } } ``` */ providerOptions?: ProviderOptions; /** Maximum number of retries per transcript model call. Set to 0 to disable retries. @default 2 */ maxRetries?: number; /** Abort signal. */ abortSignal?: AbortSignal; /** Additional headers to include in the request. Only applicable for HTTP-based providers. */ headers?: Record<string, string>; }): Promise<TranscriptionResult> { if (model.specificationVersion !== 'v2') { throw new UnsupportedModelVersionError({ version: model.specificationVersion, provider: model.provider, modelId: model.modelId, }); } const { retry } = prepareRetries({ maxRetries: maxRetriesArg, abortSignal, }); const audioData = audio instanceof URL ? (await download({ url: audio })).data : convertDataContentToUint8Array(audio); const result = await retry(() => model.doGenerate({ audio: audioData, abortSignal, headers, providerOptions, mediaType: detectMediaType({ data: audioData, signatures: audioMediaTypeSignatures, }) ?? 'audio/wav', }), ); if (!result.text) { throw new NoTranscriptGeneratedError({ responses: [result.response] }); } return new DefaultTranscriptionResult({ text: result.text, segments: result.segments, language: result.language, durationInSeconds: result.durationInSeconds, warnings: result.warnings, responses: [result.response], providerMetadata: result.providerMetadata, }); } class DefaultTranscriptionResult implements TranscriptionResult { readonly text: string; readonly segments: Array<{ text: string; startSecond: number; endSecond: number; }>; readonly language: string | undefined; readonly durationInSeconds: number | undefined; readonly warnings: Array<TranscriptionWarning>; readonly responses: Array<TranscriptionModelResponseMetadata>; readonly providerMetadata: Record<string, Record<string, JSONValue>>; constructor(options: { text: string; segments: Array<{ text: string; startSecond: number; endSecond: number; }>; language: string | undefined; durationInSeconds: number | undefined; warnings: Array<TranscriptionWarning>; responses: Array<TranscriptionModelResponseMetadata>; providerMetadata: Record<string, Record<string, JSONValue>> | undefined; }) { this.text = options.text; this.segments = options.segments; this.language = options.language; this.durationInSeconds = options.durationInSeconds; this.warnings = options.warnings; this.responses = options.responses; this.providerMetadata = options.providerMetadata ?? {}; } } --- File: /ai/packages/ai/src/types/embedding-model.ts --- import { EmbeddingModelV2, EmbeddingModelV2Embedding } from '@ai-sdk/provider'; /** Embedding model that is used by the AI SDK Core functions. */ export type EmbeddingModel<VALUE = string> = string | EmbeddingModelV2<VALUE>; /** Embedding. */ export type Embedding = EmbeddingModelV2Embedding; --- File: /ai/packages/ai/src/types/image-model-response-metadata.ts --- export type ImageModelResponseMetadata = { /** Timestamp for the start of the generated response. */ timestamp: Date; /** The ID of the response model that was used to generate the response. */ modelId: string; /** Response headers. */ headers?: Record<string, string>; }; --- File: /ai/packages/ai/src/types/image-model.ts --- import { ImageModelV2, ImageModelV2CallWarning, ImageModelV2ProviderMetadata, } from '@ai-sdk/provider'; /** Image model that is used by the AI SDK Core functions. */ export type ImageModel = ImageModelV2; /** Warning from the model provider for this call. The call will proceed, but e.g. some settings might not be supported, which can lead to suboptimal results. */ export type ImageGenerationWarning = ImageModelV2CallWarning; /** Metadata from the model provider for this call */ export type ImageModelProviderMetadata = ImageModelV2ProviderMetadata; --- File: /ai/packages/ai/src/types/index.ts --- export type { Embedding, EmbeddingModel } from './embedding-model'; export type { ImageModel, ImageGenerationWarning as ImageModelCallWarning, ImageModelProviderMetadata, } from './image-model'; export type { ImageModelResponseMetadata } from './image-model-response-metadata'; export type { JSONValue } from './json-value'; export type { JSONSchema7 } from '@ai-sdk/provider'; export type { CallWarning, FinishReason, LanguageModel, ToolChoice, } from './language-model'; export type { LanguageModelRequestMetadata } from './language-model-request-metadata'; export type { LanguageModelResponseMetadata } from './language-model-response-metadata'; export type { Provider } from './provider'; export type { ProviderMetadata } from './provider-metadata'; export type { SpeechModel, SpeechWarning } from './speech-model'; export type { SpeechModelResponseMetadata } from './speech-model-response-metadata'; export type { TranscriptionModel, TranscriptionWarning, } from './transcription-model'; export type { TranscriptionModelResponseMetadata } from './transcription-model-response-metadata'; export type { EmbeddingModelUsage, LanguageModelUsage } from './usage'; --- File: /ai/packages/ai/src/types/json-value.ts --- import { JSONValue as OriginalJSONValue } from '@ai-sdk/provider'; import { z } from 'zod/v4'; export const jsonValueSchema: z.ZodType<JSONValue> = z.lazy(() => z.union([ z.null(), z.string(), z.number(), z.boolean(), z.record(z.string(), jsonValueSchema), z.array(jsonValueSchema), ]), ); export type JSONValue = OriginalJSONValue; --- File: /ai/packages/ai/src/types/language-model-request-metadata.ts --- export type LanguageModelRequestMetadata = { /** Request HTTP body that was sent to the provider API. */ body?: unknown; }; --- File: /ai/packages/ai/src/types/language-model-response-metadata.ts --- export type LanguageModelResponseMetadata = { /** ID for the generated response. */ id: string; /** Timestamp for the start of the generated response. */ timestamp: Date; /** The ID of the response model that was used to generate the response. */ modelId: string; /** Response headers (available only for providers that use HTTP requests). */ headers?: Record<string, string>; }; --- File: /ai/packages/ai/src/types/language-model.ts --- import { LanguageModelV2, LanguageModelV2CallWarning, LanguageModelV2FinishReason, LanguageModelV2Source, } from '@ai-sdk/provider'; /** Language model that is used by the AI SDK Core functions. */ export type LanguageModel = string | LanguageModelV2; /** Reason why a language model finished generating a response. Can be one of the following: - `stop`: model generated stop sequence - `length`: model generated maximum number of tokens - `content-filter`: content filter violation stopped the model - `tool-calls`: model triggered tool calls - `error`: model stopped because of an error - `other`: model stopped for other reasons */ export type FinishReason = LanguageModelV2FinishReason; /** Warning from the model provider for this call. The call will proceed, but e.g. some settings might not be supported, which can lead to suboptimal results. */ export type CallWarning = LanguageModelV2CallWarning; /** A source that has been used as input to generate the response. */ export type Source = LanguageModelV2Source; /** Tool choice for the generation. It supports the following settings: - `auto` (default): the model can choose whether and which tools to call. - `required`: the model must call a tool. It can choose which tool to call. - `none`: the model must not call tools - `{ type: 'tool', toolName: string (typed) }`: the model must call the specified tool */ export type ToolChoice<TOOLS extends Record<string, unknown>> = | 'auto' | 'none' | 'required' | { type: 'tool'; toolName: Extract<keyof TOOLS, string> }; --- File: /ai/packages/ai/src/types/provider-metadata.ts --- import { SharedV2ProviderMetadata } from '@ai-sdk/provider'; import { z } from 'zod/v4'; import { jsonValueSchema } from './json-value'; /** Additional provider-specific metadata that is returned from the provider. This is needed to enable provider-specific functionality that can be fully encapsulated in the provider. */ export type ProviderMetadata = SharedV2ProviderMetadata; export const providerMetadataSchema: z.ZodType<ProviderMetadata> = z.record( z.string(), z.record(z.string(), jsonValueSchema), ); --- File: /ai/packages/ai/src/types/provider.ts --- import { EmbeddingModel } from './embedding-model'; import { LanguageModel } from './language-model'; import { ImageModel } from './image-model'; /** * Provider for language, text embedding, and image models. */ export type Provider = { /** Returns the language model with the given id. The model id is then passed to the provider function to get the model. @param {string} id - The id of the model to return. @returns {LanguageModel} The language model associated with the id @throws {NoSuchModelError} If no such model exists. */ languageModel(modelId: string): LanguageModel; /** Returns the text embedding model with the given id. The model id is then passed to the provider function to get the model. @param {string} id - The id of the model to return. @returns {LanguageModel} The language model associated with the id @throws {NoSuchModelError} If no such model exists. */ textEmbeddingModel(modelId: string): EmbeddingModel<string>; /** Returns the image model with the given id. The model id is then passed to the provider function to get the model. @param {string} id - The id of the model to return. @returns {ImageModel} The image model associated with the id */ imageModel(modelId: string): ImageModel; }; --- File: /ai/packages/ai/src/types/speech-model-response-metadata.ts --- export type SpeechModelResponseMetadata = { /** Timestamp for the start of the generated response. */ timestamp: Date; /** The ID of the response model that was used to generate the response. */ modelId: string; /** Response headers. */ headers?: Record<string, string>; /** Response body. */ body?: unknown; }; --- File: /ai/packages/ai/src/types/speech-model.ts --- import { SpeechModelV2, SpeechModelV2CallWarning } from '@ai-sdk/provider'; /** Speech model that is used by the AI SDK Core functions. */ export type SpeechModel = SpeechModelV2; /** Warning from the model provider for this call. The call will proceed, but e.g. some settings might not be supported, which can lead to suboptimal results. */ export type SpeechWarning = SpeechModelV2CallWarning; --- File: /ai/packages/ai/src/types/transcription-model-response-metadata.ts --- export type TranscriptionModelResponseMetadata = { /** Timestamp for the start of the generated response. */ timestamp: Date; /** The ID of the response model that was used to generate the response. */ modelId: string; /** Response headers. */ headers?: Record<string, string>; }; --- File: /ai/packages/ai/src/types/transcription-model.ts --- import { TranscriptionModelV2, TranscriptionModelV2CallWarning, } from '@ai-sdk/provider'; /** Transcription model that is used by the AI SDK Core functions. */ export type TranscriptionModel = TranscriptionModelV2; /** Warning from the model provider for this call. The call will proceed, but e.g. some settings might not be supported, which can lead to suboptimal results. */ export type TranscriptionWarning = TranscriptionModelV2CallWarning; --- File: /ai/packages/ai/src/types/usage.ts --- import { LanguageModelV2Usage } from '@ai-sdk/provider'; /** Represents the number of tokens used in a prompt and completion. */ export type LanguageModelUsage = LanguageModelV2Usage; /** Represents the number of tokens used in an embedding. */ // TODO replace with EmbeddingModelV2Usage export type EmbeddingModelUsage = { /** The number of tokens used in the embedding. */ tokens: number; }; export function addLanguageModelUsage( usage1: LanguageModelUsage, usage2: LanguageModelUsage, ): LanguageModelUsage { return { inputTokens: addTokenCounts(usage1.inputTokens, usage2.inputTokens), outputTokens: addTokenCounts(usage1.outputTokens, usage2.outputTokens), totalTokens: addTokenCounts(usage1.totalTokens, usage2.totalTokens), reasoningTokens: addTokenCounts( usage1.reasoningTokens, usage2.reasoningTokens, ), cachedInputTokens: addTokenCounts( usage1.cachedInputTokens, usage2.cachedInputTokens, ), }; } function addTokenCounts( tokenCount1: number | undefined, tokenCount2: number | undefined, ): number | undefined { return tokenCount1 == null && tokenCount2 == null ? undefined : (tokenCount1 ?? 0) + (tokenCount2 ?? 0); } --- File: /ai/packages/ai/src/ui/call-completion-api.ts --- import { parseJsonEventStream, ParseResult } from '@ai-sdk/provider-utils'; import { UIMessageChunk, uiMessageChunkSchema, } from '../ui-message-stream/ui-message-chunks'; import { consumeStream } from '../util/consume-stream'; import { processTextStream } from './process-text-stream'; // use function to allow for mocking in tests: const getOriginalFetch = () => fetch; export async function callCompletionApi({ api, prompt, credentials, headers, body, streamProtocol = 'data', setCompletion, setLoading, setError, setAbortController, onFinish, onError, fetch = getOriginalFetch(), }: { api: string; prompt: string; credentials: RequestCredentials | undefined; headers: HeadersInit | undefined; body: Record<string, any>; streamProtocol: 'data' | 'text' | undefined; setCompletion: (completion: string) => void; setLoading: (loading: boolean) => void; setError: (error: Error | undefined) => void; setAbortController: (abortController: AbortController | null) => void; onFinish: ((prompt: string, completion: string) => void) | undefined; onError: ((error: Error) => void) | undefined; fetch: ReturnType<typeof getOriginalFetch> | undefined; }) { try { setLoading(true); setError(undefined); const abortController = new AbortController(); setAbortController(abortController); // Empty the completion immediately. setCompletion(''); const response = await fetch(api, { method: 'POST', body: JSON.stringify({ prompt, ...body, }), credentials, headers: { 'Content-Type': 'application/json', ...headers, }, signal: abortController.signal, }).catch(err => { throw err; }); if (!response.ok) { throw new Error( (await response.text()) ?? 'Failed to fetch the chat response.', ); } if (!response.body) { throw new Error('The response body is empty.'); } let result = ''; switch (streamProtocol) { case 'text': { await processTextStream({ stream: response.body, onTextPart: chunk => { result += chunk; setCompletion(result); }, }); break; } case 'data': { await consumeStream({ stream: parseJsonEventStream({ stream: response.body, schema: uiMessageChunkSchema, }).pipeThrough( new TransformStream<ParseResult<UIMessageChunk>, UIMessageChunk>({ async transform(part) { if (!part.success) { throw part.error; } const streamPart = part.value; if (streamPart.type === 'text-delta') { result += streamPart.delta; setCompletion(result); } else if (streamPart.type === 'error') { throw new Error(streamPart.errorText); } }, }), ), onError: error => { throw error; }, }); break; } default: { const exhaustiveCheck: never = streamProtocol; throw new Error(`Unknown stream protocol: ${exhaustiveCheck}`); } } if (onFinish) { onFinish(prompt, result); } setAbortController(null); return result; } catch (err) { // Ignore abort errors as they are expected. if ((err as any).name === 'AbortError') { setAbortController(null); return null; } if (err instanceof Error) { if (onError) { onError(err); } } setError(err as Error); } finally { setLoading(false); } } --- File: /ai/packages/ai/src/ui/chat-transport.ts --- import { UIMessageChunk } from '../ui-message-stream'; import { ChatRequestOptions } from './chat'; import { UIMessage } from './ui-messages'; /** * Transport interface for handling chat message communication and streaming. * * The `ChatTransport` interface provides fine-grained control over how messages * are sent to API endpoints and how responses are processed. This enables * alternative communication protocols like WebSockets, custom authentication * patterns, or specialized backend integrations. * * @template UI_MESSAGE - The UI message type extending UIMessage */ export interface ChatTransport<UI_MESSAGE extends UIMessage> { /** * Sends messages to the chat API endpoint and returns a streaming response. * * This method handles both new message submission and message regeneration. * It supports real-time streaming of responses through UIMessageChunk events. * * @param options - Configuration object containing: * @param options.trigger - The type of message submission: * - `'submit-message'`: Submitting a new user message * - `'regenerate-message'`: Regenerating an assistant response * @param options.chatId - Unique identifier for the chat session * @param options.messageId - ID of the message to regenerate (for regenerate-message trigger) or undefined for new messages * @param options.messages - Array of UI messages representing the conversation history * @param options.abortSignal - Signal to abort the request if needed * @param options.headers - Additional HTTP headers to include in the request * @param options.body - Additional JSON properties to include in the request body * @param options.metadata - Custom metadata to attach to the request * * @returns Promise resolving to a ReadableStream of UIMessageChunk objects. * The stream emits various chunk types like: * - `text-start`, `text-delta`, `text-end`: For streaming text content * - `tool-input-start`, `tool-input-delta`, `tool-input-available`: For tool calls * - `data-part-start`, `data-part-delta`, `data-part-available`: For data parts * - `error`: For error handling * * @throws Error when the API request fails or response is invalid */ sendMessages: ( options: { /** The type of message submission - either new message or regeneration */ trigger: 'submit-message' | 'regenerate-message'; /** Unique identifier for the chat session */ chatId: string; /** ID of the message to regenerate, or undefined for new messages */ messageId: string | undefined; /** Array of UI messages representing the conversation history */ messages: UI_MESSAGE[]; /** Signal to abort the request if needed */ abortSignal: AbortSignal | undefined; } & ChatRequestOptions, ) => Promise<ReadableStream<UIMessageChunk>>; /** * Reconnects to an existing streaming response for the specified chat session. * * This method is used to resume streaming when a connection is interrupted * or when resuming a chat session. It's particularly useful for maintaining * continuity in long-running conversations or recovering from network issues. * * @param options - Configuration object containing: * @param options.chatId - Unique identifier for the chat session to reconnect to * @param options.headers - Additional HTTP headers to include in the reconnection request * @param options.body - Additional JSON properties to include in the request body * @param options.metadata - Custom metadata to attach to the request * * @returns Promise resolving to: * - `ReadableStream<UIMessageChunk>`: If an active stream is found and can be resumed * - `null`: If no active stream exists for the specified chat session (e.g., response already completed) * * @throws Error when the reconnection request fails or response is invalid */ reconnectToStream: ( options: { /** Unique identifier for the chat session to reconnect to */ chatId: string; } & ChatRequestOptions, ) => Promise<ReadableStream<UIMessageChunk> | null>; } --- File: /ai/packages/ai/src/ui/chat.test-d.ts --- import { z } from 'zod/v4'; import { tool } from '@ai-sdk/provider-utils'; import { ChatInit } from './chat'; import { ToolSet } from '../generate-text/tool-set'; import { InferUITools, UIDataTypes, UIMessage, UITools } from './ui-messages'; type ToolCallCallback<TOOLS extends ToolSet | UITools> = NonNullable< ChatInit< UIMessage< never, UIDataTypes, TOOLS extends ToolSet ? InferUITools<TOOLS> : TOOLS > >['onToolCall'] >; type ToolCallArgument<TOOLS extends ToolSet | UITools> = Parameters< ToolCallCallback<TOOLS> >[0]['toolCall']; describe('onToolCall', () => { describe('no helpers', () => { it('single tool with output schema', () => { type Tools = { simple: { input: number; output: string; }; }; expectTypeOf< ToolCallArgument<Tools> & { dynamic?: false } >().toMatchTypeOf<{ toolName: 'simple'; input: number; }>(); }); it('single tool without output schema', () => { type Tools = { simple: { input: number; output: undefined; }; }; expectTypeOf< ToolCallArgument<Tools> & { dynamic?: false } >().toMatchTypeOf<{ toolName: 'simple'; input: number; }>(); }); it('multiple tools with output schema', () => { type Tools = { simple: { input: number; output: string; }; complex: { input: { title: string; description: string; }; output: Array<{ message: string; }>; }; }; expectTypeOf< ToolCallArgument<Tools> & { dynamic?: false } >().toMatchTypeOf< | { toolName: 'simple'; input: number; } | { toolName: 'complex'; input: { title: string; description: string; }; } >(); }); it('multiple tools without output schema', () => { type Tools = { simple: { input: number; output: undefined; }; complex: { input: { title: string; description: string; }; output: undefined; }; }; expectTypeOf< ToolCallArgument<Tools> & { dynamic?: false } >().toMatchTypeOf< | { toolName: 'simple'; input: number; } | { toolName: 'complex'; input: { title: string; description: string; }; } >(); }); }); describe('with helpers', () => { it('single tool with output schema', () => { const simple = tool({ inputSchema: z.number(), outputSchema: z.string(), }); const tools = { simple, }; expectTypeOf< ToolCallArgument<typeof tools> & { dynamic?: false } >().toMatchTypeOf<{ toolName: 'simple'; input: number; }>(); }); it('single tool without output schema', () => { const simple = tool({ inputSchema: z.number(), }); const tools = { simple, }; expectTypeOf< ToolCallArgument<typeof tools> & { dynamic?: false } >().toMatchTypeOf<{ toolName: 'simple'; input: number; }>(); }); it('multiple tools with output schema', () => { const simple = tool({ inputSchema: z.number(), outputSchema: z.string(), }); const complex = tool({ inputSchema: z.object({ title: z.string(), description: z.string(), }), outputSchema: z.array( z.object({ message: z.string(), }), ), }); const tools = { simple, complex, }; expectTypeOf< ToolCallArgument<typeof tools> & { dynamic?: false } >().toMatchTypeOf< | { toolName: 'simple'; input: number; } | { toolName: 'complex'; input: { title: string; description: string; }; } >(); }); it('multiple tools without output schema', () => { const simple = tool({ inputSchema: z.number(), }); const complex = tool({ inputSchema: z.object({ title: z.string(), description: z.string(), }), }); const tools = { simple, complex, }; expectTypeOf< ToolCallArgument<typeof tools> & { dynamic?: false } >().toMatchTypeOf< | { toolName: 'simple'; input: number; } | { toolName: 'complex'; input: { title: string; description: string; }; } >(); }); }); }); --- File: /ai/packages/ai/src/ui/chat.test.ts --- import { createTestServer, mockId, TestResponseController, } from '@ai-sdk/provider-utils/test'; import { createResolvablePromise } from '../util/create-resolvable-promise'; import { AbstractChat, ChatInit, ChatState, ChatStatus } from './chat'; import { UIMessage } from './ui-messages'; import { UIMessageChunk } from '../ui-message-stream/ui-message-chunks'; import { DefaultChatTransport } from './default-chat-transport'; import { lastAssistantMessageIsCompleteWithToolCalls } from './last-assistant-message-is-complete-with-tool-calls'; class TestChatState<UI_MESSAGE extends UIMessage> implements ChatState<UI_MESSAGE> { history: UI_MESSAGE[][] = []; status: ChatStatus = 'ready'; messages: UI_MESSAGE[]; error: Error | undefined = undefined; constructor(initialMessages: UI_MESSAGE[] = []) { this.messages = initialMessages; this.history.push(structuredClone(initialMessages)); } pushMessage = (message: UI_MESSAGE) => { this.messages = this.messages.concat(message); this.history.push(structuredClone(this.messages)); }; popMessage = () => { this.messages = this.messages.slice(0, -1); this.history.push(structuredClone(this.messages)); }; replaceMessage = (index: number, message: UI_MESSAGE) => { this.messages = [ ...this.messages.slice(0, index), message, ...this.messages.slice(index + 1), ]; this.history.push(structuredClone(this.messages)); }; snapshot = <T>(value: T): T => value; } class TestChat extends AbstractChat<UIMessage> { constructor(init: ChatInit<UIMessage>) { super({ ...init, state: new TestChatState(init.messages ?? []), }); } get history() { return (this.state as TestChatState<UIMessage>).history; } } function formatChunk(part: UIMessageChunk) { return `data: ${JSON.stringify(part)}\n\n`; } const server = createTestServer({ 'http://localhost:3000/api/chat': {}, }); describe('Chat', () => { describe('sendMessage', () => { it('should send a simple message', async () => { server.urls['http://localhost:3000/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'start' }), formatChunk({ type: 'start-step' }), formatChunk({ type: 'text-start', id: 'text-1' }), formatChunk({ type: 'text-delta', id: 'text-1', delta: 'Hello', }), formatChunk({ type: 'text-delta', id: 'text-1', delta: ',' }), formatChunk({ type: 'text-delta', id: 'text-1', delta: ' world', }), formatChunk({ type: 'text-delta', id: 'text-1', delta: '.' }), formatChunk({ type: 'text-end', id: 'text-1' }), formatChunk({ type: 'finish-step' }), formatChunk({ type: 'finish' }), ], }; const finishPromise = createResolvablePromise<void>(); const chat = new TestChat({ id: '123', generateId: mockId(), transport: new DefaultChatTransport({ api: 'http://localhost:3000/api/chat', }), onFinish: () => finishPromise.resolve(), }); chat.sendMessage({ text: 'Hello, world!', }); await finishPromise.promise; expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot( ` { "id": "123", "messages": [ { "id": "id-0", "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `, ); expect(chat.messages).toMatchInlineSnapshot(` [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "id-1", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Hello, world.", "type": "text", }, ], "role": "assistant", }, ] `); expect(chat.history).toMatchInlineSnapshot(` [ [], [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, ], [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "id-1", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, ], [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "id-1", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "Hello", "type": "text", }, ], "role": "assistant", }, ], [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "id-1", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "Hello,", "type": "text", }, ], "role": "assistant", }, ], [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "id-1", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "Hello, world", "type": "text", }, ], "role": "assistant", }, ], [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "id-1", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "Hello, world.", "type": "text", }, ], "role": "assistant", }, ], [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "id-1", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Hello, world.", "type": "text", }, ], "role": "assistant", }, ], ] `); }); it('should include the metadata of text message', async () => { server.urls['http://localhost:3000/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'start' }), formatChunk({ type: 'start-step' }), formatChunk({ type: 'text-start', id: 'text-1' }), formatChunk({ type: 'text-delta', id: 'text-1', delta: 'Hello, world.', }), formatChunk({ type: 'text-end', id: 'text-1' }), formatChunk({ type: 'finish-step' }), formatChunk({ type: 'finish' }), ], }; const finishPromise = createResolvablePromise<void>(); const chat = new TestChat({ id: '123', generateId: mockId(), transport: new DefaultChatTransport({ api: 'http://localhost:3000/api/chat', }), onFinish: () => finishPromise.resolve(), }); chat.sendMessage({ text: 'Hello, world!', metadata: { someData: true }, }); await finishPromise.promise; expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot( ` { "id": "123", "messages": [ { "id": "id-0", "metadata": { "someData": true, }, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `, ); expect(chat.messages).toMatchInlineSnapshot(` [ { "id": "id-0", "metadata": { "someData": true, }, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "id-1", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Hello, world.", "type": "text", }, ], "role": "assistant", }, ] `); expect(chat.history).toMatchInlineSnapshot(` [ [], [ { "id": "id-0", "metadata": { "someData": true, }, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, ], [ { "id": "id-0", "metadata": { "someData": true, }, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "id-1", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, ], [ { "id": "id-0", "metadata": { "someData": true, }, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "id-1", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "Hello, world.", "type": "text", }, ], "role": "assistant", }, ], [ { "id": "id-0", "metadata": { "someData": true, }, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "id-1", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Hello, world.", "type": "text", }, ], "role": "assistant", }, ], ] `); }); it('should replace an existing user message', async () => { server.urls['http://localhost:3000/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'start' }), formatChunk({ type: 'start-step' }), formatChunk({ type: 'text-start', id: 'text-1' }), formatChunk({ type: 'text-delta', id: 'text-1', delta: 'Hello', }), formatChunk({ type: 'text-delta', id: 'text-1', delta: ',' }), formatChunk({ type: 'text-delta', id: 'text-1', delta: ' world', }), formatChunk({ type: 'text-delta', id: 'text-1', delta: '.' }), formatChunk({ type: 'text-end', id: 'text-1' }), formatChunk({ type: 'finish-step' }), formatChunk({ type: 'finish' }), ], }; const finishPromise = createResolvablePromise<void>(); const chat = new TestChat({ id: '123', generateId: mockId({ prefix: 'newid' }), transport: new DefaultChatTransport({ api: 'http://localhost:3000/api/chat', }), onFinish: () => finishPromise.resolve(), messages: [ { id: 'id-0', role: 'user', parts: [{ text: 'Hi!', type: 'text' }], }, { id: 'id-1', role: 'assistant', parts: [ { text: 'How can I help you?', type: 'text', state: 'done' }, ], }, ], }); chat.sendMessage({ text: 'Hello, world!', messageId: 'id-0', }); await finishPromise.promise; expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot( ` { "id": "123", "messageId": "id-0", "messages": [ { "id": "id-0", "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `, ); expect(chat.messages).toMatchInlineSnapshot(` [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "newid-0", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Hello, world.", "type": "text", }, ], "role": "assistant", }, ] `); expect(chat.history).toMatchInlineSnapshot(` [ [ { "id": "id-0", "parts": [ { "text": "Hi!", "type": "text", }, ], "role": "user", }, { "id": "id-1", "parts": [ { "state": "done", "text": "How can I help you?", "type": "text", }, ], "role": "assistant", }, ], [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, ], [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "newid-0", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, ], [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "newid-0", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "Hello", "type": "text", }, ], "role": "assistant", }, ], [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "newid-0", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "Hello,", "type": "text", }, ], "role": "assistant", }, ], [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "newid-0", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "Hello, world", "type": "text", }, ], "role": "assistant", }, ], [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "newid-0", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "Hello, world.", "type": "text", }, ], "role": "assistant", }, ], [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "newid-0", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Hello, world.", "type": "text", }, ], "role": "assistant", }, ], ] `); }); it('should handle error parts', async () => { server.urls['http://localhost:3000/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'start' }), formatChunk({ type: 'error', errorText: 'test-error' }), ], }; const errorPromise = createResolvablePromise<void>(); const chat = new TestChat({ id: '123', generateId: mockId(), transport: new DefaultChatTransport({ api: 'http://localhost:3000/api/chat', }), onError: () => errorPromise.resolve(), }); chat.sendMessage({ text: 'Hello, world!', }); await errorPromise.promise; expect(chat.error).toMatchInlineSnapshot(`[Error: test-error]`); expect(chat.status).toBe('error'); }); }); describe('sendAutomaticallyWhen', () => { it('should delay tool result submission until the stream is finished', async () => { const controller1 = new TestResponseController(); server.urls['http://localhost:3000/api/chat'].response = [ { type: 'controlled-stream', controller: controller1 }, { type: 'stream-chunks', chunks: [formatChunk({ type: 'start' })] }, ]; const toolCallPromise = createResolvablePromise<void>(); const submitMessagePromise = createResolvablePromise<void>(); let callCount = 0; const chat = new TestChat({ id: '123', generateId: mockId(), transport: new DefaultChatTransport({ api: 'http://localhost:3000/api/chat', }), sendAutomaticallyWhen: () => callCount < 2, onToolCall: () => toolCallPromise.resolve(), onFinish: () => { callCount++; }, }); chat .sendMessage({ text: 'Hello, world!', }) .then(() => { submitMessagePromise.resolve(); }); // start stream controller1.write(formatChunk({ type: 'start' })); controller1.write(formatChunk({ type: 'start-step' })); // tool call controller1.write( formatChunk({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), ); await toolCallPromise.promise; // user submits the tool result await chat.addToolResult({ tool: 'test-tool', toolCallId: 'tool-call-0', output: 'test-result', }); // UI should show the tool result expect(chat.messages).toMatchInlineSnapshot(` [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "id-1", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "testArg": "test-value", }, "output": "test-result", "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-0", "type": "tool-test-tool", }, ], "role": "assistant", }, ] `); // should not have called the API yet expect(server.calls.length).toBe(1); // finish stream controller1.write(formatChunk({ type: 'finish-step' })); controller1.write(formatChunk({ type: 'finish' })); await controller1.close(); await submitMessagePromise.promise; // 2nd call should happen after the stream is finished expect(server.calls.length).toBe(2); // check details of the 2nd call expect(await server.calls[1].requestBodyJson).toMatchInlineSnapshot(` { "id": "123", "messageId": "id-1", "messages": [ { "id": "id-0", "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "id-1", "parts": [ { "type": "step-start", }, { "input": { "testArg": "test-value", }, "output": "test-result", "state": "output-available", "toolCallId": "tool-call-0", "type": "tool-test-tool", }, ], "role": "assistant", }, ], "trigger": "submit-message", } `); }); it('should send message when a tool result is submitted', async () => { server.urls['http://localhost:3000/api/chat'].response = [ { type: 'stream-chunks', chunks: [ formatChunk({ type: 'start' }), formatChunk({ type: 'start-step' }), formatChunk({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), formatChunk({ type: 'finish-step' }), formatChunk({ type: 'finish' }), ], }, { type: 'stream-chunks', chunks: [ formatChunk({ type: 'start' }), formatChunk({ type: 'start-step' }), formatChunk({ type: 'finish-step' }), formatChunk({ type: 'finish' }), ], }, ]; let callCount = 0; const onFinishPromise = createResolvablePromise<void>(); const chat = new TestChat({ id: '123', generateId: mockId(), transport: new DefaultChatTransport({ api: 'http://localhost:3000/api/chat', }), sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, onFinish: () => { callCount++; if (callCount === 2) { onFinishPromise.resolve(); } }, }); await chat.sendMessage({ text: 'Hello, world!', }); // user submits the tool result await chat.addToolResult({ tool: 'test-tool', toolCallId: 'tool-call-0', output: 'test-result', }); // UI should show the tool result expect(chat.messages).toMatchInlineSnapshot(` [ { "id": "id-0", "metadata": undefined, "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "id-1", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "testArg": "test-value", }, "output": "test-result", "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-0", "type": "tool-test-tool", }, ], "role": "assistant", }, ] `); await onFinishPromise.promise; // 2nd call should happen after the stream is finished expect(server.calls.length).toBe(2); // check details of the 2nd call expect(await server.calls[1].requestBodyJson).toMatchInlineSnapshot(` { "id": "123", "messageId": "id-1", "messages": [ { "id": "id-0", "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, { "id": "id-1", "parts": [ { "type": "step-start", }, { "input": { "testArg": "test-value", }, "output": "test-result", "state": "output-available", "toolCallId": "tool-call-0", "type": "tool-test-tool", }, ], "role": "assistant", }, ], "trigger": "submit-message", } `); }); }); describe('clearError', () => { it('should clear the error and set the status to ready', async () => { server.urls['http://localhost:3000/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'start' }), formatChunk({ type: 'error', errorText: 'test-error' }), ], }; const errorPromise = createResolvablePromise<void>(); const chat = new TestChat({ id: '123', generateId: mockId(), transport: new DefaultChatTransport({ api: 'http://localhost:3000/api/chat', }), onError: () => errorPromise.resolve(), }); chat.sendMessage({ text: 'Hello, world!', }); await errorPromise.promise; expect(chat.error).toMatchInlineSnapshot(`[Error: test-error]`); expect(chat.status).toBe('error'); chat.clearError(); expect(chat.error).toBeUndefined(); expect(chat.status).toBe('ready'); }); }); }); --- File: /ai/packages/ai/src/ui/chat.ts --- import { generateId as generateIdFunc, IdGenerator, StandardSchemaV1, Validator, } from '@ai-sdk/provider-utils'; import { UIMessageChunk } from '../ui-message-stream/ui-message-chunks'; import { consumeStream } from '../util/consume-stream'; import { SerialJobExecutor } from '../util/serial-job-executor'; import { ChatTransport } from './chat-transport'; import { convertFileListToFileUIParts } from './convert-file-list-to-file-ui-parts'; import { DefaultChatTransport } from './default-chat-transport'; import { createStreamingUIMessageState, processUIMessageStream, StreamingUIMessageState, } from './process-ui-message-stream'; import { InferUIMessageToolCall, isToolUIPart, type DataUIPart, type FileUIPart, type InferUIMessageData, type InferUIMessageMetadata, type InferUIMessageTools, type UIDataTypes, type UIMessage, } from './ui-messages'; export type CreateUIMessage<UI_MESSAGE extends UIMessage> = Omit< UI_MESSAGE, 'id' | 'role' > & { id?: UI_MESSAGE['id']; role?: UI_MESSAGE['role']; }; export type UIDataPartSchemas = Record< string, Validator<any> | StandardSchemaV1<any> >; export type UIDataTypesToSchemas<T extends UIDataTypes> = { [K in keyof T]: Validator<T[K]> | StandardSchemaV1<T[K]>; }; export type InferUIDataParts<T extends UIDataPartSchemas> = { [K in keyof T]: T[K] extends Validator<infer U> ? U : T[K] extends StandardSchemaV1<infer U> ? U : unknown; }; export type ChatRequestOptions = { /** Additional headers that should be to be passed to the API endpoint. */ headers?: Record<string, string> | Headers; /** Additional body JSON properties that should be sent to the API endpoint. */ body?: object; // TODO JSONStringifyable metadata?: unknown; }; export type ChatStatus = 'submitted' | 'streaming' | 'ready' | 'error'; type ActiveResponse<UI_MESSAGE extends UIMessage> = { state: StreamingUIMessageState<UI_MESSAGE>; abortController: AbortController; }; export interface ChatState<UI_MESSAGE extends UIMessage> { status: ChatStatus; error: Error | undefined; messages: UI_MESSAGE[]; pushMessage: (message: UI_MESSAGE) => void; popMessage: () => void; replaceMessage: (index: number, message: UI_MESSAGE) => void; snapshot: <T>(thing: T) => T; } export type ChatOnErrorCallback = (error: Error) => void; export type ChatOnToolCallCallback<UI_MESSAGE extends UIMessage = UIMessage> = (options: { toolCall: InferUIMessageToolCall<UI_MESSAGE>; }) => void | PromiseLike<void>; export type ChatOnDataCallback<UI_MESSAGE extends UIMessage> = ( dataPart: DataUIPart<InferUIMessageData<UI_MESSAGE>>, ) => void; export type ChatOnFinishCallback<UI_MESSAGE extends UIMessage> = (options: { message: UI_MESSAGE; }) => void; export interface ChatInit<UI_MESSAGE extends UIMessage> { /** * A unique identifier for the chat. If not provided, a random one will be * generated. */ id?: string; messageMetadataSchema?: | Validator<InferUIMessageMetadata<UI_MESSAGE>> | StandardSchemaV1<InferUIMessageMetadata<UI_MESSAGE>>; dataPartSchemas?: UIDataTypesToSchemas<InferUIMessageData<UI_MESSAGE>>; messages?: UI_MESSAGE[]; /** * A way to provide a function that is going to be used for ids for messages and the chat. * If not provided the default AI SDK `generateId` is used. */ generateId?: IdGenerator; transport?: ChatTransport<UI_MESSAGE>; /** * Callback function to be called when an error is encountered. */ onError?: ChatOnErrorCallback; /** Optional callback function that is invoked when a tool call is received. Intended for automatic client-side tool execution. You can optionally return a result for the tool call, either synchronously or asynchronously. */ onToolCall?: ChatOnToolCallCallback<UI_MESSAGE>; /** * Optional callback function that is called when the assistant message is finished streaming. * * @param message The message that was streamed. */ onFinish?: ChatOnFinishCallback<UI_MESSAGE>; /** * Optional callback function that is called when a data part is received. * * @param data The data part that was received. */ onData?: ChatOnDataCallback<UI_MESSAGE>; /** * When provided, this function will be called when the stream is finished or a tool call is added * to determine if the current messages should be resubmitted. */ sendAutomaticallyWhen?: (options: { messages: UI_MESSAGE[]; }) => boolean | PromiseLike<boolean>; } export abstract class AbstractChat<UI_MESSAGE extends UIMessage> { readonly id: string; readonly generateId: IdGenerator; protected state: ChatState<UI_MESSAGE>; private messageMetadataSchema: | Validator<InferUIMessageMetadata<UI_MESSAGE>> | StandardSchemaV1<InferUIMessageMetadata<UI_MESSAGE>> | undefined; private dataPartSchemas: | UIDataTypesToSchemas<InferUIMessageData<UI_MESSAGE>> | undefined; private readonly transport: ChatTransport<UI_MESSAGE>; private onError?: ChatInit<UI_MESSAGE>['onError']; private onToolCall?: ChatInit<UI_MESSAGE>['onToolCall']; private onFinish?: ChatInit<UI_MESSAGE>['onFinish']; private onData?: ChatInit<UI_MESSAGE>['onData']; private sendAutomaticallyWhen?: ChatInit<UI_MESSAGE>['sendAutomaticallyWhen']; private activeResponse: ActiveResponse<UI_MESSAGE> | undefined = undefined; private jobExecutor = new SerialJobExecutor(); constructor({ generateId = generateIdFunc, id = generateId(), transport = new DefaultChatTransport(), messageMetadataSchema, dataPartSchemas, state, onError, onToolCall, onFinish, onData, sendAutomaticallyWhen, }: Omit<ChatInit<UI_MESSAGE>, 'messages'> & { state: ChatState<UI_MESSAGE>; }) { this.id = id; this.transport = transport; this.generateId = generateId; this.messageMetadataSchema = messageMetadataSchema; this.dataPartSchemas = dataPartSchemas; this.state = state; this.onError = onError; this.onToolCall = onToolCall; this.onFinish = onFinish; this.onData = onData; this.sendAutomaticallyWhen = sendAutomaticallyWhen; } /** * Hook status: * * - `submitted`: The message has been sent to the API and we're awaiting the start of the response stream. * - `streaming`: The response is actively streaming in from the API, receiving chunks of data. * - `ready`: The full response has been received and processed; a new user message can be submitted. * - `error`: An error occurred during the API request, preventing successful completion. */ get status(): ChatStatus { return this.state.status; } protected setStatus({ status, error, }: { status: ChatStatus; error?: Error; }) { if (this.status === status) return; this.state.status = status; this.state.error = error; } get error() { return this.state.error; } get messages(): UI_MESSAGE[] { return this.state.messages; } get lastMessage(): UI_MESSAGE | undefined { return this.state.messages[this.state.messages.length - 1]; } set messages(messages: UI_MESSAGE[]) { this.state.messages = messages; } /** * Appends or replaces a user message to the chat list. This triggers the API call to fetch * the assistant's response. * * If a messageId is provided, the message will be replaced. */ sendMessage = async ( message?: | (CreateUIMessage<UI_MESSAGE> & { text?: never; files?: never; messageId?: string; }) | { text: string; files?: FileList | FileUIPart[]; metadata?: InferUIMessageMetadata<UI_MESSAGE>; parts?: never; messageId?: string; } | { files: FileList | FileUIPart[]; metadata?: InferUIMessageMetadata<UI_MESSAGE>; parts?: never; messageId?: string; }, options?: ChatRequestOptions, ): Promise<void> => { if (message == null) { await this.makeRequest({ trigger: 'submit-message', messageId: this.lastMessage?.id, ...options, }); return; } let uiMessage: CreateUIMessage<UI_MESSAGE>; if ('text' in message || 'files' in message) { const fileParts = Array.isArray(message.files) ? message.files : await convertFileListToFileUIParts(message.files); uiMessage = { parts: [ ...fileParts, ...('text' in message && message.text != null ? [{ type: 'text' as const, text: message.text }] : []), ], } as UI_MESSAGE; } else { uiMessage = message; } if (message.messageId != null) { const messageIndex = this.state.messages.findIndex( m => m.id === message.messageId, ); if (messageIndex === -1) { throw new Error(`message with id ${message.messageId} not found`); } if (this.state.messages[messageIndex].role !== 'user') { throw new Error( `message with id ${message.messageId} is not a user message`, ); } // remove all messages after the message with the given id this.state.messages = this.state.messages.slice(0, messageIndex + 1); // update the message with the new content this.state.replaceMessage(messageIndex, { ...uiMessage, id: message.messageId, role: uiMessage.role ?? 'user', metadata: message.metadata, } as UI_MESSAGE); } else { this.state.pushMessage({ ...uiMessage, id: uiMessage.id ?? this.generateId(), role: uiMessage.role ?? 'user', metadata: message.metadata, } as UI_MESSAGE); } await this.makeRequest({ trigger: 'submit-message', messageId: message.messageId, ...options, }); }; /** * Regenerate the assistant message with the provided message id. * If no message id is provided, the last assistant message will be regenerated. */ regenerate = async ({ messageId, ...options }: { messageId?: string; } & ChatRequestOptions = {}): Promise<void> => { const messageIndex = messageId == null ? this.state.messages.length - 1 : this.state.messages.findIndex(message => message.id === messageId); if (messageIndex === -1) { throw new Error(`message ${messageId} not found`); } // set the messages to the message before the assistant message this.state.messages = this.state.messages.slice( 0, // if the message is a user message, we need to include it in the request: this.messages[messageIndex].role === 'assistant' ? messageIndex : messageIndex + 1, ); await this.makeRequest({ trigger: 'regenerate-message', messageId, ...options, }); }; /** * Attempt to resume an ongoing streaming response. */ resumeStream = async (options: ChatRequestOptions = {}): Promise<void> => { await this.makeRequest({ trigger: 'resume-stream', ...options }); }; /** * Clear the error state and set the status to ready if the chat is in an error state. */ clearError = () => { if (this.status === 'error') { this.state.error = undefined; this.setStatus({ status: 'ready' }); } }; addToolResult = async <TOOL extends keyof InferUIMessageTools<UI_MESSAGE>>({ tool, toolCallId, output, }: { tool: TOOL; toolCallId: string; output: InferUIMessageTools<UI_MESSAGE>[TOOL]['output']; }) => this.jobExecutor.run(async () => { const messages = this.state.messages; const lastMessage = messages[messages.length - 1]; this.state.replaceMessage(messages.length - 1, { ...lastMessage, parts: lastMessage.parts.map(part => isToolUIPart(part) && part.toolCallId === toolCallId ? { ...part, state: 'output-available', output } : part, ), }); // update the active response if it exists if (this.activeResponse) { this.activeResponse.state.message.parts = this.activeResponse.state.message.parts.map(part => isToolUIPart(part) && part.toolCallId === toolCallId ? { ...part, state: 'output-available', output, errorText: undefined, } : part, ); } // automatically send the message if the sendAutomaticallyWhen function returns true if ( this.status !== 'streaming' && this.status !== 'submitted' && this.sendAutomaticallyWhen?.({ messages: this.state.messages }) ) { // no await to avoid deadlocking this.makeRequest({ trigger: 'submit-message', messageId: this.lastMessage?.id, }); } }); /** * Abort the current request immediately, keep the generated tokens if any. */ stop = async () => { if (this.status !== 'streaming' && this.status !== 'submitted') return; if (this.activeResponse?.abortController) { this.activeResponse.abortController.abort(); } }; private async makeRequest({ trigger, metadata, headers, body, messageId, }: { trigger: 'submit-message' | 'resume-stream' | 'regenerate-message'; messageId?: string; } & ChatRequestOptions) { this.setStatus({ status: 'submitted', error: undefined }); const lastMessage = this.lastMessage; try { const activeResponse = { state: createStreamingUIMessageState({ lastMessage: this.state.snapshot(lastMessage), messageId: this.generateId(), }), abortController: new AbortController(), } as ActiveResponse<UI_MESSAGE>; this.activeResponse = activeResponse; let stream: ReadableStream<UIMessageChunk>; if (trigger === 'resume-stream') { const reconnect = await this.transport.reconnectToStream({ chatId: this.id, metadata, headers, body, }); if (reconnect == null) { this.setStatus({ status: 'ready' }); return; // no active stream found, so we do not resume } stream = reconnect; } else { stream = await this.transport.sendMessages({ chatId: this.id, messages: this.state.messages, abortSignal: activeResponse.abortController.signal, metadata, headers, body, trigger, messageId, }); } const runUpdateMessageJob = ( job: (options: { state: StreamingUIMessageState<UI_MESSAGE>; write: () => void; }) => Promise<void>, ) => // serialize the job execution to avoid race conditions: this.jobExecutor.run(() => job({ state: activeResponse.state, write: () => { // streaming is set on first write (before it should be "submitted") this.setStatus({ status: 'streaming' }); const replaceLastMessage = activeResponse.state.message.id === this.lastMessage?.id; if (replaceLastMessage) { this.state.replaceMessage( this.state.messages.length - 1, activeResponse.state.message, ); } else { this.state.pushMessage(activeResponse.state.message); } }, }), ); await consumeStream({ stream: processUIMessageStream({ stream, onToolCall: this.onToolCall, onData: this.onData, messageMetadataSchema: this.messageMetadataSchema, dataPartSchemas: this.dataPartSchemas, runUpdateMessageJob, onError: error => { throw error; }, }), onError: error => { throw error; }, }); this.onFinish?.({ message: activeResponse.state.message }); this.setStatus({ status: 'ready' }); } catch (err) { // Ignore abort errors as they are expected. if ((err as any).name === 'AbortError') { this.setStatus({ status: 'ready' }); return null; } if (this.onError && err instanceof Error) { this.onError(err); } this.setStatus({ status: 'error', error: err as Error }); } finally { this.activeResponse = undefined; } // automatically send the message if the sendAutomaticallyWhen function returns true if (this.sendAutomaticallyWhen?.({ messages: this.state.messages })) { await this.makeRequest({ trigger: 'submit-message', messageId: this.lastMessage?.id, metadata, headers, body, }); } } } --- File: /ai/packages/ai/src/ui/convert-file-list-to-file-ui-parts.ts --- import { FileUIPart } from './ui-messages'; export async function convertFileListToFileUIParts( files: FileList | undefined, ): Promise<Array<FileUIPart>> { if (files == null) { return []; } // React-native doesn't have a FileList global: if (!globalThis.FileList || !(files instanceof globalThis.FileList)) { throw new Error('FileList is not supported in the current environment'); } return Promise.all( Array.from(files).map(async file => { const { name, type } = file; const dataUrl = await new Promise<string>((resolve, reject) => { const reader = new FileReader(); reader.onload = readerEvent => { resolve(readerEvent.target?.result as string); }; reader.onerror = error => reject(error); reader.readAsDataURL(file); }); return { type: 'file', mediaType: type, filename: name, url: dataUrl, }; }), ); } --- File: /ai/packages/ai/src/ui/convert-to-model-messages.test.ts --- import { ModelMessage } from '@ai-sdk/provider-utils'; import { convertToModelMessages } from './convert-to-model-messages'; describe('convertToModelMessages', () => { describe('system message', () => { it('should convert a simple system message', () => { const result = convertToModelMessages([ { role: 'system', parts: [{ text: 'System message', type: 'text' }], }, ]); expect(result).toEqual([{ role: 'system', content: 'System message' }]); }); it('should convert a system message with provider metadata', () => { const result = convertToModelMessages([ { role: 'system', parts: [ { text: 'System message with metadata', type: 'text', providerMetadata: { testProvider: { systemSignature: 'abc123' } }, }, ], }, ]); expect(result).toEqual([ { role: 'system', content: 'System message with metadata', providerOptions: { testProvider: { systemSignature: 'abc123' } }, }, ]); }); it('should merge provider metadata from multiple text parts in system message', () => { const result = convertToModelMessages([ { role: 'system', parts: [ { text: 'Part 1', type: 'text', providerMetadata: { provider1: { key1: 'value1' } }, }, { text: ' Part 2', type: 'text', providerMetadata: { provider2: { key2: 'value2' } }, }, ], }, ]); expect(result).toEqual([ { role: 'system', content: 'Part 1 Part 2', providerOptions: { provider1: { key1: 'value1' }, provider2: { key2: 'value2' }, }, }, ]); }); it('should convert a system message with Anthropic cache control metadata', () => { const SYSTEM_PROMPT = 'You are a helpful assistant.'; const systemMessage = { id: 'system', role: 'system' as const, parts: [ { type: 'text' as const, text: SYSTEM_PROMPT, providerMetadata: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, ], }; const result = convertToModelMessages([systemMessage]); expect(result).toEqual([ { role: 'system', content: SYSTEM_PROMPT, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, ]); }); }); describe('user message', () => { it('should convert a simple user message', () => { const result = convertToModelMessages([ { role: 'user', parts: [{ text: 'Hello, AI!', type: 'text' }], }, ]); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "text": "Hello, AI!", "type": "text", }, ], "role": "user", }, ] `); }); it('should handle user message file parts', () => { const result = convertToModelMessages([ { role: 'user', parts: [ { type: 'file', mediaType: 'image/jpeg', url: 'https://example.com/image.jpg', }, { type: 'text', text: 'Check this image' }, ], }, ]); expect(result).toEqual([ { role: 'user', content: [ { type: 'file', mediaType: 'image/jpeg', data: 'https://example.com/image.jpg', }, { type: 'text', text: 'Check this image' }, ], }, ]); }); }); describe('assistant message', () => { it('should convert a simple assistant text message', () => { const result = convertToModelMessages([ { role: 'assistant', parts: [{ type: 'text', text: 'Hello, human!', state: 'done' }], }, ]); expect(result).toEqual([ { role: 'assistant', content: [{ type: 'text', text: 'Hello, human!' }], }, ]); }); it('should convert a simple assistant text message with provider metadata', () => { const result = convertToModelMessages([ { role: 'assistant', parts: [ { type: 'text', text: 'Hello, human!', state: 'done', providerMetadata: { testProvider: { signature: '1234567890' } }, }, ], }, ]); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "providerOptions": { "testProvider": { "signature": "1234567890", }, }, "text": "Hello, human!", "type": "text", }, ], "role": "assistant", }, ] `); }); it('should convert an assistant message with reasoning', () => { const result = convertToModelMessages([ { role: 'assistant', parts: [ { type: 'reasoning', text: 'Thinking...', providerMetadata: { testProvider: { signature: '1234567890', }, }, state: 'done', }, { type: 'reasoning', text: 'redacted-data', providerMetadata: { testProvider: { isRedacted: true }, }, state: 'done', }, { type: 'text', text: 'Hello, human!', state: 'done' }, ], }, ]); expect(result).toEqual([ { role: 'assistant', content: [ { type: 'reasoning', text: 'Thinking...', providerOptions: { testProvider: { signature: '1234567890' } }, }, { type: 'reasoning', text: 'redacted-data', providerOptions: { testProvider: { isRedacted: true } }, }, { type: 'text', text: 'Hello, human!' }, ], }, ] satisfies ModelMessage[]); }); it('should convert an assistant message with file parts', () => { const result = convertToModelMessages([ { role: 'assistant', parts: [ { type: 'file', mediaType: 'image/png', url: 'data:image/png;base64,dGVzdA==', }, ], }, ]); expect(result).toEqual([ { role: 'assistant', content: [ { type: 'file', mediaType: 'image/png', data: 'data:image/png;base64,dGVzdA==', }, ], }, ] satisfies ModelMessage[]); }); it('should handle assistant message with tool output available', () => { const result = convertToModelMessages([ { role: 'assistant', parts: [ { type: 'step-start' }, { type: 'text', text: 'Let me calculate that for you.', state: 'done', }, { type: 'tool-calculator', state: 'output-available', toolCallId: 'call1', input: { operation: 'add', numbers: [1, 2] }, output: '3', callProviderMetadata: { testProvider: { signature: '1234567890', }, }, }, ], }, ]); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "text": "Let me calculate that for you.", "type": "text", }, { "input": { "numbers": [ 1, 2, ], "operation": "add", }, "providerExecuted": undefined, "providerOptions": { "testProvider": { "signature": "1234567890", }, }, "toolCallId": "call1", "toolName": "calculator", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "3", }, "toolCallId": "call1", "toolName": "calculator", "type": "tool-result", }, ], "role": "tool", }, ] `); }); describe('tool output error', () => { it('should handle assistant message with tool output error that has raw input', () => { const result = convertToModelMessages([ { role: 'assistant', parts: [ { type: 'step-start' }, { type: 'text', text: 'Let me calculate that for you.', state: 'done', }, { type: 'tool-calculator', state: 'output-error', toolCallId: 'call1', errorText: 'Error: Invalid input', input: undefined, rawInput: { operation: 'add', numbers: [1, 2] }, }, ], }, ]); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "text": "Let me calculate that for you.", "type": "text", }, { "input": { "numbers": [ 1, 2, ], "operation": "add", }, "providerExecuted": undefined, "toolCallId": "call1", "toolName": "calculator", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "error-text", "value": "Error: Invalid input", }, "toolCallId": "call1", "toolName": "calculator", "type": "tool-result", }, ], "role": "tool", }, ] `); }); it('should handle assistant message with tool output error that has no raw input', () => { const result = convertToModelMessages([ { role: 'assistant', parts: [ { type: 'step-start' }, { type: 'text', text: 'Let me calculate that for you.', state: 'done', }, { type: 'tool-calculator', state: 'output-error', toolCallId: 'call1', input: { operation: 'add', numbers: [1, 2] }, errorText: 'Error: Invalid input', }, ], }, ]); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "text": "Let me calculate that for you.", "type": "text", }, { "input": { "numbers": [ 1, 2, ], "operation": "add", }, "providerExecuted": undefined, "toolCallId": "call1", "toolName": "calculator", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "error-text", "value": "Error: Invalid input", }, "toolCallId": "call1", "toolName": "calculator", "type": "tool-result", }, ], "role": "tool", }, ] `); }); }); it('should handle assistant message with provider-executed tool output available', () => { const result = convertToModelMessages([ { role: 'assistant', parts: [ { type: 'step-start' }, { type: 'text', text: 'Let me calculate that for you.', state: 'done', }, { type: 'tool-calculator', state: 'output-available', toolCallId: 'call1', input: { operation: 'add', numbers: [1, 2] }, output: '3', providerExecuted: true, }, ], }, ]); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "text": "Let me calculate that for you.", "type": "text", }, { "input": { "numbers": [ 1, 2, ], "operation": "add", }, "providerExecuted": true, "toolCallId": "call1", "toolName": "calculator", "type": "tool-call", }, { "output": { "type": "text", "value": "3", }, "toolCallId": "call1", "toolName": "calculator", "type": "tool-result", }, ], "role": "assistant", }, ] `); }); it('should handle assistant message with provider-executed tool output error', () => { const result = convertToModelMessages([ { role: 'assistant', parts: [ { type: 'step-start' }, { type: 'text', text: 'Let me calculate that for you.', state: 'done', }, { type: 'tool-calculator', state: 'output-error', toolCallId: 'call1', input: { operation: 'add', numbers: [1, 2] }, errorText: 'Error: Invalid input', providerExecuted: true, }, ], }, ]); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "text": "Let me calculate that for you.", "type": "text", }, { "input": { "numbers": [ 1, 2, ], "operation": "add", }, "providerExecuted": true, "toolCallId": "call1", "toolName": "calculator", "type": "tool-call", }, { "output": { "type": "error-json", "value": "Error: Invalid input", }, "toolCallId": "call1", "toolName": "calculator", "type": "tool-result", }, ], "role": "assistant", }, ] `); }); it('should handle assistant message with tool invocations that have multi-part responses', () => { const result = convertToModelMessages([ { role: 'assistant', parts: [ { type: 'step-start' }, { type: 'text', text: 'Let me calculate that for you.', state: 'done', }, { type: 'tool-screenshot', state: 'output-available', toolCallId: 'call1', input: {}, output: 'imgbase64', }, ], }, ]); expect(result).toMatchSnapshot(); }); it('should handle conversation with an assistant message that has empty tool invocations', () => { const result = convertToModelMessages([ { role: 'user', parts: [{ type: 'text', text: 'text1' }], }, { role: 'assistant', parts: [{ type: 'text', text: 'text2', state: 'done' }], }, ]); expect(result).toMatchSnapshot(); }); it('should handle conversation with multiple tool invocations that have step information', () => { const result = convertToModelMessages([ { role: 'assistant', parts: [ { type: 'step-start' }, { type: 'text', text: 'response', state: 'done' }, { type: 'tool-screenshot', state: 'output-available', toolCallId: 'call-1', input: { value: 'value-1' }, output: 'result-1', }, { type: 'step-start' }, { type: 'tool-screenshot', state: 'output-available', toolCallId: 'call-2', input: { value: 'value-2' }, output: 'result-2', }, { type: 'tool-screenshot', state: 'output-available', toolCallId: 'call-3', input: { value: 'value-3' }, output: 'result-3', }, { type: 'step-start' }, { type: 'tool-screenshot', state: 'output-available', toolCallId: 'call-4', input: { value: 'value-4' }, output: 'result-4', }, ], }, ]); expect(result).toMatchSnapshot(); }); it('should handle conversation with mix of tool invocations and text', () => { const result = convertToModelMessages([ { role: 'assistant', parts: [ { type: 'step-start' }, { type: 'text', text: 'i am gonna use tool1', state: 'done' }, { type: 'tool-screenshot', state: 'output-available', toolCallId: 'call-1', input: { value: 'value-1' }, output: 'result-1', }, { type: 'step-start' }, { type: 'text', text: 'i am gonna use tool2 and tool3', state: 'done', }, { type: 'tool-screenshot', state: 'output-available', toolCallId: 'call-2', input: { value: 'value-2' }, output: 'result-2', }, { type: 'tool-screenshot', state: 'output-available', toolCallId: 'call-3', input: { value: 'value-3' }, output: 'result-3', }, { type: 'step-start' }, { type: 'tool-screenshot', state: 'output-available', toolCallId: 'call-4', input: { value: 'value-4' }, output: 'result-4', }, { type: 'step-start' }, { type: 'text', text: 'final response', state: 'done' }, ], }, ]); expect(result).toMatchSnapshot(); }); }); describe('multiple messages', () => { it('should handle a conversation with multiple messages', () => { const result = convertToModelMessages([ { role: 'user', parts: [{ type: 'text', text: "What's the weather like?" }], }, { role: 'assistant', parts: [ { type: 'text', text: "I'll check that for you.", state: 'done' }, ], }, { role: 'user', parts: [{ type: 'text', text: 'Thanks!' }], }, ]); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "text": "What's the weather like?", "type": "text", }, ], "role": "user", }, { "content": [ { "text": "I'll check that for you.", "type": "text", }, ], "role": "assistant", }, { "content": [ { "text": "Thanks!", "type": "text", }, ], "role": "user", }, ] `); }); it('should handle conversation with multiple tool invocations and user message at the end', () => { const result = convertToModelMessages([ { role: 'assistant', parts: [ { type: 'step-start' }, { type: 'tool-screenshot', state: 'output-available', toolCallId: 'call-1', input: { value: 'value-1' }, output: 'result-1', }, { type: 'step-start' }, { type: 'tool-screenshot', state: 'output-available', toolCallId: 'call-2', input: { value: 'value-2' }, output: 'result-2', }, { type: 'tool-screenshot', state: 'output-available', toolCallId: 'call-3', input: { value: 'value-3' }, output: 'result-3', }, { type: 'step-start' }, { type: 'tool-screenshot', state: 'output-available', toolCallId: 'call-4', input: { value: 'value-4' }, output: 'result-4', }, { type: 'step-start' }, { type: 'text', text: 'response', state: 'done' }, ], }, { role: 'user', parts: [{ type: 'text', text: 'Thanks!' }], }, ]); expect(result).toMatchSnapshot(); }); }); describe('error handling', () => { it('should throw an error for unhandled roles', () => { expect(() => { convertToModelMessages([ { role: 'unknown' as any, parts: [{ text: 'unknown role message', type: 'text' }], }, ]); }).toThrow('Unsupported role: unknown'); }); }); describe('when ignoring incomplete tool calls', () => { it('should handle conversation with multiple tool invocations and user message at the end', () => { const result = convertToModelMessages( [ { role: 'assistant', parts: [ { type: 'step-start' }, { type: 'tool-screenshot', state: 'output-available', toolCallId: 'call-1', input: { value: 'value-1' }, output: 'result-1', }, { type: 'step-start' }, { type: 'tool-screenshot', state: 'input-streaming', toolCallId: 'call-2', input: { value: 'value-2' }, }, { type: 'tool-screenshot', state: 'input-available', toolCallId: 'call-3', input: { value: 'value-3' }, }, { type: 'text', text: 'response', state: 'done' }, ], }, { role: 'user', parts: [{ type: 'text', text: 'Thanks!' }], }, ], { ignoreIncompleteToolCalls: true }, ); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "input": { "value": "value-1", }, "providerExecuted": undefined, "toolCallId": "call-1", "toolName": "screenshot", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result-1", }, "toolCallId": "call-1", "toolName": "screenshot", "type": "tool-result", }, ], "role": "tool", }, { "content": [ { "text": "response", "type": "text", }, ], "role": "assistant", }, { "content": [ { "text": "Thanks!", "type": "text", }, ], "role": "user", }, ] `); }); }); describe('when converting dynamic tool invocations', () => { it('should convert a dynamic tool invocation', () => { const result = convertToModelMessages( [ { role: 'assistant', parts: [ { type: 'step-start' }, { type: 'dynamic-tool', toolName: 'screenshot', state: 'output-available', toolCallId: 'call-1', input: { value: 'value-1' }, output: 'result-1', }, ], }, { role: 'user', parts: [{ type: 'text', text: 'Thanks!' }], }, ], { ignoreIncompleteToolCalls: true }, ); expect(result).toMatchInlineSnapshot(` [ { "content": [ { "input": { "value": "value-1", }, "toolCallId": "call-1", "toolName": "screenshot", "type": "tool-call", }, ], "role": "assistant", }, { "content": [ { "output": { "type": "text", "value": "result-1", }, "toolCallId": "call-1", "toolName": "screenshot", "type": "tool-result", }, ], "role": "tool", }, { "content": [ { "text": "Thanks!", "type": "text", }, ], "role": "user", }, ] `); }); }); }); --- File: /ai/packages/ai/src/ui/convert-to-model-messages.ts --- import { AssistantContent, ModelMessage, ToolResultPart, } from '@ai-sdk/provider-utils'; import { ToolSet } from '../generate-text/tool-set'; import { createToolModelOutput } from '../prompt/create-tool-model-output'; import { MessageConversionError } from '../prompt/message-conversion-error'; import { DynamicToolUIPart, FileUIPart, getToolName, isToolUIPart, ReasoningUIPart, TextUIPart, ToolUIPart, UIMessage, UITools, } from './ui-messages'; /** Converts an array of messages from useChat into an array of CoreMessages that can be used with the AI core functions (e.g. `streamText`). @param messages - The messages to convert. @param options.tools - The tools to use. @param options.ignoreIncompleteToolCalls - Whether to ignore incomplete tool calls. Default is `false`. */ export function convertToModelMessages( messages: Array<Omit<UIMessage, 'id'>>, options?: { tools?: ToolSet; ignoreIncompleteToolCalls?: boolean; }, ): ModelMessage[] { const modelMessages: ModelMessage[] = []; if (options?.ignoreIncompleteToolCalls) { messages = messages.map(message => ({ ...message, parts: message.parts.filter( part => !isToolUIPart(part) || (part.state !== 'input-streaming' && part.state !== 'input-available'), ), })); } for (const message of messages) { switch (message.role) { case 'system': { const textParts = message.parts.filter(part => part.type === 'text'); const providerMetadata = textParts.reduce((acc, part) => { if (part.providerMetadata != null) { return { ...acc, ...part.providerMetadata }; } return acc; }, {}); modelMessages.push({ role: 'system', content: textParts.map(part => part.text).join(''), ...(Object.keys(providerMetadata).length > 0 ? { providerOptions: providerMetadata } : {}), }); break; } case 'user': { modelMessages.push({ role: 'user', content: message.parts .filter( (part): part is TextUIPart | FileUIPart => part.type === 'text' || part.type === 'file', ) .map(part => { switch (part.type) { case 'text': return { type: 'text' as const, text: part.text, }; case 'file': return { type: 'file' as const, mediaType: part.mediaType, filename: part.filename, data: part.url, }; default: return part; } }), }); break; } case 'assistant': { if (message.parts != null) { let block: Array< | TextUIPart | ToolUIPart<UITools> | ReasoningUIPart | FileUIPart | DynamicToolUIPart > = []; function processBlock() { if (block.length === 0) { return; } const content: AssistantContent = []; for (const part of block) { if (part.type === 'text') { content.push({ type: 'text' as const, text: part.text, ...(part.providerMetadata != null ? { providerOptions: part.providerMetadata } : {}), }); } else if (part.type === 'file') { content.push({ type: 'file' as const, mediaType: part.mediaType, data: part.url, }); } else if (part.type === 'reasoning') { content.push({ type: 'reasoning' as const, text: part.text, providerOptions: part.providerMetadata, }); } else if (part.type === 'dynamic-tool') { const toolName = part.toolName; if (part.state === 'input-streaming') { throw new MessageConversionError({ originalMessage: message, message: `incomplete tool input is not supported: ${part.toolCallId}`, }); } else { content.push({ type: 'tool-call' as const, toolCallId: part.toolCallId, toolName, input: part.input, ...(part.callProviderMetadata != null ? { providerOptions: part.callProviderMetadata } : {}), }); } } else if (isToolUIPart(part)) { const toolName = getToolName(part); if (part.state === 'input-streaming') { throw new MessageConversionError({ originalMessage: message, message: `incomplete tool input is not supported: ${part.toolCallId}`, }); } else { content.push({ type: 'tool-call' as const, toolCallId: part.toolCallId, toolName, input: part.state === 'output-error' ? (part.input ?? part.rawInput) : part.input, providerExecuted: part.providerExecuted, ...(part.callProviderMetadata != null ? { providerOptions: part.callProviderMetadata } : {}), }); if ( part.providerExecuted === true && (part.state === 'output-available' || part.state === 'output-error') ) { content.push({ type: 'tool-result', toolCallId: part.toolCallId, toolName, output: createToolModelOutput({ output: part.state === 'output-error' ? part.errorText : part.output, tool: options?.tools?.[toolName], errorMode: part.state === 'output-error' ? 'json' : 'none', }), }); } } } else { const _exhaustiveCheck: never = part; throw new Error(`Unsupported part: ${_exhaustiveCheck}`); } } modelMessages.push({ role: 'assistant', content, }); // check if there are tool invocations with results in the block const toolParts = block.filter( part => (isToolUIPart(part) && part.providerExecuted !== true) || part.type === 'dynamic-tool', ) as (ToolUIPart<UITools> | DynamicToolUIPart)[]; // tool message with tool results if (toolParts.length > 0) { modelMessages.push({ role: 'tool', content: toolParts.map((toolPart): ToolResultPart => { switch (toolPart.state) { case 'output-error': case 'output-available': { const toolName = toolPart.type === 'dynamic-tool' ? toolPart.toolName : getToolName(toolPart); return { type: 'tool-result', toolCallId: toolPart.toolCallId, toolName, output: createToolModelOutput({ output: toolPart.state === 'output-error' ? toolPart.errorText : toolPart.output, tool: options?.tools?.[toolName], errorMode: toolPart.state === 'output-error' ? 'text' : 'none', }), }; } default: { throw new MessageConversionError({ originalMessage: message, message: `Unsupported tool part state: ${toolPart.state}`, }); } } }), }); } // updates for next block block = []; } for (const part of message.parts) { if ( part.type === 'text' || part.type === 'reasoning' || part.type === 'file' || part.type === 'dynamic-tool' || isToolUIPart(part) ) { block.push(part); } else if (part.type === 'step-start') { processBlock(); } } processBlock(); break; } break; } default: { const _exhaustiveCheck: never = message.role; throw new MessageConversionError({ originalMessage: message, message: `Unsupported role: ${_exhaustiveCheck}`, }); } } } return modelMessages; } /** @deprecated Use `convertToModelMessages` instead. */ // TODO remove in AI SDK 6 export const convertToCoreMessages = convertToModelMessages; --- File: /ai/packages/ai/src/ui/default-chat-transport.ts --- import { parseJsonEventStream, ParseResult } from '@ai-sdk/provider-utils'; import { UIMessageChunk, uiMessageChunkSchema, } from '../ui-message-stream/ui-message-chunks'; import { HttpChatTransport, HttpChatTransportInitOptions, } from './http-chat-transport'; import { UIMessage } from './ui-messages'; export class DefaultChatTransport< UI_MESSAGE extends UIMessage, > extends HttpChatTransport<UI_MESSAGE> { constructor(options: HttpChatTransportInitOptions<UI_MESSAGE> = {}) { super(options); } protected processResponseStream( stream: ReadableStream<Uint8Array<ArrayBufferLike>>, ): ReadableStream<UIMessageChunk> { return parseJsonEventStream({ stream, schema: uiMessageChunkSchema, }).pipeThrough( new TransformStream<ParseResult<UIMessageChunk>, UIMessageChunk>({ async transform(chunk, controller) { if (!chunk.success) { throw chunk.error; } controller.enqueue(chunk.value); }, }), ); } } --- File: /ai/packages/ai/src/ui/http-chat-transport.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { UIMessageChunk } from '../ui-message-stream/ui-message-chunks'; import { HttpChatTransport, HttpChatTransportInitOptions, } from './http-chat-transport'; import { UIMessage } from './ui-messages'; class MockHttpChatTransport extends HttpChatTransport<UIMessage> { constructor(options: HttpChatTransportInitOptions<UIMessage> = {}) { super(options); } protected processResponseStream( stream: ReadableStream<Uint8Array<ArrayBufferLike>>, ): ReadableStream<UIMessageChunk> { return new ReadableStream(); } } const server = createTestServer({ 'http://localhost/api/chat': {}, }); describe('HttpChatTransport', () => { describe('body', () => { it('should include the body in the request by default', async () => { server.urls['http://localhost/api/chat'].response = { type: 'stream-chunks', chunks: [], }; const transport = new MockHttpChatTransport({ api: 'http://localhost/api/chat', body: { someData: true }, }); await transport.sendMessages({ chatId: 'c123', messageId: 'm123', trigger: 'submit-message', messages: [ { id: 'm123', role: 'user', parts: [{ text: 'Hello, world!', type: 'text' }], }, ], abortSignal: new AbortController().signal, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "c123", "messageId": "m123", "messages": [ { "id": "m123", "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, ], "someData": true, "trigger": "submit-message", } `); }); it('should include the body in the request when a function is provided', async () => { server.urls['http://localhost/api/chat'].response = { type: 'stream-chunks', chunks: [], }; const transport = new MockHttpChatTransport({ api: 'http://localhost/api/chat', body: () => ({ someData: true }), }); await transport.sendMessages({ chatId: 'c123', messageId: 'm123', trigger: 'submit-message', messages: [ { id: 'm123', role: 'user', parts: [{ text: 'Hello, world!', type: 'text' }], }, ], abortSignal: new AbortController().signal, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "c123", "messageId": "m123", "messages": [ { "id": "m123", "parts": [ { "text": "Hello, world!", "type": "text", }, ], "role": "user", }, ], "someData": true, "trigger": "submit-message", } `); }); }); describe('headers', () => { it('should include headers in the request by default', async () => { server.urls['http://localhost/api/chat'].response = { type: 'stream-chunks', chunks: [], }; const transport = new MockHttpChatTransport({ api: 'http://localhost/api/chat', headers: { 'X-Test-Header': 'test-value' }, }); await transport.sendMessages({ chatId: 'c123', messageId: 'm123', trigger: 'submit-message', messages: [ { id: 'm123', role: 'user', parts: [{ text: 'Hello, world!', type: 'text' }], }, ], abortSignal: new AbortController().signal, }); expect(server.calls[0].requestHeaders['x-test-header']).toBe( 'test-value', ); }); it('should include headers in the request when a function is provided', async () => { server.urls['http://localhost/api/chat'].response = { type: 'stream-chunks', chunks: [], }; const transport = new MockHttpChatTransport({ api: 'http://localhost/api/chat', headers: () => ({ 'X-Test-Header': 'test-value-fn' }), }); await transport.sendMessages({ chatId: 'c123', messageId: 'm123', trigger: 'submit-message', messages: [ { id: 'm123', role: 'user', parts: [{ text: 'Hello, world!', type: 'text' }], }, ], abortSignal: new AbortController().signal, }); expect(server.calls[0].requestHeaders['x-test-header']).toBe( 'test-value-fn', ); }); }); }); --- File: /ai/packages/ai/src/ui/http-chat-transport.ts --- import { FetchFunction, Resolvable, resolve } from '@ai-sdk/provider-utils'; import { UIMessageChunk } from '../ui-message-stream/ui-message-chunks'; import { ChatTransport } from './chat-transport'; import { UIMessage } from './ui-messages'; export type PrepareSendMessagesRequest<UI_MESSAGE extends UIMessage> = ( options: { id: string; messages: UI_MESSAGE[]; requestMetadata: unknown; body: Record<string, any> | undefined; credentials: RequestCredentials | undefined; headers: HeadersInit | undefined; api: string; } & { trigger: 'submit-message' | 'regenerate-message'; messageId: string | undefined; }, ) => | { body: object; headers?: HeadersInit; credentials?: RequestCredentials; api?: string; } | PromiseLike<{ body: object; headers?: HeadersInit; credentials?: RequestCredentials; api?: string; }>; export type PrepareReconnectToStreamRequest = (options: { id: string; requestMetadata: unknown; body: Record<string, any> | undefined; credentials: RequestCredentials | undefined; headers: HeadersInit | undefined; api: string; }) => | { headers?: HeadersInit; credentials?: RequestCredentials; api?: string; } | PromiseLike<{ headers?: HeadersInit; credentials?: RequestCredentials; api?: string; }>; /** * Options for the `HttpChatTransport` class. * * @param UI_MESSAGE - The type of message to be used in the chat. */ export type HttpChatTransportInitOptions<UI_MESSAGE extends UIMessage> = { /** * The API URL to be used for the chat transport. * Defaults to '/api/chat'. */ api?: string; /** * The credentials mode to be used for the fetch request. * Possible values are: 'omit', 'same-origin', 'include'. * Defaults to 'same-origin'. */ credentials?: Resolvable<RequestCredentials>; /** * HTTP headers to be sent with the API request. */ headers?: Resolvable<Record<string, string> | Headers>; /** * Extra body object to be sent with the API request. * @example * Send a `sessionId` to the API along with the messages. * ```js * useChat({ * body: { * sessionId: '123', * } * }) * ``` */ body?: Resolvable<object>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; /** * When a function is provided, it will be used * to prepare the request body for the chat API. This can be useful for * customizing the request body based on the messages and data in the chat. * * @param id The id of the chat. * @param messages The current messages in the chat. * @param requestBody The request body object passed in the chat request. */ prepareSendMessagesRequest?: PrepareSendMessagesRequest<UI_MESSAGE>; /** * When a function is provided, it will be used * to prepare the request body for the chat API. This can be useful for * customizing the request body based on the messages and data in the chat. * * @param id The id of the chat. * @param messages The current messages in the chat. * @param requestBody The request body object passed in the chat request. */ prepareReconnectToStreamRequest?: PrepareReconnectToStreamRequest; }; export abstract class HttpChatTransport<UI_MESSAGE extends UIMessage> implements ChatTransport<UI_MESSAGE> { protected api: string; protected credentials: HttpChatTransportInitOptions<UI_MESSAGE>['credentials']; protected headers: HttpChatTransportInitOptions<UI_MESSAGE>['headers']; protected body: HttpChatTransportInitOptions<UI_MESSAGE>['body']; protected fetch?: FetchFunction; protected prepareSendMessagesRequest?: PrepareSendMessagesRequest<UI_MESSAGE>; protected prepareReconnectToStreamRequest?: PrepareReconnectToStreamRequest; constructor({ api = '/api/chat', credentials, headers, body, fetch, prepareSendMessagesRequest, prepareReconnectToStreamRequest, }: HttpChatTransportInitOptions<UI_MESSAGE>) { this.api = api; this.credentials = credentials; this.headers = headers; this.body = body; this.fetch = fetch; this.prepareSendMessagesRequest = prepareSendMessagesRequest; this.prepareReconnectToStreamRequest = prepareReconnectToStreamRequest; } async sendMessages({ abortSignal, ...options }: Parameters<ChatTransport<UI_MESSAGE>['sendMessages']>[0]) { const resolvedBody = await resolve(this.body); const resolvedHeaders = await resolve(this.headers); const resolvedCredentials = await resolve(this.credentials); const preparedRequest = await this.prepareSendMessagesRequest?.({ api: this.api, id: options.chatId, messages: options.messages, body: { ...resolvedBody, ...options.body }, headers: { ...resolvedHeaders, ...options.headers }, credentials: resolvedCredentials, requestMetadata: options.metadata, trigger: options.trigger, messageId: options.messageId, }); const api = preparedRequest?.api ?? this.api; const headers = preparedRequest?.headers !== undefined ? preparedRequest.headers : { ...resolvedHeaders, ...options.headers }; const body = preparedRequest?.body !== undefined ? preparedRequest.body : { ...resolvedBody, ...options.body, id: options.chatId, messages: options.messages, trigger: options.trigger, messageId: options.messageId, }; const credentials = preparedRequest?.credentials ?? resolvedCredentials; // avoid caching globalThis.fetch in case it is patched by other libraries const fetch = this.fetch ?? globalThis.fetch; const response = await fetch(api, { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers, }, body: JSON.stringify(body), credentials, signal: abortSignal, }); if (!response.ok) { throw new Error( (await response.text()) ?? 'Failed to fetch the chat response.', ); } if (!response.body) { throw new Error('The response body is empty.'); } return this.processResponseStream(response.body); } async reconnectToStream( options: Parameters<ChatTransport<UI_MESSAGE>['reconnectToStream']>[0], ): Promise<ReadableStream<UIMessageChunk> | null> { const resolvedBody = await resolve(this.body); const resolvedHeaders = await resolve(this.headers); const resolvedCredentials = await resolve(this.credentials); const preparedRequest = await this.prepareReconnectToStreamRequest?.({ api: this.api, id: options.chatId, body: { ...resolvedBody, ...options.body }, headers: { ...resolvedHeaders, ...options.headers }, credentials: resolvedCredentials, requestMetadata: options.metadata, }); const api = preparedRequest?.api ?? `${this.api}/${options.chatId}/stream`; const headers = preparedRequest?.headers !== undefined ? preparedRequest.headers : { ...resolvedHeaders, ...options.headers }; const credentials = preparedRequest?.credentials ?? resolvedCredentials; // avoid caching globalThis.fetch in case it is patched by other libraries const fetch = this.fetch ?? globalThis.fetch; const response = await fetch(api, { method: 'GET', headers, credentials, }); // no active stream found, so we do not resume if (response.status === 204) { return null; } if (!response.ok) { throw new Error( (await response.text()) ?? 'Failed to fetch the chat response.', ); } if (!response.body) { throw new Error('The response body is empty.'); } return this.processResponseStream(response.body); } protected abstract processResponseStream( stream: ReadableStream<Uint8Array<ArrayBufferLike>>, ): ReadableStream<UIMessageChunk>; } --- File: /ai/packages/ai/src/ui/index.ts --- export { callCompletionApi } from './call-completion-api'; export { AbstractChat, type ChatInit, type ChatOnDataCallback, type ChatOnErrorCallback, type ChatOnFinishCallback, type ChatOnToolCallCallback, type ChatRequestOptions, type ChatState, type ChatStatus, type CreateUIMessage, type InferUIDataParts, type UIDataPartSchemas, } from './chat'; export { type ChatTransport } from './chat-transport'; export { convertFileListToFileUIParts } from './convert-file-list-to-file-ui-parts'; export { convertToCoreMessages, convertToModelMessages, } from './convert-to-model-messages'; export { DefaultChatTransport } from './default-chat-transport'; export { HttpChatTransport, type HttpChatTransportInitOptions, type PrepareReconnectToStreamRequest, type PrepareSendMessagesRequest, } from './http-chat-transport'; export { lastAssistantMessageIsCompleteWithToolCalls } from './last-assistant-message-is-complete-with-tool-calls'; export { TextStreamChatTransport } from './text-stream-chat-transport'; export { getToolName, isToolUIPart, type DataUIPart, type DynamicToolUIPart, type FileUIPart, type InferUITool, type InferUITools, type ReasoningUIPart, type SourceDocumentUIPart, type SourceUrlUIPart, type StepStartUIPart, type TextUIPart, type ToolUIPart, type UIDataTypes, type UIMessage, type UIMessagePart, type UITool, type UITools, } from './ui-messages'; export { type CompletionRequestOptions, type UseCompletionOptions, } from './use-completion'; --- File: /ai/packages/ai/src/ui/last-assistant-message-is-complete-with-tool-calls.test.ts --- import { lastAssistantMessageIsCompleteWithToolCalls } from './last-assistant-message-is-complete-with-tool-calls'; describe('lastAssistantMessageIsCompleteWithToolCalls', () => { it('should return false if the last step of a multi-step sequency only has text', () => { expect( lastAssistantMessageIsCompleteWithToolCalls({ messages: [ { id: '1', role: 'assistant', parts: [ { type: 'step-start' }, { type: 'tool-getLocation', toolCallId: 'call_CuEdmzpx4ZldCkg5SVr3ikLz', state: 'output-available', input: {}, output: 'New York', }, { type: 'step-start' }, { type: 'text', text: 'The current weather in New York is windy.', state: 'done', }, ], }, ], }), ).toBe(false); }); it('should return true when there is a text part after the last tool result in the last step', () => { expect( lastAssistantMessageIsCompleteWithToolCalls({ messages: [ { id: '1', role: 'assistant', parts: [ { type: 'step-start' }, { type: 'tool-getWeatherInformation', toolCallId: 'call_6iy0GxZ9R4VDI5MKohXxV48y', state: 'output-available', input: { city: 'New York', }, output: 'windy', }, { type: 'text', text: 'The current weather in New York is windy.', state: 'done', }, ], }, ], }), ).toBe(true); }); it('should return true when dynamic tool call is complete', () => { expect( lastAssistantMessageIsCompleteWithToolCalls({ messages: [ { id: '1', role: 'assistant', parts: [ { type: 'step-start' }, { type: 'dynamic-tool', toolName: 'getDynamicWeather', toolCallId: 'call_dynamic_123', state: 'output-available', input: { location: 'San Francisco', }, output: 'sunny', }, ], }, ], }), ).toBe(true); }); it('should return false when dynamic tool call is still streaming input', () => { expect( lastAssistantMessageIsCompleteWithToolCalls({ messages: [ { id: '1', role: 'assistant', parts: [ { type: 'step-start' }, { type: 'dynamic-tool', toolName: 'getDynamicWeather', toolCallId: 'call_dynamic_123', state: 'input-streaming', input: { location: 'San Francisco', }, }, ], }, ], }), ).toBe(false); }); it('should return false when dynamic tool call has input but no output', () => { expect( lastAssistantMessageIsCompleteWithToolCalls({ messages: [ { id: '1', role: 'assistant', parts: [ { type: 'step-start' }, { type: 'dynamic-tool', toolName: 'getDynamicWeather', toolCallId: 'call_dynamic_123', state: 'input-available', input: { location: 'San Francisco', }, }, ], }, ], }), ).toBe(false); }); it('should return false when dynamic tool call has an error', () => { expect( lastAssistantMessageIsCompleteWithToolCalls({ messages: [ { id: '1', role: 'assistant', parts: [ { type: 'step-start' }, { type: 'dynamic-tool', toolName: 'getDynamicWeather', toolCallId: 'call_dynamic_123', state: 'output-error', input: { location: 'San Francisco', }, errorText: 'Failed to fetch weather data', }, ], }, ], }), ).toBe(false); }); it('should return true when mixing regular and dynamic tool calls and all are complete', () => { expect( lastAssistantMessageIsCompleteWithToolCalls({ messages: [ { id: '1', role: 'assistant', parts: [ { type: 'step-start' }, { type: 'tool-getWeatherInformation', toolCallId: 'call_regular_123', state: 'output-available', input: { city: 'New York', }, output: 'windy', }, { type: 'dynamic-tool', toolName: 'getDynamicWeather', toolCallId: 'call_dynamic_123', state: 'output-available', input: { location: 'San Francisco', }, output: 'sunny', }, ], }, ], }), ).toBe(true); }); it('should return false when mixing regular and dynamic tool calls and some are incomplete', () => { expect( lastAssistantMessageIsCompleteWithToolCalls({ messages: [ { id: '1', role: 'assistant', parts: [ { type: 'step-start' }, { type: 'tool-getWeatherInformation', toolCallId: 'call_regular_123', state: 'output-available', input: { city: 'New York', }, output: 'windy', }, { type: 'dynamic-tool', toolName: 'getDynamicWeather', toolCallId: 'call_dynamic_123', state: 'input-available', // incomplete input: { location: 'San Francisco', }, }, ], }, ], }), ).toBe(false); }); it('should return true for multi-step sequence where last step has complete dynamic tool calls', () => { expect( lastAssistantMessageIsCompleteWithToolCalls({ messages: [ { id: '1', role: 'assistant', parts: [ // First step with regular tool { type: 'step-start' }, { type: 'tool-getLocation', toolCallId: 'call_location_123', state: 'output-available', input: {}, output: 'New York', }, // Second step with dynamic tool { type: 'step-start' }, { type: 'dynamic-tool', toolName: 'getDynamicWeather', toolCallId: 'call_dynamic_456', state: 'output-available', input: { location: 'New York', }, output: 'cloudy', }, { type: 'text', text: 'The current weather in New York is cloudy.', state: 'done', }, ], }, ], }), ).toBe(true); }); it('should return false for multi-step sequence where last step has incomplete dynamic tool calls', () => { expect( lastAssistantMessageIsCompleteWithToolCalls({ messages: [ { id: '1', role: 'assistant', parts: [ // First step with regular tool { type: 'step-start' }, { type: 'tool-getLocation', toolCallId: 'call_location_123', state: 'output-available', input: {}, output: 'New York', }, // Second step with incomplete dynamic tool { type: 'step-start' }, { type: 'dynamic-tool', toolName: 'getDynamicWeather', toolCallId: 'call_dynamic_456', state: 'input-streaming', // incomplete input: { location: 'New York', }, }, ], }, ], }), ).toBe(false); }); }); --- File: /ai/packages/ai/src/ui/last-assistant-message-is-complete-with-tool-calls.ts --- import { isToolUIPart, UIMessage } from './ui-messages'; /** Check if the message is an assistant message with completed tool calls. The last step of the message must have at least one tool invocation and all tool invocations must have a result. */ export function lastAssistantMessageIsCompleteWithToolCalls({ messages, }: { messages: UIMessage[]; }): boolean { const message = messages[messages.length - 1]; if (!message) { return false; } if (message.role !== 'assistant') { return false; } const lastStepStartIndex = message.parts.reduce((lastIndex, part, index) => { return part.type === 'step-start' ? index : lastIndex; }, -1); const lastStepToolInvocations = message.parts .slice(lastStepStartIndex + 1) .filter(part => isToolUIPart(part) || part.type === 'dynamic-tool'); return ( lastStepToolInvocations.length > 0 && lastStepToolInvocations.every(part => part.state === 'output-available') ); } --- File: /ai/packages/ai/src/ui/process-text-stream.test.ts --- import { convertArrayToReadableStream } from '@ai-sdk/provider-utils/test'; import { processTextStream } from './process-text-stream'; describe('processTextStream', () => { it('should process stream chunks correctly', async () => { // Mock data const testData = ['Hello', ' ', 'World']; const chunks: string[] = []; // Create stream using utility const encoder = new TextEncoder(); const stream = convertArrayToReadableStream( testData.map(chunk => encoder.encode(chunk)), ); // Mock callback function const onChunk = vi.fn((chunk: string) => { chunks.push(chunk); }); // Process the stream await processTextStream({ stream, onTextPart: onChunk }); // Verify the results expect(onChunk).toHaveBeenCalledTimes(3); expect(chunks).toEqual(testData); }); it('should handle empty streams', async () => { const onChunk = vi.fn(); const stream = convertArrayToReadableStream<Uint8Array>([]); await processTextStream({ stream, onTextPart: onChunk }); expect(onChunk).not.toHaveBeenCalled(); }); }); --- File: /ai/packages/ai/src/ui/process-text-stream.ts --- export async function processTextStream({ stream, onTextPart, }: { stream: ReadableStream<Uint8Array>; onTextPart: (chunk: string) => Promise<void> | void; }): Promise<void> { const reader = stream.pipeThrough(new TextDecoderStream()).getReader(); while (true) { const { done, value } = await reader.read(); if (done) { break; } await onTextPart(value); } } --- File: /ai/packages/ai/src/ui/process-ui-message-stream.test.ts --- import { convertArrayToReadableStream } from '@ai-sdk/provider-utils/test'; import { UIMessageChunk } from '../ui-message-stream/ui-message-chunks'; import { consumeStream } from '../util/consume-stream'; import { createStreamingUIMessageState, processUIMessageStream, StreamingUIMessageState, } from './process-ui-message-stream'; import { InferUIMessageData, UIMessage } from './ui-messages'; function createUIMessageStream(parts: UIMessageChunk[]) { return convertArrayToReadableStream(parts); } describe('processUIMessageStream', () => { let writeCalls: Array<{ message: UIMessage }> = []; let state: StreamingUIMessageState<UIMessage> | undefined; beforeEach(() => { writeCalls = []; state = undefined; }); const runUpdateMessageJob = async ( job: (options: { state: StreamingUIMessageState<UIMessage>; write: () => void; }) => Promise<void>, ) => { await job({ state: state!, write: () => { writeCalls.push({ message: structuredClone(state!.message) }); }, }); }; describe('text', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'Hello, ' }, { type: 'text-delta', id: 'text-1', delta: 'world!' }, { type: 'text-end', id: 'text-1' }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "Hello, ", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Hello, world!", "type": "text", }, ], "role": "assistant", } `); }); }); describe('errors', () => { let errors: Array<unknown>; beforeEach(async () => { errors = []; const stream = createUIMessageStream([ { type: 'error', errorText: 'test error' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { errors.push(error); }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(`[]`); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", } `); }); it('should call the onError function with the correct arguments', async () => { expect(errors).toMatchInlineSnapshot(` [ [Error: test error], ] `); }); }); describe('server-side tool roundtrip', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'tool-input-available', toolCallId: 'tool-call-id', toolName: 'tool-name', input: { city: 'London' }, }, { type: 'tool-output-available', toolCallId: 'tool-call-id', output: { weather: 'sunny' }, }, { type: 'finish-step' }, { type: 'start-step' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'The weather in London is sunny.', }, { type: 'text-end', id: 'text-1' }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "city": "London", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "input-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", } `); }); }); describe('server-side tool roundtrip with existing assistant message', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'tool-input-available', toolCallId: 'tool-call-id', toolName: 'tool-name', input: { city: 'London' }, }, { type: 'tool-output-available', toolCallId: 'tool-call-id', output: { weather: 'sunny' }, }, { type: 'finish-step' }, { type: 'start-step' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'The weather in London is sunny.', }, { type: 'text-end', id: 'text-1' }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: { role: 'assistant', id: 'original-id', metadata: undefined, parts: [ { type: 'tool-tool-name-original', toolCallId: 'tool-call-id-original', state: 'output-available', input: {}, output: { location: 'Berlin' }, }, ], }, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "input": {}, "output": { "location": "Berlin", }, "state": "output-available", "toolCallId": "tool-call-id-original", "type": "tool-tool-name-original", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "input": {}, "output": { "location": "Berlin", }, "state": "output-available", "toolCallId": "tool-call-id-original", "type": "tool-tool-name-original", }, { "type": "step-start", }, { "errorText": undefined, "input": { "city": "London", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "input-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "input": {}, "output": { "location": "Berlin", }, "state": "output-available", "toolCallId": "tool-call-id-original", "type": "tool-tool-name-original", }, { "type": "step-start", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "input": {}, "output": { "location": "Berlin", }, "state": "output-available", "toolCallId": "tool-call-id-original", "type": "tool-tool-name-original", }, { "type": "step-start", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "input": {}, "output": { "location": "Berlin", }, "state": "output-available", "toolCallId": "tool-call-id-original", "type": "tool-tool-name-original", }, { "type": "step-start", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "input": {}, "output": { "location": "Berlin", }, "state": "output-available", "toolCallId": "tool-call-id-original", "type": "tool-tool-name-original", }, { "type": "step-start", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [ { "input": {}, "output": { "location": "Berlin", }, "state": "output-available", "toolCallId": "tool-call-id-original", "type": "tool-tool-name-original", }, { "type": "step-start", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", } `); }); }); describe('server-side tool roundtrip with multiple assistant texts', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'I will ' }, { type: 'text-delta', id: 'text-1', delta: 'use a tool to get the weather in London.', }, { type: 'text-end', id: 'text-1' }, { type: 'tool-input-available', toolCallId: 'tool-call-id', toolName: 'tool-name', input: { city: 'London' }, }, { type: 'tool-output-available', toolCallId: 'tool-call-id', output: { weather: 'sunny' }, }, { type: 'finish-step' }, { type: 'start-step' }, { type: 'text-start', id: 'text-2' }, { type: 'text-delta', id: 'text-2', delta: 'The weather in London ' }, { type: 'text-delta', id: 'text-2', delta: 'is sunny.' }, { type: 'text-end', id: 'text-2' }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "I will ", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "I will use a tool to get the weather in London.", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "text", }, { "errorText": undefined, "input": { "city": "London", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "input-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "text", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "text", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "text", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "The weather in London ", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "text", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "text", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "text", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", } `); }); }); describe('server-side tool roundtrip with multiple assistant reasoning', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'reasoning-start', id: 'reasoning-1' }, { type: 'reasoning-delta', id: 'reasoning-1', delta: 'I will ', providerMetadata: { testProvider: { signature: '1234567890' }, }, }, { type: 'reasoning-delta', id: 'reasoning-1', delta: 'use a tool to get the weather in London.', }, { type: 'reasoning-end', id: 'reasoning-1' }, { type: 'tool-input-available', toolCallId: 'tool-call-id', toolName: 'tool-name', input: { city: 'London' }, }, { type: 'tool-output-available', toolCallId: 'tool-call-id', output: { weather: 'sunny' }, }, { type: 'finish-step' }, { type: 'start-step' }, { type: 'reasoning-start', id: 'reasoning-2' }, { type: 'reasoning-delta', id: 'reasoning-2', delta: 'I now know the weather in London.', providerMetadata: { testProvider: { signature: 'abc123' }, }, }, { type: 'reasoning-end', id: 'reasoning-2' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'The weather in London is sunny.', }, { type: 'text-end', id: 'text-1' }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "streaming", "text": "I will ", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "streaming", "text": "I will use a tool to get the weather in London.", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "reasoning", }, { "errorText": undefined, "input": { "city": "London", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "input-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "reasoning", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "reasoning", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "reasoning", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "abc123", }, }, "state": "streaming", "text": "I now know the weather in London.", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "reasoning", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "abc123", }, }, "state": "done", "text": "I now know the weather in London.", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "reasoning", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "abc123", }, }, "state": "done", "text": "I now know the weather in London.", "type": "reasoning", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "reasoning", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "abc123", }, }, "state": "done", "text": "I now know the weather in London.", "type": "reasoning", }, { "providerMetadata": undefined, "state": "streaming", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "reasoning", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "abc123", }, }, "state": "done", "text": "I now know the weather in London.", "type": "reasoning", }, { "providerMetadata": undefined, "state": "done", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will use a tool to get the weather in London.", "type": "reasoning", }, { "errorText": undefined, "input": { "city": "London", }, "output": { "weather": "sunny", }, "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "abc123", }, }, "state": "done", "text": "I now know the weather in London.", "type": "reasoning", }, { "providerMetadata": undefined, "state": "done", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", } `); }); }); describe('server-side tool roundtrip with output-error', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'tool-input-available', toolCallId: 'tool-call-id', toolName: 'tool-name', input: { city: 'London' }, }, { type: 'tool-output-error', toolCallId: 'tool-call-id', errorText: 'error-text', }, { type: 'finish-step' }, { type: 'start-step' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'The weather in London is sunny.', }, { type: 'text-end', id: 'text-1' }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "city": "London", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "input-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": "error-text", "input": { "city": "London", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "output-error", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": "error-text", "input": { "city": "London", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "output-error", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": "error-text", "input": { "city": "London", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "output-error", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": "error-text", "input": { "city": "London", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "output-error", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": "error-text", "input": { "city": "London", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "output-error", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", } `); }); }); describe('message metadata', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123', messageMetadata: { start: 'start-1', shared: { key1: 'value-1a', key2: 'value-2a', }, }, }, { type: 'start-step' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 't1' }, { type: 'message-metadata', messageMetadata: { metadata: 'metadata-1', }, }, { type: 'text-delta', id: 'text-1', delta: 't2' }, { type: 'text-end', id: 'text-1' }, { type: 'finish-step' }, { type: 'finish', messageMetadata: { finish: 'finish-1', shared: { key1: 'value-1e', key6: 'value-6e', }, }, }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": { "shared": { "key1": "value-1a", "key2": "value-2a", }, "start": "start-1", }, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": { "shared": { "key1": "value-1a", "key2": "value-2a", }, "start": "start-1", }, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": { "shared": { "key1": "value-1a", "key2": "value-2a", }, "start": "start-1", }, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "t1", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": { "metadata": "metadata-1", "shared": { "key1": "value-1a", "key2": "value-2a", }, "start": "start-1", }, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "t1", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": { "metadata": "metadata-1", "shared": { "key1": "value-1a", "key2": "value-2a", }, "start": "start-1", }, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "t1t2", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": { "metadata": "metadata-1", "shared": { "key1": "value-1a", "key2": "value-2a", }, "start": "start-1", }, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "t1t2", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": { "finish": "finish-1", "metadata": "metadata-1", "shared": { "key1": "value-1e", "key2": "value-2a", "key6": "value-6e", }, "start": "start-1", }, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "t1t2", "type": "text", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": { "finish": "finish-1", "metadata": "metadata-1", "shared": { "key1": "value-1e", "key2": "value-2a", "key6": "value-6e", }, "start": "start-1", }, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "t1t2", "type": "text", }, ], "role": "assistant", } `); }); }); describe('message metadata delayed after finish', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 't1' }, { type: 'text-end', id: 'text-1' }, { type: 'finish-step' }, { type: 'finish' }, { type: 'message-metadata', messageMetadata: { key1: 'value-1', }, }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "t1", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "t1", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": { "key1": "value-1", }, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "t1", "type": "text", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": { "key1": "value-1", }, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "t1", "type": "text", }, ], "role": "assistant", } `); }); }); describe('message metadata with existing assistant lastMessage', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123', messageMetadata: { key1: 'value-1b', key2: 'value-2b', }, }, { type: 'start-step' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 't1' }, { type: 'text-end', id: 'text-1' }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: { role: 'assistant', id: 'original-id', metadata: { key1: 'value-1a', key3: 'value-3a', }, parts: [], }, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": { "key1": "value-1b", "key2": "value-2b", "key3": "value-3a", }, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": { "key1": "value-1b", "key2": "value-2b", "key3": "value-3a", }, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": { "key1": "value-1b", "key2": "value-2b", "key3": "value-3a", }, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "t1", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": { "key1": "value-1b", "key2": "value-2b", "key3": "value-3a", }, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "t1", "type": "text", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": { "key1": "value-1b", "key2": "value-2b", "key3": "value-3a", }, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "t1", "type": "text", }, ], "role": "assistant", } `); }); }); describe('tool call streaming', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'tool-input-start', toolCallId: 'tool-call-0', toolName: 'test-tool', }, { type: 'tool-input-delta', toolCallId: 'tool-call-0', inputTextDelta: '{"testArg":"t', }, { type: 'tool-input-delta', toolCallId: 'tool-call-0', inputTextDelta: 'est-value"}}', }, { type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }, { type: 'tool-output-available', toolCallId: 'tool-call-0', output: 'test-result', }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": undefined, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "input-streaming", "toolCallId": "tool-call-0", "type": "tool-test-tool", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "testArg": "t", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "input-streaming", "toolCallId": "tool-call-0", "type": "tool-test-tool", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "testArg": "test-value", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "input-streaming", "toolCallId": "tool-call-0", "type": "tool-test-tool", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "testArg": "test-value", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "input-available", "toolCallId": "tool-call-0", "type": "tool-test-tool", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "testArg": "test-value", }, "output": "test-result", "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-0", "type": "tool-test-tool", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "testArg": "test-value", }, "output": "test-result", "providerExecuted": undefined, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-0", "type": "tool-test-tool", }, ], "role": "assistant", } `); }); }); describe('start with message id', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'Hello, ' }, { type: 'text-delta', id: 'text-1', delta: 'world!' }, { type: 'text-end', id: 'text-1' }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "Hello, ", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Hello, world!", "type": "text", }, ], "role": "assistant", } `); }); }); describe('reasoning', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'reasoning-start', id: 'reasoning-1' }, { type: 'reasoning-delta', id: 'reasoning-1', delta: 'I will open the conversation', }, { type: 'reasoning-delta', id: 'reasoning-1', delta: ' with witty banter. ', providerMetadata: { testProvider: { signature: '1234567890' }, }, }, { type: 'reasoning-end', id: 'reasoning-1' }, { type: 'reasoning-start', id: 'reasoning-2' }, { type: 'reasoning-delta', id: 'reasoning-2', delta: 'redacted-data', providerMetadata: { testProvider: { isRedacted: true }, }, }, { type: 'reasoning-end', id: 'reasoning-2' }, { type: 'reasoning-start', id: 'reasoning-3' }, { type: 'reasoning-delta', id: 'reasoning-3', delta: 'Once the user has relaxed,', }, { type: 'reasoning-delta', id: 'reasoning-3', delta: ' I will pry for valuable information.', providerMetadata: { testProvider: { signature: 'abc123' }, }, }, { type: 'reasoning-end', id: 'reasoning-3' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'Hi there!' }, { type: 'text-end', id: 'text-1' }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "I will open the conversation", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "streaming", "text": "I will open the conversation with witty banter. ", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will open the conversation with witty banter. ", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will open the conversation with witty banter. ", "type": "reasoning", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will open the conversation with witty banter. ", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "isRedacted": true, }, }, "state": "streaming", "text": "redacted-data", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will open the conversation with witty banter. ", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "isRedacted": true, }, }, "state": "done", "text": "redacted-data", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will open the conversation with witty banter. ", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "isRedacted": true, }, }, "state": "done", "text": "redacted-data", "type": "reasoning", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will open the conversation with witty banter. ", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "isRedacted": true, }, }, "state": "done", "text": "redacted-data", "type": "reasoning", }, { "providerMetadata": undefined, "state": "streaming", "text": "Once the user has relaxed,", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will open the conversation with witty banter. ", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "isRedacted": true, }, }, "state": "done", "text": "redacted-data", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "signature": "abc123", }, }, "state": "streaming", "text": "Once the user has relaxed, I will pry for valuable information.", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will open the conversation with witty banter. ", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "isRedacted": true, }, }, "state": "done", "text": "redacted-data", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "signature": "abc123", }, }, "state": "done", "text": "Once the user has relaxed, I will pry for valuable information.", "type": "reasoning", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will open the conversation with witty banter. ", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "isRedacted": true, }, }, "state": "done", "text": "redacted-data", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "signature": "abc123", }, }, "state": "done", "text": "Once the user has relaxed, I will pry for valuable information.", "type": "reasoning", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will open the conversation with witty banter. ", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "isRedacted": true, }, }, "state": "done", "text": "redacted-data", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "signature": "abc123", }, }, "state": "done", "text": "Once the user has relaxed, I will pry for valuable information.", "type": "reasoning", }, { "providerMetadata": undefined, "state": "streaming", "text": "Hi there!", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will open the conversation with witty banter. ", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "isRedacted": true, }, }, "state": "done", "text": "redacted-data", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "signature": "abc123", }, }, "state": "done", "text": "Once the user has relaxed, I will pry for valuable information.", "type": "reasoning", }, { "providerMetadata": undefined, "state": "done", "text": "Hi there!", "type": "text", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "state": "done", "text": "I will open the conversation with witty banter. ", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "isRedacted": true, }, }, "state": "done", "text": "redacted-data", "type": "reasoning", }, { "providerMetadata": { "testProvider": { "signature": "abc123", }, }, "state": "done", "text": "Once the user has relaxed, I will pry for valuable information.", "type": "reasoning", }, { "providerMetadata": undefined, "state": "done", "text": "Hi there!", "type": "text", }, ], "role": "assistant", } `); }); }); describe('onToolCall is executed', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'tool-input-available', toolCallId: 'tool-call-id', toolName: 'tool-name', input: { city: 'London' }, }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onToolCall: vi.fn().mockResolvedValue('test-result'), onError: error => { throw error; }, }), }); }); it('should call the update function twice with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "city": "London", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "input-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "city": "London", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "input-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, ], "role": "assistant", } `); }); }); describe('sources', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'The weather in London is sunny.', }, { type: 'text-end', id: 'text-1' }, { type: 'source-url', sourceId: 'source-id', url: 'https://example.com', title: 'Example', }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "The weather in London is sunny.", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "The weather in London is sunny.", "type": "text", }, { "providerMetadata": undefined, "sourceId": "source-id", "title": "Example", "type": "source-url", "url": "https://example.com", }, ], "role": "assistant", }, }, ] `); }); it('should call the onFinish function with the correct arguments', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "The weather in London is sunny.", "type": "text", }, { "providerMetadata": undefined, "sourceId": "source-id", "title": "Example", "type": "source-url", "url": "https://example.com", }, ], "role": "assistant", } `); }); }); describe('file parts', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'Here is a file:' }, { type: 'text-end', id: 'text-1' }, { type: 'file', url: 'data:text/plain;base64,SGVsbG8gV29ybGQ=', mediaType: 'text/plain', }, { type: 'text-start', id: 'text-2' }, { type: 'text-delta', id: 'text-2', delta: 'And another one:' }, { type: 'text-end', id: 'text-2' }, { type: 'file', url: 'data:application/json;base64,eyJrZXkiOiJ2YWx1ZSJ9', mediaType: 'application/json', }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "Here is a file:", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Here is a file:", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Here is a file:", "type": "text", }, { "mediaType": "text/plain", "type": "file", "url": "data:text/plain;base64,SGVsbG8gV29ybGQ=", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Here is a file:", "type": "text", }, { "mediaType": "text/plain", "type": "file", "url": "data:text/plain;base64,SGVsbG8gV29ybGQ=", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Here is a file:", "type": "text", }, { "mediaType": "text/plain", "type": "file", "url": "data:text/plain;base64,SGVsbG8gV29ybGQ=", }, { "providerMetadata": undefined, "state": "streaming", "text": "And another one:", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Here is a file:", "type": "text", }, { "mediaType": "text/plain", "type": "file", "url": "data:text/plain;base64,SGVsbG8gV29ybGQ=", }, { "providerMetadata": undefined, "state": "done", "text": "And another one:", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Here is a file:", "type": "text", }, { "mediaType": "text/plain", "type": "file", "url": "data:text/plain;base64,SGVsbG8gV29ybGQ=", }, { "providerMetadata": undefined, "state": "done", "text": "And another one:", "type": "text", }, { "mediaType": "application/json", "type": "file", "url": "data:application/json;base64,eyJrZXkiOiJ2YWx1ZSJ9", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Here is a file:", "type": "text", }, { "mediaType": "text/plain", "type": "file", "url": "data:text/plain;base64,SGVsbG8gV29ybGQ=", }, { "providerMetadata": undefined, "state": "done", "text": "And another one:", "type": "text", }, { "mediaType": "application/json", "type": "file", "url": "data:application/json;base64,eyJrZXkiOiJ2YWx1ZSJ9", }, ], "role": "assistant", } `); }); }); describe('data ui parts (single part)', () => { let dataCalls: InferUIMessageData<UIMessage>[] = []; beforeEach(async () => { dataCalls = []; const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'data-test', data: 'example-data-can-be-anything', }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, onData: data => { dataCalls.push(data); }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "data": "example-data-can-be-anything", "type": "data-test", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "data": "example-data-can-be-anything", "type": "data-test", }, ], "role": "assistant", } `); }); it('should call the onData callback with the correct arguments', async () => { expect(dataCalls).toMatchInlineSnapshot(` [ { "data": "example-data-can-be-anything", "type": "data-test", }, ] `); }); }); describe('data ui parts (transient part)', () => { let dataCalls: InferUIMessageData<UIMessage>[] = []; beforeEach(async () => { dataCalls = []; const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'data-test', data: 'example-data-can-be-anything', transient: true, }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, onData: data => { dataCalls.push(data); }, }), }); }); it('should not call the update function with the transient part', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, ] `); }); it('should not have the transient part in the final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, ], "role": "assistant", } `); }); it('should call the onData callback with the transient part', async () => { expect(dataCalls).toMatchInlineSnapshot(` [ { "data": "example-data-can-be-anything", "transient": true, "type": "data-test", }, ] `); }); }); describe('data ui parts (single part with id and replacement update)', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'data-test', id: 'data-part-id', data: 'example-data-can-be-anything', }, { type: 'data-test', id: 'data-part-id', data: 'or-something-else', }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "data": "example-data-can-be-anything", "id": "data-part-id", "type": "data-test", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "data": "or-something-else", "id": "data-part-id", "type": "data-test", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "data": "or-something-else", "id": "data-part-id", "type": "data-test", }, ], "role": "assistant", } `); }); }); describe('data ui parts (single part with id and merge update)', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'data-test', id: 'data-part-id', data: { a: 'a1', b: 'b1', }, }, { type: 'data-test', id: 'data-part-id', data: { b: 'b2', c: 'c2', }, }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "data": { "a": "a1", "b": "b1", }, "id": "data-part-id", "type": "data-test", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "data": { "b": "b2", "c": "c2", }, "id": "data-part-id", "type": "data-test", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message).toMatchInlineSnapshot(` { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "data": { "b": "b2", "c": "c2", }, "id": "data-part-id", "type": "data-test", }, ], "role": "assistant", } `); }); }); describe('provider-executed tools', () => { let onToolCallInvoked: boolean; beforeEach(async () => { onToolCallInvoked = false; const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'tool-input-start', toolCallId: 'tool-call-1', toolName: 'tool-name', providerExecuted: true, }, { type: 'tool-input-delta', toolCallId: 'tool-call-1', inputTextDelta: '{ "query": "test" }', }, { type: 'tool-input-available', toolCallId: 'tool-call-1', toolName: 'tool-name', input: { query: 'test' }, providerExecuted: true, }, { type: 'tool-output-available', toolCallId: 'tool-call-1', output: { result: 'provider-result' }, providerExecuted: true, }, { type: 'tool-input-available', toolCallId: 'tool-call-2', toolName: 'tool-name', input: { query: 'test' }, providerExecuted: true, }, { type: 'tool-output-error', toolCallId: 'tool-call-2', errorText: 'error-text', providerExecuted: true, }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, onToolCall: () => { onToolCallInvoked = true; }, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should not call onToolCall', async () => { expect(onToolCallInvoked).toBe(false); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": undefined, "output": undefined, "providerExecuted": true, "rawInput": undefined, "state": "input-streaming", "toolCallId": "tool-call-1", "type": "tool-tool-name", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "query": "test", }, "output": undefined, "providerExecuted": true, "rawInput": undefined, "state": "input-streaming", "toolCallId": "tool-call-1", "type": "tool-tool-name", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "query": "test", }, "output": undefined, "providerExecuted": true, "rawInput": undefined, "state": "input-available", "toolCallId": "tool-call-1", "type": "tool-tool-name", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "query": "test", }, "output": { "result": "provider-result", }, "providerExecuted": true, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-1", "type": "tool-tool-name", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "query": "test", }, "output": { "result": "provider-result", }, "providerExecuted": true, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-1", "type": "tool-tool-name", }, { "errorText": undefined, "input": { "query": "test", }, "output": undefined, "providerExecuted": true, "rawInput": undefined, "state": "input-available", "toolCallId": "tool-call-2", "type": "tool-tool-name", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "query": "test", }, "output": { "result": "provider-result", }, "providerExecuted": true, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-1", "type": "tool-tool-name", }, { "errorText": "error-text", "input": { "query": "test", }, "output": undefined, "providerExecuted": true, "rawInput": undefined, "state": "output-error", "toolCallId": "tool-call-2", "type": "tool-tool-name", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message.parts).toMatchInlineSnapshot(` [ { "type": "step-start", }, { "errorText": undefined, "input": { "query": "test", }, "output": { "result": "provider-result", }, "providerExecuted": true, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-1", "type": "tool-tool-name", }, { "errorText": "error-text", "input": { "query": "test", }, "output": undefined, "providerExecuted": true, "rawInput": undefined, "state": "output-error", "toolCallId": "tool-call-2", "type": "tool-tool-name", }, ] `); }); }); it('should call onToolCall for client-executed tools', async () => { let onToolCallInvoked = false; const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'tool-input-available', toolCallId: 'tool-call-id', toolName: 'tool-name', input: { query: 'test' }, }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, onToolCall: async () => { onToolCallInvoked = true; }, runUpdateMessageJob, onError: error => { throw error; }, }), }); expect(onToolCallInvoked).toBe(true); expect(state.message.parts).toMatchInlineSnapshot(` [ { "type": "step-start", }, { "errorText": undefined, "input": { "query": "test", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "input-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, ] `); }); describe('dynamic tools', () => { let onToolCallInvoked: boolean; beforeEach(async () => { onToolCallInvoked = false; const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'tool-input-start', toolCallId: 'tool-call-1', toolName: 't1', dynamic: true, }, { type: 'tool-input-delta', toolCallId: 'tool-call-1', inputTextDelta: '{ "query": "test" }', }, { type: 'tool-input-available', toolCallId: 'tool-call-1', toolName: 't1', input: { query: 'test' }, dynamic: true, }, { type: 'tool-input-available', toolCallId: 'tool-call-2', toolName: 't1', input: { query: 'test' }, dynamic: true, }, { type: 'tool-output-available', toolCallId: 'tool-call-1', output: { result: 'provider-result' }, dynamic: true, }, { type: 'tool-output-error', toolCallId: 'tool-call-2', errorText: 'error-text', dynamic: true, }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, onToolCall: () => { onToolCallInvoked = true; }, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should invoke onToolCall for dynamic tools', async () => { expect(onToolCallInvoked).toBe(true); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": undefined, "output": undefined, "state": "input-streaming", "toolCallId": "tool-call-1", "toolName": "t1", "type": "dynamic-tool", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "query": "test", }, "output": undefined, "rawInput": undefined, "state": "input-streaming", "toolCallId": "tool-call-1", "toolName": "t1", "type": "dynamic-tool", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "query": "test", }, "output": undefined, "rawInput": undefined, "state": "input-available", "toolCallId": "tool-call-1", "toolName": "t1", "type": "dynamic-tool", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "query": "test", }, "output": undefined, "rawInput": undefined, "state": "input-available", "toolCallId": "tool-call-1", "toolName": "t1", "type": "dynamic-tool", }, { "errorText": undefined, "input": { "query": "test", }, "output": undefined, "state": "input-available", "toolCallId": "tool-call-2", "toolName": "t1", "type": "dynamic-tool", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "query": "test", }, "output": { "result": "provider-result", }, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-1", "toolName": "t1", "type": "dynamic-tool", }, { "errorText": undefined, "input": { "query": "test", }, "output": undefined, "state": "input-available", "toolCallId": "tool-call-2", "toolName": "t1", "type": "dynamic-tool", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "query": "test", }, "output": { "result": "provider-result", }, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-1", "toolName": "t1", "type": "dynamic-tool", }, { "errorText": "error-text", "input": { "query": "test", }, "output": undefined, "rawInput": undefined, "state": "output-error", "toolCallId": "tool-call-2", "toolName": "t1", "type": "dynamic-tool", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message.parts).toMatchInlineSnapshot(` [ { "type": "step-start", }, { "errorText": undefined, "input": { "query": "test", }, "output": { "result": "provider-result", }, "rawInput": undefined, "state": "output-available", "toolCallId": "tool-call-1", "toolName": "t1", "type": "dynamic-tool", }, { "errorText": "error-text", "input": { "query": "test", }, "output": undefined, "rawInput": undefined, "state": "output-error", "toolCallId": "tool-call-2", "toolName": "t1", "type": "dynamic-tool", }, ] `); }); }); describe('provider metadata', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'text-start', id: '1', providerMetadata: { testProvider: { signature: '1' } }, }, { type: 'text-delta', id: '1', delta: 'Hello', }, { type: 'text-delta', id: '1', delta: ', ', }, { type: 'text-delta', id: '1', delta: 'world!', }, { type: 'text-end', id: '1', }, { type: 'tool-input-available', toolCallId: 'tool-call-id', toolName: 'tool-name', input: { query: 'test' }, providerMetadata: { testProvider: { signature: '2' } }, }, { type: 'finish-step' }, { type: 'finish' }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1", }, }, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1", }, }, "state": "streaming", "text": "Hello", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1", }, }, "state": "streaming", "text": "Hello, ", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1", }, }, "state": "streaming", "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1", }, }, "state": "done", "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1", }, }, "state": "done", "text": "Hello, world!", "type": "text", }, { "callProviderMetadata": { "testProvider": { "signature": "2", }, }, "errorText": undefined, "input": { "query": "test", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "input-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message.parts).toMatchInlineSnapshot(` [ { "type": "step-start", }, { "providerMetadata": { "testProvider": { "signature": "1", }, }, "state": "done", "text": "Hello, world!", "type": "text", }, { "callProviderMetadata": { "testProvider": { "signature": "2", }, }, "errorText": undefined, "input": { "query": "test", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "input-available", "toolCallId": "tool-call-id", "type": "tool-tool-name", }, ] `); }); }); describe('tool input error', () => { beforeEach(async () => { const stream = createUIMessageStream([ { type: 'start', }, { type: 'start-step', }, { toolCallId: 'call-1', toolName: 'cityAttractions', type: 'tool-input-start', }, { inputTextDelta: '{ "cities": "San Francisco" }', toolCallId: 'call-1', type: 'tool-input-delta', }, { errorText: 'Invalid input for tool cityAttractions', input: '{ "cities": "San Francisco" }', toolCallId: 'call-1', toolName: 'cityAttractions', type: 'tool-input-error', }, { errorText: 'Invalid input for tool cityAttractions', toolCallId: 'call-1', type: 'tool-output-error', }, { type: 'finish-step', }, { type: 'finish', }, ]); state = createStreamingUIMessageState({ messageId: 'msg-123', lastMessage: undefined, }); await consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob, onError: error => { throw error; }, }), }); }); it('should call the update function with the correct arguments', async () => { expect(writeCalls).toMatchInlineSnapshot(` [ { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": undefined, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "input-streaming", "toolCallId": "call-1", "type": "tool-cityAttractions", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": undefined, "input": { "cities": "San Francisco", }, "output": undefined, "providerExecuted": undefined, "rawInput": undefined, "state": "input-streaming", "toolCallId": "call-1", "type": "tool-cityAttractions", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": "Invalid input for tool cityAttractions", "input": undefined, "output": undefined, "providerExecuted": undefined, "rawInput": "{ "cities": "San Francisco" }", "state": "output-error", "toolCallId": "call-1", "type": "tool-cityAttractions", }, ], "role": "assistant", }, }, { "message": { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "errorText": "Invalid input for tool cityAttractions", "input": undefined, "output": undefined, "providerExecuted": undefined, "rawInput": "{ "cities": "San Francisco" }", "state": "output-error", "toolCallId": "call-1", "type": "tool-cityAttractions", }, ], "role": "assistant", }, }, ] `); }); it('should have the correct final message state', async () => { expect(state!.message.parts).toMatchInlineSnapshot(` [ { "type": "step-start", }, { "errorText": "Invalid input for tool cityAttractions", "input": undefined, "output": undefined, "providerExecuted": undefined, "rawInput": "{ "cities": "San Francisco" }", "state": "output-error", "toolCallId": "call-1", "type": "tool-cityAttractions", }, ] `); }); }); }); --- File: /ai/packages/ai/src/ui/process-ui-message-stream.ts --- import { StandardSchemaV1, validateTypes, Validator, } from '@ai-sdk/provider-utils'; import { ProviderMetadata } from '../types'; import { DataUIMessageChunk, InferUIMessageChunk, isDataUIMessageChunk, UIMessageChunk, } from '../ui-message-stream/ui-message-chunks'; import { ErrorHandler } from '../util/error-handler'; import { mergeObjects } from '../util/merge-objects'; import { parsePartialJson } from '../util/parse-partial-json'; import { UIDataTypesToSchemas } from './chat'; import { DataUIPart, DynamicToolUIPart, getToolName, InferUIMessageData, InferUIMessageMetadata, InferUIMessageToolCall, InferUIMessageTools, isToolUIPart, ReasoningUIPart, TextUIPart, ToolUIPart, UIMessage, UIMessagePart, } from './ui-messages'; export type StreamingUIMessageState<UI_MESSAGE extends UIMessage> = { message: UI_MESSAGE; activeTextParts: Record<string, TextUIPart>; activeReasoningParts: Record<string, ReasoningUIPart>; partialToolCalls: Record< string, { text: string; index: number; toolName: string; dynamic?: boolean } >; }; export function createStreamingUIMessageState<UI_MESSAGE extends UIMessage>({ lastMessage, messageId, }: { lastMessage: UI_MESSAGE | undefined; messageId: string; }): StreamingUIMessageState<UI_MESSAGE> { return { message: lastMessage?.role === 'assistant' ? lastMessage : ({ id: messageId, metadata: undefined, role: 'assistant', parts: [] as UIMessagePart< InferUIMessageData<UI_MESSAGE>, InferUIMessageTools<UI_MESSAGE> >[], } as UI_MESSAGE), activeTextParts: {}, activeReasoningParts: {}, partialToolCalls: {}, }; } export function processUIMessageStream<UI_MESSAGE extends UIMessage>({ stream, messageMetadataSchema, dataPartSchemas, runUpdateMessageJob, onError, onToolCall, onData, }: { // input stream is not fully typed yet: stream: ReadableStream<UIMessageChunk>; messageMetadataSchema?: | Validator<InferUIMessageMetadata<UI_MESSAGE>> | StandardSchemaV1<InferUIMessageMetadata<UI_MESSAGE>>; dataPartSchemas?: UIDataTypesToSchemas<InferUIMessageData<UI_MESSAGE>>; onToolCall?: (options: { toolCall: InferUIMessageToolCall<UI_MESSAGE>; }) => void | PromiseLike<void>; onData?: (dataPart: DataUIPart<InferUIMessageData<UI_MESSAGE>>) => void; runUpdateMessageJob: ( job: (options: { state: StreamingUIMessageState<UI_MESSAGE>; write: () => void; }) => Promise<void>, ) => Promise<void>; onError: ErrorHandler; }): ReadableStream<InferUIMessageChunk<UI_MESSAGE>> { return stream.pipeThrough( new TransformStream<UIMessageChunk, InferUIMessageChunk<UI_MESSAGE>>({ async transform(chunk, controller) { await runUpdateMessageJob(async ({ state, write }) => { function getToolInvocation(toolCallId: string) { const toolInvocations = state.message.parts.filter(isToolUIPart); const toolInvocation = toolInvocations.find( invocation => invocation.toolCallId === toolCallId, ); if (toolInvocation == null) { throw new Error( 'tool-output-error must be preceded by a tool-input-available', ); } return toolInvocation; } function getDynamicToolInvocation(toolCallId: string) { const toolInvocations = state.message.parts.filter( part => part.type === 'dynamic-tool', ) as DynamicToolUIPart[]; const toolInvocation = toolInvocations.find( invocation => invocation.toolCallId === toolCallId, ); if (toolInvocation == null) { throw new Error( 'tool-output-error must be preceded by a tool-input-available', ); } return toolInvocation; } function updateToolPart( options: { toolName: keyof InferUIMessageTools<UI_MESSAGE> & string; toolCallId: string; providerExecuted?: boolean; } & ( | { state: 'input-streaming'; input: unknown; providerExecuted?: boolean; } | { state: 'input-available'; input: unknown; providerExecuted?: boolean; providerMetadata?: ProviderMetadata; } | { state: 'output-available'; input: unknown; output: unknown; providerExecuted?: boolean; } | { state: 'output-error'; input: unknown; rawInput?: unknown; errorText: string; providerExecuted?: boolean; providerMetadata?: ProviderMetadata; } ), ) { const part = state.message.parts.find( part => isToolUIPart(part) && part.toolCallId === options.toolCallId, ) as ToolUIPart<InferUIMessageTools<UI_MESSAGE>> | undefined; const anyOptions = options as any; const anyPart = part as any; if (part != null) { part.state = options.state; anyPart.input = anyOptions.input; anyPart.output = anyOptions.output; anyPart.errorText = anyOptions.errorText; anyPart.rawInput = anyOptions.rawInput; // once providerExecuted is set, it stays for streaming anyPart.providerExecuted = anyOptions.providerExecuted ?? part.providerExecuted; if ( anyOptions.providerMetadata != null && part.state === 'input-available' ) { part.callProviderMetadata = anyOptions.providerMetadata; } } else { state.message.parts.push({ type: `tool-${options.toolName}`, toolCallId: options.toolCallId, state: options.state, input: anyOptions.input, output: anyOptions.output, rawInput: anyOptions.rawInput, errorText: anyOptions.errorText, providerExecuted: anyOptions.providerExecuted, ...(anyOptions.providerMetadata != null ? { callProviderMetadata: anyOptions.providerMetadata } : {}), } as ToolUIPart<InferUIMessageTools<UI_MESSAGE>>); } } function updateDynamicToolPart( options: { toolName: keyof InferUIMessageTools<UI_MESSAGE> & string; toolCallId: string; providerExecuted?: boolean; } & ( | { state: 'input-streaming'; input: unknown; } | { state: 'input-available'; input: unknown; providerMetadata?: ProviderMetadata; } | { state: 'output-available'; input: unknown; output: unknown; } | { state: 'output-error'; input: unknown; errorText: string; providerMetadata?: ProviderMetadata; } ), ) { const part = state.message.parts.find( part => part.type === 'dynamic-tool' && part.toolCallId === options.toolCallId, ) as DynamicToolUIPart | undefined; const anyOptions = options as any; const anyPart = part as any; if (part != null) { part.state = options.state; anyPart.toolName = options.toolName; anyPart.input = anyOptions.input; anyPart.output = anyOptions.output; anyPart.errorText = anyOptions.errorText; anyPart.rawInput = anyOptions.rawInput ?? anyPart.rawInput; if ( anyOptions.providerMetadata != null && part.state === 'input-available' ) { part.callProviderMetadata = anyOptions.providerMetadata; } } else { state.message.parts.push({ type: 'dynamic-tool', toolName: options.toolName, toolCallId: options.toolCallId, state: options.state, input: anyOptions.input, output: anyOptions.output, errorText: anyOptions.errorText, ...(anyOptions.providerMetadata != null ? { callProviderMetadata: anyOptions.providerMetadata } : {}), } as DynamicToolUIPart); } } async function updateMessageMetadata(metadata: unknown) { if (metadata != null) { const mergedMetadata = state.message.metadata != null ? mergeObjects(state.message.metadata, metadata) : metadata; if (messageMetadataSchema != null) { await validateTypes({ value: mergedMetadata, schema: messageMetadataSchema, }); } state.message.metadata = mergedMetadata as InferUIMessageMetadata<UI_MESSAGE>; } } switch (chunk.type) { case 'text-start': { const textPart: TextUIPart = { type: 'text', text: '', providerMetadata: chunk.providerMetadata, state: 'streaming', }; state.activeTextParts[chunk.id] = textPart; state.message.parts.push(textPart); write(); break; } case 'text-delta': { const textPart = state.activeTextParts[chunk.id]; textPart.text += chunk.delta; textPart.providerMetadata = chunk.providerMetadata ?? textPart.providerMetadata; write(); break; } case 'text-end': { const textPart = state.activeTextParts[chunk.id]; textPart.state = 'done'; textPart.providerMetadata = chunk.providerMetadata ?? textPart.providerMetadata; delete state.activeTextParts[chunk.id]; write(); break; } case 'reasoning-start': { const reasoningPart: ReasoningUIPart = { type: 'reasoning', text: '', providerMetadata: chunk.providerMetadata, state: 'streaming', }; state.activeReasoningParts[chunk.id] = reasoningPart; state.message.parts.push(reasoningPart); write(); break; } case 'reasoning-delta': { const reasoningPart = state.activeReasoningParts[chunk.id]; reasoningPart.text += chunk.delta; reasoningPart.providerMetadata = chunk.providerMetadata ?? reasoningPart.providerMetadata; write(); break; } case 'reasoning-end': { const reasoningPart = state.activeReasoningParts[chunk.id]; reasoningPart.providerMetadata = chunk.providerMetadata ?? reasoningPart.providerMetadata; reasoningPart.state = 'done'; delete state.activeReasoningParts[chunk.id]; write(); break; } case 'file': { state.message.parts.push({ type: 'file', mediaType: chunk.mediaType, url: chunk.url, }); write(); break; } case 'source-url': { state.message.parts.push({ type: 'source-url', sourceId: chunk.sourceId, url: chunk.url, title: chunk.title, providerMetadata: chunk.providerMetadata, }); write(); break; } case 'source-document': { state.message.parts.push({ type: 'source-document', sourceId: chunk.sourceId, mediaType: chunk.mediaType, title: chunk.title, filename: chunk.filename, providerMetadata: chunk.providerMetadata, }); write(); break; } case 'tool-input-start': { const toolInvocations = state.message.parts.filter(isToolUIPart); // add the partial tool call to the map state.partialToolCalls[chunk.toolCallId] = { text: '', toolName: chunk.toolName, index: toolInvocations.length, dynamic: chunk.dynamic, }; if (chunk.dynamic) { updateDynamicToolPart({ toolCallId: chunk.toolCallId, toolName: chunk.toolName, state: 'input-streaming', input: undefined, }); } else { updateToolPart({ toolCallId: chunk.toolCallId, toolName: chunk.toolName, state: 'input-streaming', input: undefined, providerExecuted: chunk.providerExecuted, }); } write(); break; } case 'tool-input-delta': { const partialToolCall = state.partialToolCalls[chunk.toolCallId]; partialToolCall.text += chunk.inputTextDelta; const { value: partialArgs } = await parsePartialJson( partialToolCall.text, ); if (partialToolCall.dynamic) { updateDynamicToolPart({ toolCallId: chunk.toolCallId, toolName: partialToolCall.toolName, state: 'input-streaming', input: partialArgs, }); } else { updateToolPart({ toolCallId: chunk.toolCallId, toolName: partialToolCall.toolName, state: 'input-streaming', input: partialArgs, }); } write(); break; } case 'tool-input-available': { if (chunk.dynamic) { updateDynamicToolPart({ toolCallId: chunk.toolCallId, toolName: chunk.toolName, state: 'input-available', input: chunk.input, providerMetadata: chunk.providerMetadata, }); } else { updateToolPart({ toolCallId: chunk.toolCallId, toolName: chunk.toolName, state: 'input-available', input: chunk.input, providerExecuted: chunk.providerExecuted, providerMetadata: chunk.providerMetadata, }); } write(); // invoke the onToolCall callback if it exists. This is blocking. // In the future we should make this non-blocking, which // requires additional state management for error handling etc. // Skip calling onToolCall for provider-executed tools since they are already executed if (onToolCall && !chunk.providerExecuted) { await onToolCall({ toolCall: chunk as InferUIMessageToolCall<UI_MESSAGE>, }); } break; } case 'tool-input-error': { if (chunk.dynamic) { updateDynamicToolPart({ toolCallId: chunk.toolCallId, toolName: chunk.toolName, state: 'output-error', input: chunk.input, errorText: chunk.errorText, providerMetadata: chunk.providerMetadata, }); } else { updateToolPart({ toolCallId: chunk.toolCallId, toolName: chunk.toolName, state: 'output-error', input: undefined, rawInput: chunk.input, errorText: chunk.errorText, providerExecuted: chunk.providerExecuted, providerMetadata: chunk.providerMetadata, }); } write(); break; } case 'tool-output-available': { if (chunk.dynamic) { const toolInvocation = getDynamicToolInvocation( chunk.toolCallId, ); updateDynamicToolPart({ toolCallId: chunk.toolCallId, toolName: toolInvocation.toolName, state: 'output-available', input: (toolInvocation as any).input, output: chunk.output, }); } else { const toolInvocation = getToolInvocation(chunk.toolCallId); updateToolPart({ toolCallId: chunk.toolCallId, toolName: getToolName(toolInvocation), state: 'output-available', input: (toolInvocation as any).input, output: chunk.output, providerExecuted: chunk.providerExecuted, }); } write(); break; } case 'tool-output-error': { if (chunk.dynamic) { const toolInvocation = getDynamicToolInvocation( chunk.toolCallId, ); updateDynamicToolPart({ toolCallId: chunk.toolCallId, toolName: toolInvocation.toolName, state: 'output-error', input: (toolInvocation as any).input, errorText: chunk.errorText, }); } else { const toolInvocation = getToolInvocation(chunk.toolCallId); updateToolPart({ toolCallId: chunk.toolCallId, toolName: getToolName(toolInvocation), state: 'output-error', input: (toolInvocation as any).input, rawInput: (toolInvocation as any).rawInput, errorText: chunk.errorText, }); } write(); break; } case 'start-step': { // add a step boundary part to the message state.message.parts.push({ type: 'step-start' }); break; } case 'finish-step': { // reset the current text and reasoning parts state.activeTextParts = {}; state.activeReasoningParts = {}; break; } case 'start': { if (chunk.messageId != null) { state.message.id = chunk.messageId; } await updateMessageMetadata(chunk.messageMetadata); if (chunk.messageId != null || chunk.messageMetadata != null) { write(); } break; } case 'finish': { await updateMessageMetadata(chunk.messageMetadata); if (chunk.messageMetadata != null) { write(); } break; } case 'message-metadata': { await updateMessageMetadata(chunk.messageMetadata); if (chunk.messageMetadata != null) { write(); } break; } case 'error': { onError?.(new Error(chunk.errorText)); break; } default: { if (isDataUIMessageChunk(chunk)) { // validate data chunk if dataPartSchemas is provided if (dataPartSchemas?.[chunk.type] != null) { await validateTypes({ value: chunk.data, schema: dataPartSchemas[chunk.type], }); } // cast, validation is done above const dataChunk = chunk as DataUIMessageChunk< InferUIMessageData<UI_MESSAGE> >; // transient parts are not added to the message state if (dataChunk.transient) { onData?.(dataChunk); break; } const existingUIPart = dataChunk.id != null ? (state.message.parts.find( chunkArg => dataChunk.type === chunkArg.type && dataChunk.id === chunkArg.id, ) as | DataUIPart<InferUIMessageData<UI_MESSAGE>> | undefined) : undefined; if (existingUIPart != null) { existingUIPart.data = dataChunk.data; } else { state.message.parts.push(dataChunk); } onData?.(dataChunk); write(); } } } controller.enqueue(chunk as InferUIMessageChunk<UI_MESSAGE>); }); }, }), ); } --- File: /ai/packages/ai/src/ui/text-stream-chat-transport.ts --- import { UIMessageChunk } from '../ui-message-stream/ui-message-chunks'; import { HttpChatTransport, HttpChatTransportInitOptions, } from './http-chat-transport'; import { transformTextToUiMessageStream } from './transform-text-to-ui-message-stream'; import { UIMessage } from './ui-messages'; export class TextStreamChatTransport< UI_MESSAGE extends UIMessage, > extends HttpChatTransport<UI_MESSAGE> { constructor(options: HttpChatTransportInitOptions<UI_MESSAGE> = {}) { super(options); } protected processResponseStream( stream: ReadableStream<Uint8Array<ArrayBufferLike>>, ): ReadableStream<UIMessageChunk> { return transformTextToUiMessageStream({ stream: stream.pipeThrough(new TextDecoderStream()), }); } } --- File: /ai/packages/ai/src/ui/transform-text-to-ui-message-stream.test.ts --- import { convertArrayToReadableStream, convertReadableStreamToArray, } from '@ai-sdk/provider-utils/test'; import { transformTextToUiMessageStream } from './transform-text-to-ui-message-stream'; describe('transformTextToUiMessageStream', () => { it('should transform text stream into UI message stream with correct sequence', async () => { const transformedStream = transformTextToUiMessageStream({ stream: convertArrayToReadableStream(['Hello', ' ', 'World']), }); expect(await convertReadableStreamToArray(transformedStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "id": "text-1", "type": "text-start", }, { "delta": "Hello", "id": "text-1", "type": "text-delta", }, { "delta": " ", "id": "text-1", "type": "text-delta", }, { "delta": "World", "id": "text-1", "type": "text-delta", }, { "id": "text-1", "type": "text-end", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); it('should handle empty streams correctly', async () => { const transformedStream = transformTextToUiMessageStream({ stream: convertArrayToReadableStream<string>([]), }); expect(await convertReadableStreamToArray(transformedStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "id": "text-1", "type": "text-start", }, { "id": "text-1", "type": "text-end", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); it('should handle single chunk streams', async () => { const transformedStream = transformTextToUiMessageStream({ stream: convertArrayToReadableStream(['Complete message']), }); expect(await convertReadableStreamToArray(transformedStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "type": "start-step", }, { "id": "text-1", "type": "text-start", }, { "delta": "Complete message", "id": "text-1", "type": "text-delta", }, { "id": "text-1", "type": "text-end", }, { "type": "finish-step", }, { "type": "finish", }, ] `); }); }); --- File: /ai/packages/ai/src/ui/transform-text-to-ui-message-stream.ts --- import { UIMessageChunk } from '../ui-message-stream/ui-message-chunks'; export function transformTextToUiMessageStream({ stream, }: { stream: ReadableStream<string>; }) { return stream.pipeThrough( new TransformStream<string, UIMessageChunk>({ start(controller) { controller.enqueue({ type: 'start' }); controller.enqueue({ type: 'start-step' }); controller.enqueue({ type: 'text-start', id: 'text-1' }); }, async transform(part, controller) { controller.enqueue({ type: 'text-delta', id: 'text-1', delta: part }); }, async flush(controller) { controller.enqueue({ type: 'text-end', id: 'text-1' }); controller.enqueue({ type: 'finish-step' }); controller.enqueue({ type: 'finish' }); }, }), ); } --- File: /ai/packages/ai/src/ui/ui-messages.test.ts --- import { getToolName } from './ui-messages'; describe('getToolName', () => { it('should return the tool name after the "tool-" prefix', () => { expect( getToolName({ type: 'tool-getLocation', toolCallId: 'tool1', state: 'output-available', input: {}, output: 'some result', }), ).toBe('getLocation'); }); it('should return the tool name for tools that contains a dash', () => { expect( getToolName({ type: 'tool-get-location', toolCallId: 'tool1', state: 'output-available', input: {}, output: 'some result', }), ).toBe('get-location'); }); }); --- File: /ai/packages/ai/src/ui/ui-messages.ts --- import { InferToolInput, InferToolOutput, Tool, ToolCall, } from '@ai-sdk/provider-utils'; import { ToolSet } from '../generate-text'; import { ProviderMetadata } from '../types/provider-metadata'; import { DeepPartial } from '../util/deep-partial'; import { ValueOf } from '../util/value-of'; /** The data types that can be used in the UI message for the UI message data parts. */ export type UIDataTypes = Record<string, unknown>; export type UITool = { input: unknown; output: unknown | undefined; }; /** * Infer the input and output types of a tool so it can be used as a UI tool. */ export type InferUITool<TOOL extends Tool> = { input: InferToolInput<TOOL>; output: InferToolOutput<TOOL>; }; /** * Infer the input and output types of a tool set so it can be used as a UI tool set. */ export type InferUITools<TOOLS extends ToolSet> = { [NAME in keyof TOOLS & string]: InferUITool<TOOLS[NAME]>; }; export type UITools = Record<string, UITool>; /** AI SDK UI Messages. They are used in the client and to communicate between the frontend and the API routes. */ export interface UIMessage< METADATA = unknown, DATA_PARTS extends UIDataTypes = UIDataTypes, TOOLS extends UITools = UITools, > { /** A unique identifier for the message. */ id: string; /** The role of the message. */ role: 'system' | 'user' | 'assistant'; /** The metadata of the message. */ metadata?: METADATA; /** The parts of the message. Use this for rendering the message in the UI. System messages should be avoided (set the system prompt on the server instead). They can have text parts. User messages can have text parts and file parts. Assistant messages can have text, reasoning, tool invocation, and file parts. */ parts: Array<UIMessagePart<DATA_PARTS, TOOLS>>; } export type UIMessagePart< DATA_TYPES extends UIDataTypes, TOOLS extends UITools, > = | TextUIPart | ReasoningUIPart | ToolUIPart<TOOLS> | DynamicToolUIPart | SourceUrlUIPart | SourceDocumentUIPart | FileUIPart | DataUIPart<DATA_TYPES> | StepStartUIPart; /** * A text part of a message. */ export type TextUIPart = { type: 'text'; /** * The text content. */ text: string; /** * The state of the text part. */ state?: 'streaming' | 'done'; /** * The provider metadata. */ providerMetadata?: ProviderMetadata; }; /** * A reasoning part of a message. */ export type ReasoningUIPart = { type: 'reasoning'; /** * The reasoning text. */ text: string; /** * The state of the reasoning part. */ state?: 'streaming' | 'done'; /** * The provider metadata. */ providerMetadata?: ProviderMetadata; }; /** * A source part of a message. */ export type SourceUrlUIPart = { type: 'source-url'; sourceId: string; url: string; title?: string; providerMetadata?: ProviderMetadata; }; /** * A document source part of a message. */ export type SourceDocumentUIPart = { type: 'source-document'; sourceId: string; mediaType: string; title: string; filename?: string; providerMetadata?: ProviderMetadata; }; /** * A file part of a message. */ export type FileUIPart = { type: 'file'; /** * IANA media type of the file. * * @see https://www.iana.org/assignments/media-types/media-types.xhtml */ mediaType: string; /** * Optional filename of the file. */ filename?: string; /** * The URL of the file. * It can either be a URL to a hosted file or a [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs). */ url: string; /** * The provider metadata. */ providerMetadata?: ProviderMetadata; }; /** * A step boundary part of a message. */ export type StepStartUIPart = { type: 'step-start'; }; export type DataUIPart<DATA_TYPES extends UIDataTypes> = ValueOf<{ [NAME in keyof DATA_TYPES & string]: { type: `data-${NAME}`; id?: string; data: DATA_TYPES[NAME]; }; }>; export type ToolUIPart<TOOLS extends UITools = UITools> = ValueOf<{ [NAME in keyof TOOLS & string]: { type: `tool-${NAME}`; toolCallId: string; } & ( | { state: 'input-streaming'; input: DeepPartial<TOOLS[NAME]['input']> | undefined; providerExecuted?: boolean; output?: never; errorText?: never; } | { state: 'input-available'; input: TOOLS[NAME]['input']; providerExecuted?: boolean; output?: never; errorText?: never; callProviderMetadata?: ProviderMetadata; } | { state: 'output-available'; input: TOOLS[NAME]['input']; output: TOOLS[NAME]['output']; errorText?: never; providerExecuted?: boolean; callProviderMetadata?: ProviderMetadata; } | { state: 'output-error'; // TODO AI SDK 6: change to 'error' state input: TOOLS[NAME]['input'] | undefined; rawInput?: unknown; // TODO AI SDK 6: remove this field, input should be unknown output?: never; errorText: string; providerExecuted?: boolean; callProviderMetadata?: ProviderMetadata; } ); }>; export type DynamicToolUIPart = { type: 'dynamic-tool'; toolName: string; toolCallId: string; } & ( | { state: 'input-streaming'; input: unknown | undefined; output?: never; errorText?: never; } | { state: 'input-available'; input: unknown; output?: never; errorText?: never; callProviderMetadata?: ProviderMetadata; } | { state: 'output-available'; input: unknown; output: unknown; errorText?: never; callProviderMetadata?: ProviderMetadata; } | { state: 'output-error'; // TODO AI SDK 6: change to 'error' state input: unknown; output?: never; errorText: string; callProviderMetadata?: ProviderMetadata; } ); export function isToolUIPart<TOOLS extends UITools>( part: UIMessagePart<UIDataTypes, TOOLS>, ): part is ToolUIPart<TOOLS> { return part.type.startsWith('tool-'); } export function getToolName<TOOLS extends UITools>( part: ToolUIPart<TOOLS>, ): keyof TOOLS { return part.type.split('-').slice(1).join('-') as keyof TOOLS; } export type InferUIMessageMetadata<T extends UIMessage> = T extends UIMessage<infer METADATA> ? METADATA : unknown; export type InferUIMessageData<T extends UIMessage> = T extends UIMessage<unknown, infer DATA_TYPES> ? DATA_TYPES : UIDataTypes; export type InferUIMessageTools<T extends UIMessage> = T extends UIMessage<unknown, UIDataTypes, infer TOOLS> ? TOOLS : UITools; export type InferUIMessageToolOutputs<UI_MESSAGE extends UIMessage> = InferUIMessageTools<UI_MESSAGE>[keyof InferUIMessageTools<UI_MESSAGE>]['output']; export type InferUIMessageToolCall<UI_MESSAGE extends UIMessage> = | ValueOf<{ [NAME in keyof InferUIMessageTools<UI_MESSAGE>]: ToolCall< NAME & string, InferUIMessageTools<UI_MESSAGE>[NAME] extends { input: infer INPUT } ? INPUT : never > & { dynamic?: false }; }> | (ToolCall<string, unknown> & { dynamic: true }); --- File: /ai/packages/ai/src/ui/use-completion.ts --- import { FetchFunction } from '@ai-sdk/provider-utils'; export type CompletionRequestOptions = { /** An optional object of headers to be passed to the API endpoint. */ headers?: Record<string, string> | Headers; /** An optional object to be passed to the API endpoint. */ body?: object; }; export type UseCompletionOptions = { /** * The API endpoint that accepts a `{ prompt: string }` object and returns * a stream of tokens of the AI completion response. Defaults to `/api/completion`. */ api?: string; /** * An unique identifier for the chat. If not provided, a random one will be * generated. When provided, the `useChat` hook with the same `id` will * have shared states across components. */ id?: string; /** * Initial prompt input of the completion. */ initialInput?: string; /** * Initial completion result. Useful to load an existing history. */ initialCompletion?: string; /** * Callback function to be called when the completion is finished streaming. */ onFinish?: (prompt: string, completion: string) => void; /** * Callback function to be called when an error is encountered. */ onError?: (error: Error) => void; /** * The credentials mode to be used for the fetch request. * Possible values are: 'omit', 'same-origin', 'include'. * Defaults to 'same-origin'. */ credentials?: RequestCredentials; /** * HTTP headers to be sent with the API request. */ headers?: Record<string, string> | Headers; /** * Extra body object to be sent with the API request. * @example * Send a `sessionId` to the API along with the prompt. * ```js * useChat({ * body: { * sessionId: '123', * } * }) * ``` */ body?: object; /** Streaming protocol that is used. Defaults to `data`. */ streamProtocol?: 'data' | 'text'; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; }; --- File: /ai/packages/ai/src/ui-message-stream/create-ui-message-stream-response.test.ts --- import { convertArrayToReadableStream, convertReadableStreamToArray, } from '@ai-sdk/provider-utils/test'; import { createUIMessageStreamResponse } from './create-ui-message-stream-response'; describe('createUIMessageStreamResponse', () => { it('should create a Response with correct headers and encoded stream', async () => { const response = createUIMessageStreamResponse({ status: 200, statusText: 'OK', headers: { 'Custom-Header': 'test', }, stream: convertArrayToReadableStream([ { type: 'text-delta', id: '1', delta: 'test-data' }, ]), }); // Verify response properties expect(response).toBeInstanceOf(Response); expect(response.status).toBe(200); expect(response.statusText).toBe('OK'); // Verify headers expect(Object.fromEntries(response.headers.entries())) .toMatchInlineSnapshot(` { "cache-control": "no-cache", "connection": "keep-alive", "content-type": "text/event-stream", "custom-header": "test", "x-accel-buffering": "no", "x-vercel-ai-ui-message-stream": "v1", } `); expect( await convertReadableStreamToArray( response.body!.pipeThrough(new TextDecoderStream()), ), ).toMatchInlineSnapshot(` [ "data: {"type":"text-delta","id":"1","delta":"test-data"} ", "data: [DONE] ", ] `); }); it('should handle errors in the stream', async () => { const response = createUIMessageStreamResponse({ status: 200, stream: convertArrayToReadableStream([ { type: 'error', errorText: 'Custom error message' }, ]), }); expect( await convertReadableStreamToArray( response.body!.pipeThrough(new TextDecoderStream()), ), ).toMatchInlineSnapshot(` [ "data: {"type":"error","errorText":"Custom error message"} ", "data: [DONE] ", ] `); }); it('should call consumeSseStream with a teed stream', async () => { const consumedData: string[] = []; const consumeSseStream = vi.fn( async ({ stream }: { stream: ReadableStream<string> }) => { const data = await convertReadableStreamToArray(stream); consumedData.push(...data); }, ); const response = createUIMessageStreamResponse({ status: 200, stream: convertArrayToReadableStream([ { type: 'text-delta', id: '1', delta: 'test-data-1' }, { type: 'text-delta', id: '1', delta: 'test-data-2' }, ]), consumeSseStream, }); // Verify consumeSseStream was called expect(consumeSseStream).toHaveBeenCalledTimes(1); expect(consumeSseStream).toHaveBeenCalledWith({ stream: expect.any(ReadableStream), }); // Verify the response stream still works correctly const responseData = await convertReadableStreamToArray( response.body!.pipeThrough(new TextDecoderStream()), ); expect(responseData).toMatchInlineSnapshot(` [ "data: {"type":"text-delta","id":"1","delta":"test-data-1"} ", "data: {"type":"text-delta","id":"1","delta":"test-data-2"} ", "data: [DONE] ", ] `); // Wait for consumeSseStream to complete await new Promise(resolve => setTimeout(resolve, 0)); // Verify consumeSseStream received the same data expect(consumedData).toMatchInlineSnapshot(` [ "data: {"type":"text-delta","id":"1","delta":"test-data-1"} ", "data: {"type":"text-delta","id":"1","delta":"test-data-2"} ", "data: [DONE] ", ] `); }); it('should not block the response when consumeSseStream takes time', async () => { let consumeResolve: () => void; const consumePromise = new Promise<void>(resolve => { consumeResolve = resolve; }); const consumeSseStream = vi.fn( async ({ stream }: { stream: ReadableStream<string> }) => { // Consume the stream but wait for external resolution await convertReadableStreamToArray(stream); await consumePromise; }, ); const response = createUIMessageStreamResponse({ status: 200, stream: convertArrayToReadableStream([ { type: 'text-delta', id: '1', delta: 'test-data' }, ]), consumeSseStream, }); // The response should be immediately available even though consumeSseStream hasn't finished expect(response).toBeInstanceOf(Response); expect(response.status).toBe(200); // The response body should be readable immediately const responseData = await convertReadableStreamToArray( response.body!.pipeThrough(new TextDecoderStream()), ); expect(responseData).toMatchInlineSnapshot(` [ "data: {"type":"text-delta","id":"1","delta":"test-data"} ", "data: [DONE] ", ] `); // Verify consumeSseStream was called but may still be running expect(consumeSseStream).toHaveBeenCalledTimes(1); // Now resolve the consumeSseStream consumeResolve!(); }); it('should handle synchronous consumeSseStream', async () => { const consumedData: string[] = []; const consumeSseStream = vi.fn( ({ stream }: { stream: ReadableStream<string> }) => { // Synchronous consumption (not returning a promise) stream.pipeTo( new WritableStream({ write(chunk) { consumedData.push(chunk); }, }), ); }, ); const response = createUIMessageStreamResponse({ status: 200, stream: convertArrayToReadableStream([ { type: 'text-delta', id: '1', delta: 'sync-test' }, ]), consumeSseStream, }); expect(consumeSseStream).toHaveBeenCalledTimes(1); const responseData = await convertReadableStreamToArray( response.body!.pipeThrough(new TextDecoderStream()), ); expect(responseData).toMatchInlineSnapshot(` [ "data: {"type":"text-delta","id":"1","delta":"sync-test"} ", "data: [DONE] ", ] `); }); it('should handle consumeSseStream errors gracefully', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const consumeSseStream = vi.fn(async () => { throw new Error('consumeSseStream error'); }); const response = createUIMessageStreamResponse({ status: 200, stream: convertArrayToReadableStream([ { type: 'text-delta', id: '1', delta: 'error-test' }, ]), consumeSseStream, }); // The response should still work even if consumeSseStream fails const responseData = await convertReadableStreamToArray( response.body!.pipeThrough(new TextDecoderStream()), ); expect(responseData).toMatchInlineSnapshot(` [ "data: {"type":"text-delta","id":"1","delta":"error-test"} ", "data: [DONE] ", ] `); expect(consumeSseStream).toHaveBeenCalledTimes(1); consoleSpy.mockRestore(); }); }); --- File: /ai/packages/ai/src/ui-message-stream/create-ui-message-stream-response.ts --- import { prepareHeaders } from '../util/prepare-headers'; import { JsonToSseTransformStream } from './json-to-sse-transform-stream'; import { UI_MESSAGE_STREAM_HEADERS } from './ui-message-stream-headers'; import { UIMessageChunk } from './ui-message-chunks'; import { UIMessageStreamResponseInit } from './ui-message-stream-response-init'; export function createUIMessageStreamResponse({ status, statusText, headers, stream, consumeSseStream, }: UIMessageStreamResponseInit & { stream: ReadableStream<UIMessageChunk>; }): Response { let sseStream = stream.pipeThrough(new JsonToSseTransformStream()); // when the consumeSseStream is provided, we need to tee the stream // and send the second part to the consumeSseStream function // so that it can be consumed by the client independently if (consumeSseStream) { const [stream1, stream2] = sseStream.tee(); sseStream = stream1; consumeSseStream({ stream: stream2 }); // no await (do not block the response) } return new Response(sseStream.pipeThrough(new TextEncoderStream()), { status, statusText, headers: prepareHeaders(headers, UI_MESSAGE_STREAM_HEADERS), }); } --- File: /ai/packages/ai/src/ui-message-stream/create-ui-message-stream.test.ts --- import { delay } from '@ai-sdk/provider-utils'; import { convertReadableStreamToArray } from '@ai-sdk/provider-utils/test'; import { DelayedPromise } from '../util/delayed-promise'; import { createUIMessageStream } from './create-ui-message-stream'; import { UIMessageChunk } from './ui-message-chunks'; import { UIMessageStreamWriter } from './ui-message-stream-writer'; import { consumeStream } from '../util/consume-stream'; import { UIMessage } from '../ui/ui-messages'; describe('createUIMessageStream', () => { it('should send data stream part and close the stream', async () => { const stream = createUIMessageStream({ execute: ({ writer }) => { writer.write({ type: 'text-start', id: '1' }); writer.write({ type: 'text-delta', id: '1', delta: '1a' }); writer.write({ type: 'text-end', id: '1' }); }, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, { "delta": "1a", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, ] `); }); it('should forward a single stream with 2 elements', async () => { const stream = createUIMessageStream({ execute: ({ writer }) => { writer.merge( new ReadableStream({ start(controller) { controller.enqueue({ type: 'text-delta', id: '1', delta: '1a' }); controller.enqueue({ type: 'text-delta', id: '1', delta: '1b' }); controller.close(); }, }), ); }, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "delta": "1a", "id": "1", "type": "text-delta", }, { "delta": "1b", "id": "1", "type": "text-delta", }, ] `); }); it('should send async message annotation and close the stream', async () => { const wait = new DelayedPromise<void>(); const stream = createUIMessageStream({ execute: async ({ writer }) => { await wait.promise; writer.write({ type: 'text-delta', id: '1', delta: '1a' }); }, }); wait.resolve(undefined); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "delta": "1a", "id": "1", "type": "text-delta", }, ] `); }); it('should forward elements from multiple streams and data parts', async () => { let controller1: ReadableStreamDefaultController<UIMessageChunk>; let controller2: ReadableStreamDefaultController<UIMessageChunk>; const stream = createUIMessageStream({ execute: ({ writer }) => { writer.write({ type: 'text-delta', id: '1', delta: 'data-part-1' }); writer.merge( new ReadableStream({ start(controllerArg) { controller1 = controllerArg; }, }), ); controller1!.enqueue({ type: 'text-delta', id: '1', delta: '1a' }); writer.write({ type: 'text-delta', id: '1', delta: 'data-part-2' }); controller1!.enqueue({ type: 'text-delta', id: '1', delta: '1b' }); writer.merge( new ReadableStream({ start(controllerArg) { controller2 = controllerArg; }, }), ); writer.write({ type: 'text-delta', id: '1', delta: 'data-part-3' }); }, }); controller2!.enqueue({ type: 'text-delta', id: '2', delta: '2a' }); controller1!.enqueue({ type: 'text-delta', id: '1', delta: '1c' }); controller2!.enqueue({ type: 'text-delta', id: '2', delta: '2b' }); controller2!.close(); controller1!.enqueue({ type: 'text-delta', id: '1', delta: '1d' }); controller1!.enqueue({ type: 'text-delta', id: '1', delta: '1e' }); controller1!.close(); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "delta": "data-part-1", "id": "1", "type": "text-delta", }, { "delta": "data-part-2", "id": "1", "type": "text-delta", }, { "delta": "data-part-3", "id": "1", "type": "text-delta", }, { "delta": "1a", "id": "1", "type": "text-delta", }, { "delta": "2a", "id": "2", "type": "text-delta", }, { "delta": "1b", "id": "1", "type": "text-delta", }, { "delta": "2b", "id": "2", "type": "text-delta", }, { "delta": "1c", "id": "1", "type": "text-delta", }, { "delta": "1d", "id": "1", "type": "text-delta", }, { "delta": "1e", "id": "1", "type": "text-delta", }, ] `); }); it('should add error parts when stream errors', async () => { let controller1: ReadableStreamDefaultController<UIMessageChunk>; let controller2: ReadableStreamDefaultController<UIMessageChunk>; const stream = createUIMessageStream({ execute: ({ writer }) => { writer.merge( new ReadableStream({ start(controllerArg) { controller1 = controllerArg; }, }), ); writer.merge( new ReadableStream({ start(controllerArg) { controller2 = controllerArg; }, }), ); }, onError: () => 'error-message', }); controller1!.enqueue({ type: 'text-delta', id: '1', delta: '1a' }); controller1!.error(new Error('1-error')); controller2!.enqueue({ type: 'text-delta', id: '2', delta: '2a' }); controller2!.enqueue({ type: 'text-delta', id: '2', delta: '2b' }); controller2!.close(); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "delta": "1a", "id": "1", "type": "text-delta", }, { "delta": "2a", "id": "2", "type": "text-delta", }, { "delta": "2b", "id": "2", "type": "text-delta", }, { "errorText": "error-message", "type": "error", }, ] `); }); it('should add error parts when execute throws', async () => { const stream = createUIMessageStream({ execute: () => { throw new Error('execute-error'); }, onError: () => 'error-message', }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "errorText": "error-message", "type": "error", }, ] `); }); it('should add error parts when execute throws with promise', async () => { const stream = createUIMessageStream({ execute: async () => { throw new Error('execute-error'); }, onError: () => 'error-message', }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "errorText": "error-message", "type": "error", }, ] `); }); it('should suppress error when writing to closed stream', async () => { let uiMessageStreamWriter: UIMessageStreamWriter<UIMessage>; const stream = createUIMessageStream({ execute: ({ writer }) => { writer.write({ type: 'text-delta', id: '1', delta: '1a' }); uiMessageStreamWriter = writer; }, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "delta": "1a", "id": "1", "type": "text-delta", }, ] `); expect(() => uiMessageStreamWriter!.write({ type: 'text-delta', id: '1', delta: '1b', }), ).not.toThrow(); }); it('should support writing from delayed merged streams', async () => { let uiMessageStreamWriter: UIMessageStreamWriter<UIMessage>; let controller1: ReadableStreamDefaultController<UIMessageChunk>; let controller2: ReadableStreamDefaultController<UIMessageChunk>; let done = false; const stream = createUIMessageStream({ execute: ({ writer }) => { writer.merge( new ReadableStream({ start(controllerArg) { controller1 = controllerArg; }, }), ); uiMessageStreamWriter = writer; done = true; }, }); const result: UIMessageChunk[] = []; const reader = stream.getReader(); async function pull() { const { value, done } = await reader.read(); result.push(value!); } // function is finished expect(done).toBe(true); controller1!.enqueue({ type: 'text-delta', id: '1', delta: '1a' }); await pull(); // controller1 is still open, create 2nd stream uiMessageStreamWriter!.merge( new ReadableStream({ start(controllerArg) { controller2 = controllerArg; }, }), ); // close controller1 controller1!.close(); await delay(); // relinquish control // it should still be able to write to controller2 controller2!.enqueue({ type: 'text-delta', id: '2', delta: '2a' }); controller2!.close(); await pull(); expect(result).toMatchInlineSnapshot(` [ { "delta": "1a", "id": "1", "type": "text-delta", }, { "delta": "2a", "id": "2", "type": "text-delta", }, ] `); }); it('should handle onFinish without original messages', async () => { const recordedOptions: any[] = []; const stream = createUIMessageStream({ execute: ({ writer }) => { writer.write({ type: 'text-start', id: '1' }); writer.write({ type: 'text-delta', id: '1', delta: '1a' }); writer.write({ type: 'text-end', id: '1' }); }, onFinish: options => { recordedOptions.push(options); }, generateId: () => 'response-message-id', }); await consumeStream({ stream }); expect(recordedOptions).toMatchInlineSnapshot(` [ { "isAborted": false, "isContinuation": false, "messages": [ { "id": "response-message-id", "metadata": undefined, "parts": [ { "providerMetadata": undefined, "state": "done", "text": "1a", "type": "text", }, ], "role": "assistant", }, ], "responseMessage": { "id": "response-message-id", "metadata": undefined, "parts": [ { "providerMetadata": undefined, "state": "done", "text": "1a", "type": "text", }, ], "role": "assistant", }, }, ] `); }); it('should handle onFinish with messages', async () => { const recordedOptions: any[] = []; const stream = createUIMessageStream({ execute: ({ writer }) => { writer.write({ type: 'text-start', id: '1' }); writer.write({ type: 'text-delta', id: '1', delta: '1b' }); writer.write({ type: 'text-end', id: '1' }); }, originalMessages: [ { id: '0', role: 'user', parts: [{ type: 'text', text: '0a' }], }, { id: '1', role: 'assistant', parts: [{ type: 'text', text: '1a', state: 'done' }], }, ], onFinish: options => { recordedOptions.push(options); }, }); await consumeStream({ stream }); expect(recordedOptions).toMatchInlineSnapshot(` [ { "isAborted": false, "isContinuation": true, "messages": [ { "id": "0", "parts": [ { "text": "0a", "type": "text", }, ], "role": "user", }, { "id": "1", "parts": [ { "state": "done", "text": "1a", "type": "text", }, { "providerMetadata": undefined, "state": "done", "text": "1b", "type": "text", }, ], "role": "assistant", }, ], "responseMessage": { "id": "1", "parts": [ { "state": "done", "text": "1a", "type": "text", }, { "providerMetadata": undefined, "state": "done", "text": "1b", "type": "text", }, ], "role": "assistant", }, }, ] `); }); it('should inject a messageId into the stream when originalMessages are provided', async () => { const recordedOptions: any[] = []; const stream = createUIMessageStream({ execute: ({ writer }) => { writer.write({ type: 'start' }); // no messageId }, originalMessages: [ { id: '0', role: 'user', parts: [{ type: 'text', text: '0a' }] }, // no assistant message ], onFinish(options) { recordedOptions.push(options); }, generateId: () => 'response-message-id', }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "messageId": "response-message-id", "type": "start", }, ] `); expect(recordedOptions).toMatchInlineSnapshot(` [ { "isAborted": false, "isContinuation": false, "messages": [ { "id": "0", "parts": [ { "text": "0a", "type": "text", }, ], "role": "user", }, { "id": "response-message-id", "metadata": undefined, "parts": [], "role": "assistant", }, ], "responseMessage": { "id": "response-message-id", "metadata": undefined, "parts": [], "role": "assistant", }, }, ] `); }); it('should keep existing messageId from start chunk when originalMessages are provided', async () => { const recordedOptions: any[] = []; const stream = createUIMessageStream({ execute: ({ writer }) => { writer.write({ type: 'start', messageId: 'existing-message-id' }); }, originalMessages: [ { id: '0', role: 'user', parts: [{ type: 'text', text: '0a' }] }, // no assistant message ], onFinish(options) { recordedOptions.push(options); }, generateId: () => 'response-message-id', }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "messageId": "existing-message-id", "type": "start", }, ] `); expect(recordedOptions).toMatchInlineSnapshot(` [ { "isAborted": false, "isContinuation": false, "messages": [ { "id": "0", "parts": [ { "text": "0a", "type": "text", }, ], "role": "user", }, { "id": "existing-message-id", "metadata": undefined, "parts": [], "role": "assistant", }, ], "responseMessage": { "id": "existing-message-id", "metadata": undefined, "parts": [], "role": "assistant", }, }, ] `); }); }); --- File: /ai/packages/ai/src/ui-message-stream/create-ui-message-stream.ts --- import { generateId as generateIdFunc, getErrorMessage, IdGenerator, } from '@ai-sdk/provider-utils'; import { UIMessage } from '../ui/ui-messages'; import { handleUIMessageStreamFinish } from './handle-ui-message-stream-finish'; import { InferUIMessageChunk } from './ui-message-chunks'; import { UIMessageStreamOnFinishCallback } from './ui-message-stream-on-finish-callback'; import { UIMessageStreamWriter } from './ui-message-stream-writer'; export function createUIMessageStream<UI_MESSAGE extends UIMessage>({ execute, onError = getErrorMessage, originalMessages, onFinish, generateId = generateIdFunc, }: { execute: (options: { writer: UIMessageStreamWriter<UI_MESSAGE>; }) => Promise<void> | void; onError?: (error: unknown) => string; /** * The original messages. If they are provided, persistence mode is assumed, * and a message ID is provided for the response message. */ originalMessages?: UI_MESSAGE[]; onFinish?: UIMessageStreamOnFinishCallback<UI_MESSAGE>; generateId?: IdGenerator; }): ReadableStream<InferUIMessageChunk<UI_MESSAGE>> { let controller!: ReadableStreamDefaultController< InferUIMessageChunk<UI_MESSAGE> >; const ongoingStreamPromises: Promise<void>[] = []; const stream = new ReadableStream({ start(controllerArg) { controller = controllerArg; }, }); function safeEnqueue(data: InferUIMessageChunk<UI_MESSAGE>) { try { controller.enqueue(data); } catch (error) { // suppress errors when the stream has been closed } } try { const result = execute({ writer: { write(part: InferUIMessageChunk<UI_MESSAGE>) { safeEnqueue(part); }, merge(streamArg) { ongoingStreamPromises.push( (async () => { const reader = streamArg.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; safeEnqueue(value); } })().catch(error => { safeEnqueue({ type: 'error', errorText: onError(error), } as InferUIMessageChunk<UI_MESSAGE>); }), ); }, onError, }, }); if (result) { ongoingStreamPromises.push( result.catch(error => { safeEnqueue({ type: 'error', errorText: onError(error), } as InferUIMessageChunk<UI_MESSAGE>); }), ); } } catch (error) { safeEnqueue({ type: 'error', errorText: onError(error), } as InferUIMessageChunk<UI_MESSAGE>); } // Wait until all ongoing streams are done. This approach enables merging // streams even after execute has returned, as long as there is still an // open merged stream. This is important to e.g. forward new streams and // from callbacks. const waitForStreams: Promise<void> = new Promise(async resolve => { while (ongoingStreamPromises.length > 0) { await ongoingStreamPromises.shift(); } resolve(); }); waitForStreams.finally(() => { try { controller.close(); } catch (error) { // suppress errors when the stream has been closed } }); return handleUIMessageStreamFinish<UI_MESSAGE>({ stream, messageId: generateId(), originalMessages, onFinish, onError, }); } --- File: /ai/packages/ai/src/ui-message-stream/get-response-ui-message-id.test.ts --- import { describe, expect, it } from 'vitest'; import { getResponseUIMessageId } from './get-response-ui-message-id'; import { UIMessage } from '../ui/ui-messages'; describe('getResponseUIMessageId', () => { const mockGenerateId = () => 'new-id'; it('should return undefined when originalMessages is null', () => { const result = getResponseUIMessageId({ originalMessages: undefined, responseMessageId: mockGenerateId, }); expect(result).toBeUndefined(); }); it('should return the last assistant message id when present', () => { const messages: UIMessage[] = [ { id: 'msg-1', role: 'user', parts: [] }, { id: 'msg-2', role: 'assistant', parts: [] }, ]; const result = getResponseUIMessageId({ originalMessages: messages, responseMessageId: mockGenerateId, }); expect(result).toBe('msg-2'); }); it('should generate new id when last message is not from assistant', () => { const messages: UIMessage[] = [ { id: 'msg-1', role: 'assistant', parts: [] }, { id: 'msg-2', role: 'user', parts: [] }, ]; const result = getResponseUIMessageId({ originalMessages: messages, responseMessageId: mockGenerateId, }); expect(result).toBe('new-id'); }); it('should generate new id when messages array is empty', () => { const result = getResponseUIMessageId({ originalMessages: [], responseMessageId: mockGenerateId, }); expect(result).toBe('new-id'); }); it('should use the responseMessageId when it is a string', () => { const result = getResponseUIMessageId({ originalMessages: [], responseMessageId: 'response-id', }); expect(result).toBe('response-id'); }); }); --- File: /ai/packages/ai/src/ui-message-stream/get-response-ui-message-id.ts --- import { IdGenerator } from '@ai-sdk/provider-utils'; import { UIMessage } from '../ui/ui-messages'; export function getResponseUIMessageId({ originalMessages, responseMessageId, }: { originalMessages: UIMessage[] | undefined; responseMessageId: string | IdGenerator; }) { // when there are no original messages (i.e. no persistence), // the assistant message id generation is handled on the client side. if (originalMessages == null) { return undefined; } const lastMessage = originalMessages[originalMessages.length - 1]; return lastMessage?.role === 'assistant' ? lastMessage.id : typeof responseMessageId === 'function' ? responseMessageId() : responseMessageId; } --- File: /ai/packages/ai/src/ui-message-stream/handle-ui-message-stream-finish.test.ts --- import { convertArrayToReadableStream, convertReadableStreamToArray, } from '@ai-sdk/provider-utils/test'; import { describe, expect, it, vi } from 'vitest'; import { UIMessage } from '../ui/ui-messages'; import { handleUIMessageStreamFinish } from './handle-ui-message-stream-finish'; import { UIMessageChunk } from './ui-message-chunks'; function createUIMessageStream(parts: UIMessageChunk[]) { return convertArrayToReadableStream(parts); } describe('handleUIMessageStreamFinish', () => { const mockErrorHandler = vi.fn(); beforeEach(() => { mockErrorHandler.mockClear(); }); describe('stream pass-through without onFinish', () => { it('should pass through stream chunks without processing when onFinish is not provided', async () => { const inputChunks: UIMessageChunk[] = [ { type: 'start', messageId: 'msg-123' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'Hello' }, { type: 'text-delta', id: 'text-1', delta: ' World' }, { type: 'text-end', id: 'text-1' }, { type: 'finish' }, ]; const stream = createUIMessageStream(inputChunks); const resultStream = handleUIMessageStreamFinish<UIMessage>({ stream, messageId: 'msg-123', originalMessages: [], onError: mockErrorHandler, // onFinish is not provided }); const result = await convertReadableStreamToArray(resultStream); expect(result).toEqual(inputChunks); expect(mockErrorHandler).not.toHaveBeenCalled(); }); it('should inject messageId when not present in start chunk', async () => { const inputChunks: UIMessageChunk[] = [ { type: 'start' }, // no messageId { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'Test' }, { type: 'text-end', id: 'text-1' }, { type: 'finish' }, ]; const stream = createUIMessageStream(inputChunks); const resultStream = handleUIMessageStreamFinish<UIMessage>({ stream, messageId: 'injected-123', originalMessages: [], onError: mockErrorHandler, }); const result = await convertReadableStreamToArray(resultStream); expect(result[0]).toEqual({ type: 'start', messageId: 'injected-123' }); expect(result.slice(1)).toEqual(inputChunks.slice(1)); }); }); describe('stream processing with onFinish callback', () => { it('should process stream and call onFinish with correct parameters', async () => { const onFinishCallback = vi.fn(); const inputChunks: UIMessageChunk[] = [ { type: 'start', messageId: 'msg-456' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'Hello' }, { type: 'text-delta', id: 'text-1', delta: ' World' }, { type: 'text-end', id: 'text-1' }, { type: 'finish' }, ]; const originalMessages: UIMessage[] = [ { id: 'user-msg-1', role: 'user', parts: [{ type: 'text', text: 'Hello' }], }, ]; const stream = createUIMessageStream(inputChunks); const resultStream = handleUIMessageStreamFinish<UIMessage>({ stream, messageId: 'msg-456', originalMessages, onError: mockErrorHandler, onFinish: onFinishCallback, }); const result = await convertReadableStreamToArray(resultStream); expect(result).toEqual(inputChunks); expect(onFinishCallback).toHaveBeenCalledTimes(1); const callArgs = onFinishCallback.mock.calls[0][0]; expect(callArgs.isContinuation).toBe(false); expect(callArgs.responseMessage.id).toBe('msg-456'); expect(callArgs.responseMessage.role).toBe('assistant'); expect(callArgs.messages).toHaveLength(2); expect(callArgs.messages[0]).toEqual(originalMessages[0]); expect(callArgs.messages[1]).toEqual(callArgs.responseMessage); }); it('should handle empty original messages array', async () => { const onFinishCallback = vi.fn(); const inputChunks: UIMessageChunk[] = [ { type: 'start', messageId: 'msg-789' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'Response' }, { type: 'text-end', id: 'text-1' }, { type: 'finish' }, ]; const stream = createUIMessageStream(inputChunks); const resultStream = handleUIMessageStreamFinish<UIMessage>({ stream, messageId: 'msg-789', originalMessages: [], onError: mockErrorHandler, onFinish: onFinishCallback, }); await convertReadableStreamToArray(resultStream); expect(onFinishCallback).toHaveBeenCalledTimes(1); const callArgs = onFinishCallback.mock.calls[0][0]; expect(callArgs.isContinuation).toBe(false); expect(callArgs.messages).toHaveLength(1); expect(callArgs.messages[0]).toEqual(callArgs.responseMessage); }); }); describe('stream processing with continuation scenario', () => { it('should handle continuation when last message is assistant', async () => { const onFinishCallback = vi.fn(); const inputChunks: UIMessageChunk[] = [ { type: 'start', messageId: 'assistant-msg-1' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: ' continued' }, { type: 'text-end', id: 'text-1' }, { type: 'finish' }, ]; const originalMessages: UIMessage[] = [ { id: 'user-msg-1', role: 'user', parts: [{ type: 'text', text: 'Continue this' }], }, { id: 'assistant-msg-1', role: 'assistant', parts: [{ type: 'text', text: 'This is' }], }, ]; const stream = createUIMessageStream(inputChunks); const resultStream = handleUIMessageStreamFinish<UIMessage>({ stream, messageId: 'msg-999', // this should be ignored since we're continuing originalMessages, onError: mockErrorHandler, onFinish: onFinishCallback, }); await convertReadableStreamToArray(resultStream); expect(onFinishCallback).toHaveBeenCalledTimes(1); const callArgs = onFinishCallback.mock.calls[0][0]; expect(callArgs.isContinuation).toBe(true); expect(callArgs.responseMessage.id).toBe('assistant-msg-1'); // uses the existing assistant message id expect(callArgs.messages).toHaveLength(2); // original user message + updated assistant message expect(callArgs.messages[0]).toEqual(originalMessages[0]); expect(callArgs.messages[1]).toEqual(callArgs.responseMessage); }); it('should not treat user message as continuation', async () => { const onFinishCallback = vi.fn(); const inputChunks: UIMessageChunk[] = [ { type: 'start', messageId: 'msg-001' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'New response' }, { type: 'text-end', id: 'text-1' }, { type: 'finish' }, ]; const originalMessages: UIMessage[] = [ { id: 'user-msg-1', role: 'user', parts: [{ type: 'text', text: 'Question' }], }, { id: 'user-msg-2', role: 'user', parts: [{ type: 'text', text: 'Another question' }], }, ]; const stream = createUIMessageStream(inputChunks); const resultStream = handleUIMessageStreamFinish<UIMessage>({ stream, messageId: 'msg-001', originalMessages, onError: mockErrorHandler, onFinish: onFinishCallback, }); await convertReadableStreamToArray(resultStream); expect(onFinishCallback).toHaveBeenCalledTimes(1); const callArgs = onFinishCallback.mock.calls[0][0]; expect(callArgs.isContinuation).toBe(false); expect(callArgs.responseMessage.id).toBe('msg-001'); expect(callArgs.messages).toHaveLength(3); // 2 user messages + 1 new assistant message }); }); describe('abort scenarios', () => { it('should set isAborted to true when abort chunk is encountered', async () => { const onFinishCallback = vi.fn(); const inputChunks: UIMessageChunk[] = [ { type: 'start', messageId: 'msg-abort-1' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'Starting text' }, { type: 'abort' }, { type: 'finish' }, ]; const originalMessages: UIMessage[] = [ { id: 'user-msg-1', role: 'user', parts: [{ type: 'text', text: 'Test request' }], }, ]; const stream = createUIMessageStream(inputChunks); const resultStream = handleUIMessageStreamFinish<UIMessage>({ stream, messageId: 'msg-abort-1', originalMessages, onError: mockErrorHandler, onFinish: onFinishCallback, }); const result = await convertReadableStreamToArray(resultStream); expect(result).toEqual(inputChunks); expect(onFinishCallback).toHaveBeenCalledTimes(1); const callArgs = onFinishCallback.mock.calls[0][0]; expect(callArgs.isAborted).toBe(true); expect(callArgs.isContinuation).toBe(false); expect(callArgs.responseMessage.id).toBe('msg-abort-1'); expect(callArgs.messages).toHaveLength(2); }); it('should set isAborted to false when no abort chunk is encountered', async () => { const onFinishCallback = vi.fn(); const inputChunks: UIMessageChunk[] = [ { type: 'start', messageId: 'msg-normal' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'Complete text' }, { type: 'text-end', id: 'text-1' }, { type: 'finish' }, ]; const originalMessages: UIMessage[] = [ { id: 'user-msg-1', role: 'user', parts: [{ type: 'text', text: 'Test request' }], }, ]; const stream = createUIMessageStream(inputChunks); const resultStream = handleUIMessageStreamFinish<UIMessage>({ stream, messageId: 'msg-normal', originalMessages, onError: mockErrorHandler, onFinish: onFinishCallback, }); await convertReadableStreamToArray(resultStream); expect(onFinishCallback).toHaveBeenCalledTimes(1); const callArgs = onFinishCallback.mock.calls[0][0]; expect(callArgs.isAborted).toBe(false); expect(callArgs.isContinuation).toBe(false); expect(callArgs.responseMessage.id).toBe('msg-normal'); }); it('should handle abort chunk in pass-through mode without onFinish', async () => { const inputChunks: UIMessageChunk[] = [ { type: 'start', messageId: 'msg-abort-passthrough' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'Text before abort' }, { type: 'abort' }, { type: 'finish' }, ]; const stream = createUIMessageStream(inputChunks); const resultStream = handleUIMessageStreamFinish<UIMessage>({ stream, messageId: 'msg-abort-passthrough', originalMessages: [], onError: mockErrorHandler, // onFinish is not provided }); const result = await convertReadableStreamToArray(resultStream); expect(result).toEqual(inputChunks); expect(mockErrorHandler).not.toHaveBeenCalled(); }); it('should handle multiple abort chunks correctly', async () => { const onFinishCallback = vi.fn(); const inputChunks: UIMessageChunk[] = [ { type: 'start', messageId: 'msg-multiple-abort' }, { type: 'text-start', id: 'text-1' }, { type: 'abort' }, { type: 'text-delta', id: 'text-1', delta: 'Some text' }, { type: 'abort' }, { type: 'finish' }, ]; const stream = createUIMessageStream(inputChunks); const resultStream = handleUIMessageStreamFinish<UIMessage>({ stream, messageId: 'msg-multiple-abort', originalMessages: [], onError: mockErrorHandler, onFinish: onFinishCallback, }); const result = await convertReadableStreamToArray(resultStream); expect(result).toEqual(inputChunks); expect(onFinishCallback).toHaveBeenCalledTimes(1); const callArgs = onFinishCallback.mock.calls[0][0]; expect(callArgs.isAborted).toBe(true); }); }); }); --- File: /ai/packages/ai/src/ui-message-stream/handle-ui-message-stream-finish.ts --- import { createStreamingUIMessageState, processUIMessageStream, StreamingUIMessageState, } from '../ui/process-ui-message-stream'; import { UIMessage } from '../ui/ui-messages'; import { ErrorHandler } from '../util/error-handler'; import { InferUIMessageChunk, UIMessageChunk } from './ui-message-chunks'; import { UIMessageStreamOnFinishCallback } from './ui-message-stream-on-finish-callback'; export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({ messageId, originalMessages = [], onFinish, onError, stream, }: { stream: ReadableStream<InferUIMessageChunk<UI_MESSAGE>>; /** * The message ID to use for the response message. * If not provided, no id will be set for the response message. */ messageId?: string; /** * The original messages. */ originalMessages?: UI_MESSAGE[]; onError: ErrorHandler; onFinish?: UIMessageStreamOnFinishCallback<UI_MESSAGE>; }): ReadableStream<InferUIMessageChunk<UI_MESSAGE>> { // last message is only relevant for assistant messages let lastMessage: UI_MESSAGE | undefined = originalMessages?.[originalMessages.length - 1]; if (lastMessage?.role !== 'assistant') { lastMessage = undefined; } else { // appending to the last message, so we need to use the same id messageId = lastMessage.id; } let isAborted = false; const idInjectedStream = stream.pipeThrough( new TransformStream< InferUIMessageChunk<UI_MESSAGE>, InferUIMessageChunk<UI_MESSAGE> >({ transform(chunk, controller) { // when there is no messageId in the start chunk, // but the user checked for persistence, // inject the messageId into the chunk if (chunk.type === 'start') { const startChunk = chunk as UIMessageChunk & { type: 'start' }; if (startChunk.messageId == null && messageId != null) { startChunk.messageId = messageId; } } if (chunk.type === 'abort') { isAborted = true; } controller.enqueue(chunk); }, }), ); if (onFinish == null) { return idInjectedStream; } const state = createStreamingUIMessageState<UI_MESSAGE>({ lastMessage: lastMessage ? (structuredClone(lastMessage) as UI_MESSAGE) : undefined, messageId: messageId ?? '', // will be overridden by the stream }); const runUpdateMessageJob = async ( job: (options: { state: StreamingUIMessageState<UI_MESSAGE>; write: () => void; }) => Promise<void>, ) => { await job({ state, write: () => {} }); }; return processUIMessageStream<UI_MESSAGE>({ stream: idInjectedStream, runUpdateMessageJob, onError, }).pipeThrough( new TransformStream< InferUIMessageChunk<UI_MESSAGE>, InferUIMessageChunk<UI_MESSAGE> >({ transform(chunk, controller) { controller.enqueue(chunk); }, async flush() { const isContinuation = state.message.id === lastMessage?.id; await onFinish({ isAborted, isContinuation, responseMessage: state.message as UI_MESSAGE, messages: [ ...(isContinuation ? originalMessages.slice(0, -1) : originalMessages), state.message, ] as UI_MESSAGE[], }); }, }), ); } --- File: /ai/packages/ai/src/ui-message-stream/index.ts --- export { createUIMessageStream } from './create-ui-message-stream'; export { createUIMessageStreamResponse } from './create-ui-message-stream-response'; export { JsonToSseTransformStream } from './json-to-sse-transform-stream'; export { pipeUIMessageStreamToResponse } from './pipe-ui-message-stream-to-response'; export { readUIMessageStream } from './read-ui-message-stream'; export type { InferUIMessageChunk, UIMessageChunk } from './ui-message-chunks'; export { UI_MESSAGE_STREAM_HEADERS } from './ui-message-stream-headers'; export type { UIMessageStreamOnFinishCallback } from './ui-message-stream-on-finish-callback'; export type { UIMessageStreamWriter } from './ui-message-stream-writer'; --- File: /ai/packages/ai/src/ui-message-stream/json-to-sse-transform-stream.ts --- export class JsonToSseTransformStream extends TransformStream<unknown, string> { constructor() { super({ transform(part, controller) { controller.enqueue(`data: ${JSON.stringify(part)}\n\n`); }, flush(controller) { controller.enqueue('data: [DONE]\n\n'); }, }); } } --- File: /ai/packages/ai/src/ui-message-stream/pipe-ui-message-stream-to-response.test.ts --- import { convertArrayToReadableStream } from '@ai-sdk/provider-utils/test'; import { createMockServerResponse } from '../test/mock-server-response'; import { pipeUIMessageStreamToResponse } from './pipe-ui-message-stream-to-response'; describe('pipeUIMessageStreamToResponse', () => { it('should write to ServerResponse with correct headers and encoded stream', async () => { const mockResponse = createMockServerResponse(); pipeUIMessageStreamToResponse({ response: mockResponse, status: 200, statusText: 'OK', headers: { 'Custom-Header': 'test', }, stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'test-data' }, { type: 'text-end', id: '1' }, ]), }); // Wait for the stream to finish writing await mockResponse.waitForEnd(); // Verify response properties expect(mockResponse.statusCode).toBe(200); expect(mockResponse.statusMessage).toBe('OK'); // Verify headers expect(mockResponse.headers).toMatchInlineSnapshot(` { "cache-control": "no-cache", "connection": "keep-alive", "content-type": "text/event-stream", "custom-header": "test", "x-accel-buffering": "no", "x-vercel-ai-ui-message-stream": "v1", } `); // Verify written data using decoded chunks const decodedChunks = mockResponse.getDecodedChunks(); expect(decodedChunks).toMatchInlineSnapshot(` [ "data: {"type":"text-start","id":"1"} ", "data: {"type":"text-delta","id":"1","delta":"test-data"} ", "data: {"type":"text-end","id":"1"} ", "data: [DONE] ", ] `); }); it('should handle errors in the stream', async () => { const mockResponse = createMockServerResponse(); pipeUIMessageStreamToResponse({ response: mockResponse, status: 200, stream: convertArrayToReadableStream([ { type: 'error', errorText: 'Custom error message' }, ]), }); // Wait for the stream to finish writing await mockResponse.waitForEnd(); // Verify error handling using decoded chunks const decodedChunks = mockResponse.getDecodedChunks(); expect(decodedChunks).toMatchInlineSnapshot(` [ "data: {"type":"error","errorText":"Custom error message"} ", "data: [DONE] ", ] `); }); }); --- File: /ai/packages/ai/src/ui-message-stream/pipe-ui-message-stream-to-response.ts --- import { ServerResponse } from 'node:http'; import { prepareHeaders } from '../util/prepare-headers'; import { writeToServerResponse } from '../util/write-to-server-response'; import { JsonToSseTransformStream } from './json-to-sse-transform-stream'; import { UI_MESSAGE_STREAM_HEADERS } from './ui-message-stream-headers'; import { UIMessageChunk } from './ui-message-chunks'; import { UIMessageStreamResponseInit } from './ui-message-stream-response-init'; export function pipeUIMessageStreamToResponse({ response, status, statusText, headers, stream, consumeSseStream, }: { response: ServerResponse; stream: ReadableStream<UIMessageChunk>; } & UIMessageStreamResponseInit): void { let sseStream = stream.pipeThrough(new JsonToSseTransformStream()); // when the consumeSseStream is provided, we need to tee the stream // and send the second part to the consumeSseStream function // so that it can be consumed by the client independently if (consumeSseStream) { const [stream1, stream2] = sseStream.tee(); sseStream = stream1; consumeSseStream({ stream: stream2 }); // no await (do not block the response) } writeToServerResponse({ response, status, statusText, headers: Object.fromEntries( prepareHeaders(headers, UI_MESSAGE_STREAM_HEADERS).entries(), ), stream: sseStream.pipeThrough(new TextEncoderStream()), }); } --- File: /ai/packages/ai/src/ui-message-stream/read-ui-message-stream.test.ts --- import { convertArrayToReadableStream, convertAsyncIterableToArray, } from '@ai-sdk/provider-utils/test'; import { UIMessageChunk } from './ui-message-chunks'; import { readUIMessageStream } from './read-ui-message-stream'; function createUIMessageStream(parts: UIMessageChunk[]) { return convertArrayToReadableStream(parts); } describe('readUIMessageStream', () => { it('should return a ui message object stream for a basic input stream', async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'start-step' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'Hello, ' }, { type: 'text-delta', id: 'text-1', delta: 'world!' }, { type: 'text-end', id: 'text-1' }, { type: 'finish-step' }, { type: 'finish' }, ]); const uiMessages = readUIMessageStream({ stream }); expect(await convertAsyncIterableToArray(uiMessages)) .toMatchInlineSnapshot(` [ { "id": "msg-123", "metadata": undefined, "parts": [], "role": "assistant", }, { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "", "type": "text", }, ], "role": "assistant", }, { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "Hello, ", "type": "text", }, ], "role": "assistant", }, { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "streaming", "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, { "id": "msg-123", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Hello, world!", "type": "text", }, ], "role": "assistant", }, ] `); }); it('should throw an error when encountering an error UI stream part', async () => { const stream = createUIMessageStream([ { type: 'start', messageId: 'msg-123' }, { type: 'text-start', id: 'text-1' }, { type: 'text-delta', id: 'text-1', delta: 'Hello' }, { type: 'error', errorText: 'Test error message' }, ]); const uiMessages = readUIMessageStream({ stream, terminateOnError: true, }); await expect(convertAsyncIterableToArray(uiMessages)).rejects.toThrow( 'Test error message', ); }); }); --- File: /ai/packages/ai/src/ui-message-stream/read-ui-message-stream.ts --- import { UIMessage } from '../ui/ui-messages'; import { UIMessageChunk } from './ui-message-chunks'; import { createStreamingUIMessageState, processUIMessageStream, StreamingUIMessageState, } from '../ui/process-ui-message-stream'; import { AsyncIterableStream, createAsyncIterableStream, } from '../util/async-iterable-stream'; import { consumeStream } from '../util/consume-stream'; /** * Transforms a stream of `UIMessageChunk`s into an `AsyncIterableStream` of `UIMessage`s. * * @param options.message - The last assistant message to use as a starting point when the conversation is resumed. Otherwise undefined. * @param options.stream - The stream of `UIMessageChunk`s to read. * @param options.terminateOnError - Whether to terminate the stream if an error occurs. * @param options.onError - A function that is called when an error occurs. * * @returns An `AsyncIterableStream` of `UIMessage`s. Each stream part is a different state of the same message * as it is being completed. */ export function readUIMessageStream<UI_MESSAGE extends UIMessage>({ message, stream, onError, terminateOnError = false, }: { message?: UI_MESSAGE; stream: ReadableStream<UIMessageChunk>; onError?: (error: unknown) => void; terminateOnError?: boolean; }): AsyncIterableStream<UI_MESSAGE> { let controller: ReadableStreamDefaultController<UI_MESSAGE> | undefined; let hasErrored = false; const outputStream = new ReadableStream<UI_MESSAGE>({ start(controllerParam) { controller = controllerParam; }, }); const state = createStreamingUIMessageState<UI_MESSAGE>({ messageId: message?.id ?? '', lastMessage: message, }); const handleError = (error: unknown) => { onError?.(error); if (!hasErrored && terminateOnError) { hasErrored = true; controller?.error(error); } }; consumeStream({ stream: processUIMessageStream({ stream, runUpdateMessageJob( job: (options: { state: StreamingUIMessageState<UI_MESSAGE>; write: () => void; }) => Promise<void>, ) { return job({ state, write: () => { controller?.enqueue(structuredClone(state.message)); }, }); }, onError: handleError, }), onError: handleError, }).finally(() => { // Only close if no error occurred. Calling close() on an errored controller // throws "Invalid state: Controller is already closed" TypeError. if (!hasErrored) { controller?.close(); } }); return createAsyncIterableStream(outputStream); } --- File: /ai/packages/ai/src/ui-message-stream/ui-message-chunks.ts --- import { z } from 'zod/v4'; import { ProviderMetadata, providerMetadataSchema, } from '../types/provider-metadata'; import { InferUIMessageData, InferUIMessageMetadata, UIDataTypes, UIMessage, } from '../ui/ui-messages'; import { ValueOf } from '../util/value-of'; export const uiMessageChunkSchema = z.union([ z.strictObject({ type: z.literal('text-start'), id: z.string(), providerMetadata: providerMetadataSchema.optional(), }), z.strictObject({ type: z.literal('text-delta'), id: z.string(), delta: z.string(), providerMetadata: providerMetadataSchema.optional(), }), z.strictObject({ type: z.literal('text-end'), id: z.string(), providerMetadata: providerMetadataSchema.optional(), }), z.strictObject({ type: z.literal('error'), errorText: z.string(), }), z.strictObject({ type: z.literal('tool-input-start'), toolCallId: z.string(), toolName: z.string(), providerExecuted: z.boolean().optional(), dynamic: z.boolean().optional(), }), z.strictObject({ type: z.literal('tool-input-delta'), toolCallId: z.string(), inputTextDelta: z.string(), }), z.strictObject({ type: z.literal('tool-input-available'), toolCallId: z.string(), toolName: z.string(), input: z.unknown(), providerExecuted: z.boolean().optional(), providerMetadata: providerMetadataSchema.optional(), dynamic: z.boolean().optional(), }), z.strictObject({ type: z.literal('tool-input-error'), toolCallId: z.string(), toolName: z.string(), input: z.unknown(), providerExecuted: z.boolean().optional(), providerMetadata: providerMetadataSchema.optional(), dynamic: z.boolean().optional(), errorText: z.string(), }), z.strictObject({ type: z.literal('tool-output-available'), toolCallId: z.string(), output: z.unknown(), providerExecuted: z.boolean().optional(), dynamic: z.boolean().optional(), }), z.strictObject({ type: z.literal('tool-output-error'), toolCallId: z.string(), errorText: z.string(), providerExecuted: z.boolean().optional(), dynamic: z.boolean().optional(), }), z.strictObject({ type: z.literal('reasoning'), text: z.string(), providerMetadata: providerMetadataSchema.optional(), }), z.strictObject({ type: z.literal('reasoning-start'), id: z.string(), providerMetadata: providerMetadataSchema.optional(), }), z.strictObject({ type: z.literal('reasoning-delta'), id: z.string(), delta: z.string(), providerMetadata: providerMetadataSchema.optional(), }), z.strictObject({ type: z.literal('reasoning-end'), id: z.string(), providerMetadata: providerMetadataSchema.optional(), }), z.strictObject({ type: z.literal('reasoning-part-finish'), }), z.strictObject({ type: z.literal('source-url'), sourceId: z.string(), url: z.string(), title: z.string().optional(), providerMetadata: providerMetadataSchema.optional(), }), z.strictObject({ type: z.literal('source-document'), sourceId: z.string(), mediaType: z.string(), title: z.string(), filename: z.string().optional(), providerMetadata: providerMetadataSchema.optional(), }), z.strictObject({ type: z.literal('file'), url: z.string(), mediaType: z.string(), providerMetadata: providerMetadataSchema.optional(), }), z.strictObject({ type: z.string().startsWith('data-'), id: z.string().optional(), data: z.unknown(), transient: z.boolean().optional(), }), z.strictObject({ type: z.literal('start-step'), }), z.strictObject({ type: z.literal('finish-step'), }), z.strictObject({ type: z.literal('start'), messageId: z.string().optional(), messageMetadata: z.unknown().optional(), }), z.strictObject({ type: z.literal('finish'), messageMetadata: z.unknown().optional(), }), z.strictObject({ type: z.literal('abort'), }), z.strictObject({ type: z.literal('message-metadata'), messageMetadata: z.unknown(), }), ]); export type DataUIMessageChunk<DATA_TYPES extends UIDataTypes> = ValueOf<{ [NAME in keyof DATA_TYPES & string]: { type: `data-${NAME}`; id?: string; data: DATA_TYPES[NAME]; transient?: boolean; }; }>; export type UIMessageChunk< METADATA = unknown, DATA_TYPES extends UIDataTypes = UIDataTypes, > = | { type: 'text-start'; id: string; providerMetadata?: ProviderMetadata; } | { type: 'text-delta'; delta: string; id: string; providerMetadata?: ProviderMetadata; } | { type: 'text-end'; id: string; providerMetadata?: ProviderMetadata; } | { type: 'reasoning-start'; id: string; providerMetadata?: ProviderMetadata; } | { type: 'reasoning-delta'; id: string; delta: string; providerMetadata?: ProviderMetadata; } | { type: 'reasoning-end'; id: string; providerMetadata?: ProviderMetadata; } | { type: 'error'; errorText: string; } | { type: 'tool-input-available'; toolCallId: string; toolName: string; input: unknown; providerExecuted?: boolean; providerMetadata?: ProviderMetadata; dynamic?: boolean; } | { type: 'tool-input-error'; toolCallId: string; toolName: string; input: unknown; providerExecuted?: boolean; providerMetadata?: ProviderMetadata; dynamic?: boolean; errorText: string; } | { type: 'tool-output-available'; toolCallId: string; output: unknown; providerExecuted?: boolean; dynamic?: boolean; } | { type: 'tool-output-error'; toolCallId: string; errorText: string; providerExecuted?: boolean; dynamic?: boolean; } | { type: 'tool-input-start'; toolCallId: string; toolName: string; providerExecuted?: boolean; dynamic?: boolean; } | { type: 'tool-input-delta'; toolCallId: string; inputTextDelta: string; } | { type: 'source-url'; sourceId: string; url: string; title?: string; providerMetadata?: ProviderMetadata; } | { type: 'source-document'; sourceId: string; mediaType: string; title: string; filename?: string; providerMetadata?: ProviderMetadata; } | { type: 'file'; url: string; mediaType: string; } | DataUIMessageChunk<DATA_TYPES> | { type: 'start-step'; } | { type: 'finish-step'; } | { type: 'start'; messageId?: string; messageMetadata?: METADATA; } | { type: 'finish'; messageMetadata?: METADATA; } | { type: 'abort'; } | { type: 'message-metadata'; messageMetadata: METADATA; }; export function isDataUIMessageChunk( chunk: UIMessageChunk, ): chunk is DataUIMessageChunk<UIDataTypes> { return chunk.type.startsWith('data-'); } export type InferUIMessageChunk<T extends UIMessage> = UIMessageChunk< InferUIMessageMetadata<T>, InferUIMessageData<T> >; --- File: /ai/packages/ai/src/ui-message-stream/ui-message-stream-headers.ts --- export const UI_MESSAGE_STREAM_HEADERS = { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', 'x-vercel-ai-ui-message-stream': 'v1', 'x-accel-buffering': 'no', // disable nginx buffering }; --- File: /ai/packages/ai/src/ui-message-stream/ui-message-stream-on-finish-callback.ts --- import { UIMessage } from '../ui/ui-messages'; export type UIMessageStreamOnFinishCallback<UI_MESSAGE extends UIMessage> = (event: { /** * The updated list of UI messages. */ messages: UI_MESSAGE[]; /** * Indicates whether the response message is a continuation of the last original message, * or if a new message was created. */ isContinuation: boolean; /** * Indicates whether the stream was aborted. */ isAborted: boolean; /** * The message that was sent to the client as a response * (including the original message if it was extended). */ responseMessage: UI_MESSAGE; }) => PromiseLike<void> | void; --- File: /ai/packages/ai/src/ui-message-stream/ui-message-stream-response-init.ts --- export type UIMessageStreamResponseInit = ResponseInit & { consumeSseStream?: (options: { stream: ReadableStream<string>; }) => PromiseLike<void> | void; }; --- File: /ai/packages/ai/src/ui-message-stream/ui-message-stream-writer.ts --- import { UIMessage } from '../ui'; import { ErrorHandler } from '../util/error-handler'; import { InferUIMessageChunk } from './ui-message-chunks'; export interface UIMessageStreamWriter< UI_MESSAGE extends UIMessage = UIMessage, > { /** * Appends a data stream part to the stream. */ write(part: InferUIMessageChunk<UI_MESSAGE>): void; /** * Merges the contents of another stream to this stream. */ merge(stream: ReadableStream<InferUIMessageChunk<UI_MESSAGE>>): void; /** * Error handler that is used by the data stream writer. * This is intended for forwarding when merging streams * to prevent duplicated error masking. */ onError: ErrorHandler | undefined; } --- File: /ai/packages/ai/src/util/as-array.ts --- export function asArray<T>(value: T | T[] | undefined): T[] { return value === undefined ? [] : Array.isArray(value) ? value : [value]; } --- File: /ai/packages/ai/src/util/async-iterable-stream.test.ts --- import { convertArrayToReadableStream, convertAsyncIterableToArray, convertReadableStreamToArray, } from '@ai-sdk/provider-utils/test'; import { describe, expect, it } from 'vitest'; import { createAsyncIterableStream } from './async-iterable-stream'; describe('createAsyncIterableStream()', () => { it('should read all chunks from a non-empty stream using async iteration', async () => { const testData = ['Hello', 'World', 'Stream']; const source = convertArrayToReadableStream(testData); const asyncIterableStream = createAsyncIterableStream(source); expect(await convertAsyncIterableToArray(asyncIterableStream)).toEqual( testData, ); }); it('should handle an empty stream gracefully', async () => { const source = convertArrayToReadableStream<string>([]); const asyncIterableStream = createAsyncIterableStream(source); expect(await convertAsyncIterableToArray(asyncIterableStream)).toEqual([]); }); it('should maintain ReadableStream functionality', async () => { const testData = ['Hello', 'World']; const source = convertArrayToReadableStream(testData); const asyncIterableStream = createAsyncIterableStream(source); expect(await convertReadableStreamToArray(asyncIterableStream)).toEqual( testData, ); }); }); --- File: /ai/packages/ai/src/util/async-iterable-stream.ts --- export type AsyncIterableStream<T> = AsyncIterable<T> & ReadableStream<T>; export function createAsyncIterableStream<T>( source: ReadableStream<T>, ): AsyncIterableStream<T> { const stream = source.pipeThrough(new TransformStream<T, T>()); (stream as AsyncIterableStream<T>)[Symbol.asyncIterator] = () => { const reader = stream.getReader(); return { async next(): Promise<IteratorResult<T>> { const { done, value } = await reader.read(); return done ? { done: true, value: undefined } : { done: false, value }; }, }; }; return stream as AsyncIterableStream<T>; } --- File: /ai/packages/ai/src/util/consume-stream.ts --- /** * Consumes a ReadableStream until it's fully read. * * This function reads the stream chunk by chunk until the stream is exhausted. * It doesn't process or return the data from the stream; it simply ensures * that the entire stream is read. * * @param {ReadableStream} stream - The ReadableStream to be consumed. * @returns {Promise<void>} A promise that resolves when the stream is fully consumed. */ export async function consumeStream({ stream, onError, }: { stream: ReadableStream; onError?: (error: unknown) => void; }): Promise<void> { const reader = stream.getReader(); try { while (true) { const { done } = await reader.read(); if (done) break; } } catch (error) { onError?.(error); } finally { reader.releaseLock(); } } --- File: /ai/packages/ai/src/util/cosine-similarity.test.ts --- import { cosineSimilarity } from './cosine-similarity'; it('should calculate cosine similarity correctly', () => { const vector1 = [1, 2, 3]; const vector2 = [4, 5, 6]; const result = cosineSimilarity(vector1, vector2); // test against pre-calculated value: expect(result).toBeCloseTo(0.9746318461970762, 5); }); it('should calculate negative cosine similarity correctly', () => { const vector1 = [1, 0]; const vector2 = [-1, 0]; const result = cosineSimilarity(vector1, vector2); // test against pre-calculated value: expect(result).toBeCloseTo(-1, 5); }); it('should throw an error when vectors have different lengths', () => { const vector1 = [1, 2, 3]; const vector2 = [4, 5]; expect(() => cosineSimilarity(vector1, vector2)).toThrowError(); }); it('should give 0 when one of the vectors is a zero vector', () => { const vector1 = [0, 1, 2]; const vector2 = [0, 0, 0]; const result = cosineSimilarity(vector1, vector2); expect(result).toBe(0); const result2 = cosineSimilarity(vector2, vector1); expect(result2).toBe(0); }); it('should handle vectors with very small magnitudes', () => { const vector1 = [1e-10, 0, 0]; const vector2 = [2e-10, 0, 0]; const result = cosineSimilarity(vector1, vector2); expect(result).toBe(1); const vector3 = [1e-10, 0, 0]; const vector4 = [-1e-10, 0, 0]; const result2 = cosineSimilarity(vector3, vector4); expect(result2).toBe(-1); }); --- File: /ai/packages/ai/src/util/cosine-similarity.ts --- import { InvalidArgumentError } from '../error/invalid-argument-error'; /** * Calculates the cosine similarity between two vectors. This is a useful metric for * comparing the similarity of two vectors such as embeddings. * * @param vector1 - The first vector. * @param vector2 - The second vector. * * @returns The cosine similarity between vector1 and vector2. * @returns 0 if either vector is the zero vector. * * @throws {InvalidArgumentError} If the vectors do not have the same length. */ export function cosineSimilarity(vector1: number[], vector2: number[]): number { if (vector1.length !== vector2.length) { throw new InvalidArgumentError({ parameter: 'vector1,vector2', value: { vector1Length: vector1.length, vector2Length: vector2.length }, message: `Vectors must have the same length`, }); } const n = vector1.length; if (n === 0) { return 0; // Return 0 for empty vectors if no error is thrown } let magnitudeSquared1 = 0; let magnitudeSquared2 = 0; let dotProduct = 0; for (let i = 0; i < n; i++) { const value1 = vector1[i]; const value2 = vector2[i]; magnitudeSquared1 += value1 * value1; magnitudeSquared2 += value2 * value2; dotProduct += value1 * value2; } return magnitudeSquared1 === 0 || magnitudeSquared2 === 0 ? 0 : dotProduct / (Math.sqrt(magnitudeSquared1) * Math.sqrt(magnitudeSquared2)); } --- File: /ai/packages/ai/src/util/create-resolvable-promise.ts --- import { ErrorHandler } from './error-handler'; /** * Creates a Promise with externally accessible resolve and reject functions. * * @template T - The type of the value that the Promise will resolve to. * @returns An object containing: * - promise: A Promise that can be resolved or rejected externally. * - resolve: A function to resolve the Promise with a value of type T. * - reject: A function to reject the Promise with an error. */ export function createResolvablePromise<T = any>(): { promise: Promise<T>; resolve: (value: T) => void; reject: ErrorHandler; } { let resolve: (value: T) => void; let reject: ErrorHandler; const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve: resolve!, reject: reject!, }; } --- File: /ai/packages/ai/src/util/create-stitchable-stream.test.ts --- import { convertArrayToReadableStream, convertReadableStreamToArray, } from '@ai-sdk/provider-utils/test'; import { expect } from 'vitest'; import { createStitchableStream } from './create-stitchable-stream'; describe('createStitchableStream', () => { describe('read full streams after they are added', () => { it('should return no stream when immediately closed', async () => { const { stream, close } = createStitchableStream<number>(); close(); expect(await convertReadableStreamToArray(stream)).toEqual([]); }); it('should return all values from a single inner stream', async () => { const { stream, addStream, close } = createStitchableStream<number>(); addStream(convertArrayToReadableStream([1, 2, 3])); close(); expect(await convertReadableStreamToArray(stream)).toEqual([1, 2, 3]); }); it('should return all values from 2 inner streams', async () => { const { stream, addStream, close } = createStitchableStream<number>(); addStream(convertArrayToReadableStream([1, 2, 3])); addStream(convertArrayToReadableStream([4, 5, 6])); close(); expect(await convertReadableStreamToArray(stream)).toEqual([ 1, 2, 3, 4, 5, 6, ]); }); it('should return all values from 3 inner streams', async () => { const { stream, addStream, close } = createStitchableStream<number>(); addStream(convertArrayToReadableStream([1, 2, 3])); addStream(convertArrayToReadableStream([4, 5, 6])); addStream(convertArrayToReadableStream([7, 8, 9])); close(); expect(await convertReadableStreamToArray(stream)).toEqual([ 1, 2, 3, 4, 5, 6, 7, 8, 9, ]); }); it('should handle empty inner streams', async () => { const { stream, addStream, close } = createStitchableStream<number>(); addStream(convertArrayToReadableStream([])); addStream(convertArrayToReadableStream([1, 2])); addStream(convertArrayToReadableStream([])); addStream(convertArrayToReadableStream([3, 4])); close(); expect(await convertReadableStreamToArray(stream)).toEqual([1, 2, 3, 4]); }); it('should handle reading a single value before it is added', async () => { const { stream, addStream, close } = createStitchableStream<number>(); // Start reading before any values are added const reader = stream.getReader(); const readPromise = reader.read(); // Add value with delay after starting read Promise.resolve().then(() => { addStream(convertArrayToReadableStream([42])); close(); }); // Value should be returned once available expect(await readPromise).toEqual({ done: false, value: 42 }); // Stream should complete after value is read expect(await reader.read()).toEqual({ done: true, value: undefined }); }); }); describe('read from partial stream and with interruptions', async () => { it('should return all values from 2 inner streams', async () => { const { stream, addStream, close } = createStitchableStream<number>(); // read 5 values from the stream before they are added // (added asynchronously) const reader = stream.getReader(); const results: Array<{ done: boolean; value?: number }> = []; for (let i = 0; i < 5; i++) { reader.read().then(result => { results.push(result); }); } addStream(convertArrayToReadableStream([1, 2, 3])); addStream(convertArrayToReadableStream([4, 5])); close(); // wait for the stream to finish via await: expect(await reader.read()).toEqual({ done: true, value: undefined }); expect(results).toEqual([ { done: false, value: 1 }, { done: false, value: 2 }, { done: false, value: 3 }, { done: false, value: 4 }, { done: false, value: 5 }, ]); }); }); describe('error handling', () => { it('should handle errors from inner streams', async () => { const { stream, addStream, close } = createStitchableStream<number>(); const errorStream = new ReadableStream({ start(controller) { controller.error(new Error('Test error')); }, }); addStream(convertArrayToReadableStream([1, 2])); addStream(errorStream); addStream(convertArrayToReadableStream([3, 4])); close(); await expect(convertReadableStreamToArray(stream)).rejects.toThrow( 'Test error', ); }); }); describe('cancellation & closing', () => { it('should cancel all inner streams when cancelled', async () => { const { stream, addStream } = createStitchableStream<number>(); let stream1Cancelled = false; let stream2Cancelled = false; const mockStream1 = new ReadableStream({ start(controller) { controller.enqueue(1); controller.enqueue(2); }, cancel() { stream1Cancelled = true; }, }); const mockStream2 = new ReadableStream({ start(controller) { controller.enqueue(3); controller.enqueue(4); }, cancel() { stream2Cancelled = true; }, }); addStream(mockStream1); addStream(mockStream2); await stream.cancel(); expect(stream1Cancelled).toBe(true); expect(stream2Cancelled).toBe(true); }); it('should throw an error when adding a stream after closing', async () => { const { addStream, close } = createStitchableStream<number>(); close(); expect(() => addStream(convertArrayToReadableStream([1, 2]))).toThrow( 'Cannot add inner stream: outer stream is closed', ); }); describe('terminate', () => { it('should immediately close the stream and cancel all inner streams', async () => { const { stream, addStream, terminate } = createStitchableStream<number>(); let stream1Cancelled = false; let stream2Cancelled = false; const mockStream1 = new ReadableStream({ start(controller) { controller.enqueue(1); controller.enqueue(2); }, cancel() { stream1Cancelled = true; }, }); const mockStream2 = new ReadableStream({ start(controller) { controller.enqueue(3); controller.enqueue(4); }, cancel() { stream2Cancelled = true; }, }); addStream(mockStream1); addStream(mockStream2); // Start reading from the stream const reader = stream.getReader(); const firstRead = await reader.read(); terminate(); // Should immediately close without reading remaining values const finalRead = await reader.read(); expect(firstRead).toEqual({ done: false, value: 1 }); expect(finalRead).toEqual({ done: true, value: undefined }); expect(stream1Cancelled).toBe(true); expect(stream2Cancelled).toBe(true); }); it('should throw an error when adding a stream after terminating', async () => { const { addStream, terminate } = createStitchableStream<number>(); terminate(); expect(() => addStream(convertArrayToReadableStream([1, 2]))).toThrow( 'Cannot add inner stream: outer stream is closed', ); }); }); }); }); --- File: /ai/packages/ai/src/util/create-stitchable-stream.ts --- import { createResolvablePromise } from './create-resolvable-promise'; /** * Creates a stitchable stream that can pipe one stream at a time. * * @template T - The type of values emitted by the streams. * @returns {Object} An object containing the stitchable stream and control methods. */ export function createStitchableStream<T>(): { stream: ReadableStream<T>; addStream: (innerStream: ReadableStream<T>) => void; close: () => void; terminate: () => void; } { let innerStreamReaders: ReadableStreamDefaultReader<T>[] = []; let controller: ReadableStreamDefaultController<T> | null = null; let isClosed = false; let waitForNewStream = createResolvablePromise<void>(); const terminate = () => { isClosed = true; waitForNewStream.resolve(); innerStreamReaders.forEach(reader => reader.cancel()); innerStreamReaders = []; controller?.close(); }; const processPull = async () => { // Case 1: Outer stream is closed and no more inner streams if (isClosed && innerStreamReaders.length === 0) { controller?.close(); return; } // Case 2: No inner streams available, but outer stream is open // wait for a new inner stream to be added or the outer stream to close if (innerStreamReaders.length === 0) { waitForNewStream = createResolvablePromise<void>(); await waitForNewStream.promise; return processPull(); } try { const { value, done } = await innerStreamReaders[0].read(); if (done) { // Case 3: Current inner stream is done innerStreamReaders.shift(); // Remove the finished stream // Continue pulling from the next stream if available if (innerStreamReaders.length > 0) { await processPull(); } else if (isClosed) { controller?.close(); } } else { // Case 4: Current inner stream returns an item controller?.enqueue(value); } } catch (error) { // Case 5: Current inner stream throws an error controller?.error(error); innerStreamReaders.shift(); // Remove the errored stream terminate(); // we have errored, terminate all streams } }; return { stream: new ReadableStream<T>({ start(controllerParam) { controller = controllerParam; }, pull: processPull, async cancel() { for (const reader of innerStreamReaders) { await reader.cancel(); } innerStreamReaders = []; isClosed = true; }, }), addStream: (innerStream: ReadableStream<T>) => { if (isClosed) { throw new Error('Cannot add inner stream: outer stream is closed'); } innerStreamReaders.push(innerStream.getReader()); waitForNewStream.resolve(); }, /** * Gracefully close the outer stream. This will let the inner streams * finish processing and then close the outer stream. */ close: () => { isClosed = true; waitForNewStream.resolve(); if (innerStreamReaders.length === 0) { controller?.close(); } }, /** * Immediately close the outer stream. This will cancel all inner streams * and close the outer stream. */ terminate, }; } --- File: /ai/packages/ai/src/util/data-url.ts --- /** * Converts a data URL of type text/* to a text string. */ export function getTextFromDataUrl(dataUrl: string): string { const [header, base64Content] = dataUrl.split(','); const mediaType = header.split(';')[0].split(':')[1]; if (mediaType == null || base64Content == null) { throw new Error('Invalid data URL format'); } try { return window.atob(base64Content); } catch (error) { throw new Error(`Error decoding data URL`); } } --- File: /ai/packages/ai/src/util/deep-partial.ts --- // License for this File only: // // MIT License // // Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com) // Copyright (c) Vercel, Inc. (https://vercel.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and // to permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial portions // of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. import * as z3 from 'zod/v3'; import * as z4 from 'zod/v4'; /** Create a type from an object with all keys and nested keys set to optional. The helper supports normal objects and Zod schemas (which are resolved automatically). It always recurses into arrays. Adopted from [type-fest](https://github.com/sindresorhus/type-fest/tree/main) PartialDeep. */ export type DeepPartial<T> = T extends z3.ZodTypeAny ? DeepPartialInternal<z3.infer<T>> // resolve Zod schemas first to prevent infinite recursion : T extends z4.core.$ZodType ? DeepPartialInternal<z4.infer<T>> : DeepPartialInternal<T>; type DeepPartialInternal<T> = T extends | null | undefined | string | number | boolean | symbol | bigint | void | Date | RegExp | ((...arguments_: any[]) => unknown) | (new (...arguments_: any[]) => unknown) ? T : T extends Map<infer KeyType, infer ValueType> ? PartialMap<KeyType, ValueType> : T extends Set<infer ItemType> ? PartialSet<ItemType> : T extends ReadonlyMap<infer KeyType, infer ValueType> ? PartialReadonlyMap<KeyType, ValueType> : T extends ReadonlySet<infer ItemType> ? PartialReadonlySet<ItemType> : T extends object ? T extends ReadonlyArray<infer ItemType> // Test for arrays/tuples, per https://github.com/microsoft/TypeScript/issues/35156 ? ItemType[] extends T // Test for arrays (non-tuples) specifically ? readonly ItemType[] extends T // Differentiate readonly and mutable arrays ? ReadonlyArray<DeepPartialInternal<ItemType | undefined>> : Array<DeepPartialInternal<ItemType | undefined>> : PartialObject<T> // Tuples behave properly : PartialObject<T> : unknown; type PartialMap<KeyType, ValueType> = {} & Map< DeepPartialInternal<KeyType>, DeepPartialInternal<ValueType> >; type PartialSet<T> = {} & Set<DeepPartialInternal<T>>; type PartialReadonlyMap<KeyType, ValueType> = {} & ReadonlyMap< DeepPartialInternal<KeyType>, DeepPartialInternal<ValueType> >; type PartialReadonlySet<T> = {} & ReadonlySet<DeepPartialInternal<T>>; type PartialObject<ObjectType extends object> = { [KeyType in keyof ObjectType]?: DeepPartialInternal<ObjectType[KeyType]>; }; --- File: /ai/packages/ai/src/util/delayed-promise.test.ts --- import { DelayedPromise } from './delayed-promise'; import { delay } from '@ai-sdk/provider-utils'; describe('DelayedPromise', () => { it('should resolve when accessed after resolution', async () => { const dp = new DelayedPromise<string>(); dp.resolve('success'); expect(await dp.promise).toBe('success'); }); it('should reject when accessed after rejection', async () => { const dp = new DelayedPromise<string>(); const error = new Error('failure'); dp.reject(error); await expect(dp.promise).rejects.toThrow('failure'); }); it('should resolve when accessed before resolution', async () => { const dp = new DelayedPromise<string>(); const promise = dp.promise; dp.resolve('success'); expect(await promise).toBe('success'); }); it('should reject when accessed before rejection', async () => { const dp = new DelayedPromise<string>(); const promise = dp.promise; const error = new Error('failure'); dp.reject(error); await expect(promise).rejects.toThrow('failure'); }); it('should maintain the resolved state after multiple accesses', async () => { const dp = new DelayedPromise<string>(); dp.resolve('success'); expect(await dp.promise).toBe('success'); expect(await dp.promise).toBe('success'); }); it('should maintain the rejected state after multiple accesses', async () => { const dp = new DelayedPromise<string>(); const error = new Error('failure'); dp.reject(error); await expect(dp.promise).rejects.toThrow('failure'); await expect(dp.promise).rejects.toThrow('failure'); }); it('should block until resolved when accessed before resolution', async () => { const dp = new DelayedPromise<string>(); let resolved = false; // Access the promise before resolving const promise = dp.promise.then(value => { resolved = true; return value; }); // Promise should not be resolved yet expect(resolved).toBe(false); // Wait a bit to ensure it's truly blocking await delay(10); expect(resolved).toBe(false); // Now resolve it dp.resolve('delayed-success'); // Should now resolve const result = await promise; expect(result).toBe('delayed-success'); expect(resolved).toBe(true); }); it('should block until rejected when accessed before rejection', async () => { const dp = new DelayedPromise<string>(); let rejected = false; // Access the promise before rejecting const promise = dp.promise.catch(error => { rejected = true; throw error; }); // Promise should not be rejected yet expect(rejected).toBe(false); // Wait a bit to ensure it's truly blocking await delay(10); expect(rejected).toBe(false); // Now reject it const error = new Error('delayed-failure'); dp.reject(error); // Should now reject await expect(promise).rejects.toThrow('delayed-failure'); expect(rejected).toBe(true); }); it('should resolve all pending promises when resolved after access', async () => { const dp = new DelayedPromise<string>(); const results: string[] = []; // Access the promise multiple times before resolution const promise1 = dp.promise.then(value => { results.push(`first: ${value}`); return value; }); const promise2 = dp.promise.then(value => { results.push(`second: ${value}`); return value; }); // Neither should be resolved yet expect(results).toHaveLength(0); // Wait to ensure they're blocking await delay(10); expect(results).toHaveLength(0); // Resolve the promise dp.resolve('success'); // Both should resolve await Promise.all([promise1, promise2]); expect(results).toHaveLength(2); expect(results).toContain('first: success'); expect(results).toContain('second: success'); }); }); --- File: /ai/packages/ai/src/util/delayed-promise.ts --- /** * Delayed promise. It is only constructed once the value is accessed. * This is useful to avoid unhandled promise rejections when the promise is created * but not accessed. */ export class DelayedPromise<T> { private status: | { type: 'pending' } | { type: 'resolved'; value: T } | { type: 'rejected'; error: unknown } = { type: 'pending' }; private _promise: Promise<T> | undefined; private _resolve: undefined | ((value: T) => void) = undefined; private _reject: undefined | ((error: unknown) => void) = undefined; get promise(): Promise<T> { if (this._promise) { return this._promise; } this._promise = new Promise<T>((resolve, reject) => { if (this.status.type === 'resolved') { resolve(this.status.value); } else if (this.status.type === 'rejected') { reject(this.status.error); } this._resolve = resolve; this._reject = reject; }); return this._promise; } resolve(value: T): void { this.status = { type: 'resolved', value }; if (this._promise) { this._resolve?.(value); } } reject(error: unknown): void { this.status = { type: 'rejected', error }; if (this._promise) { this._reject?.(error); } } } --- File: /ai/packages/ai/src/util/detect-media-type.test.ts --- import { describe, expect, it } from 'vitest'; import { audioMediaTypeSignatures, detectMediaType, imageMediaTypeSignatures, } from './detect-media-type'; import { convertUint8ArrayToBase64 } from '@ai-sdk/provider-utils'; describe('detectMediaType', () => { describe('GIF', () => { it('should detect GIF from bytes', () => { const gifBytes = new Uint8Array([0x47, 0x49, 0x46, 0xff, 0xff]); expect( detectMediaType({ data: gifBytes, signatures: imageMediaTypeSignatures, }), ).toBe('image/gif'); }); it('should detect GIF from base64', () => { const gifBase64 = 'R0lGabc123'; // Base64 string starting with GIF signature expect( detectMediaType({ data: gifBase64, signatures: imageMediaTypeSignatures, }), ).toBe('image/gif'); }); }); describe('PNG', () => { it('should detect PNG from bytes', () => { const pngBytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0xff, 0xff]); expect( detectMediaType({ data: pngBytes, signatures: imageMediaTypeSignatures, }), ).toBe('image/png'); }); it('should detect PNG from base64', () => { const pngBase64 = 'iVBORwabc123'; // Base64 string starting with PNG signature expect( detectMediaType({ data: pngBase64, signatures: imageMediaTypeSignatures, }), ).toBe('image/png'); }); }); describe('JPEG', () => { it('should detect JPEG from bytes', () => { const jpegBytes = new Uint8Array([0xff, 0xd8, 0xff, 0xff]); expect( detectMediaType({ data: jpegBytes, signatures: imageMediaTypeSignatures, }), ).toBe('image/jpeg'); }); it('should detect JPEG from base64', () => { const jpegBase64 = '/9j/abc123'; // Base64 string starting with JPEG signature expect( detectMediaType({ data: jpegBase64, signatures: imageMediaTypeSignatures, }), ).toBe('image/jpeg'); }); }); describe('WebP', () => { it('should detect WebP from bytes', () => { const webpBytes = new Uint8Array([0x52, 0x49, 0x46, 0x46, 0xff, 0xff]); expect( detectMediaType({ data: webpBytes, signatures: imageMediaTypeSignatures, }), ).toBe('image/webp'); }); it('should detect WebP from base64', () => { const webpBase64 = 'UklGRgabc123'; // Base64 string starting with WebP signature expect( detectMediaType({ data: webpBase64, signatures: imageMediaTypeSignatures, }), ).toBe('image/webp'); }); }); describe('BMP', () => { it('should detect BMP from bytes', () => { const bmpBytes = new Uint8Array([0x42, 0x4d, 0xff, 0xff]); expect( detectMediaType({ data: bmpBytes, signatures: imageMediaTypeSignatures, }), ).toBe('image/bmp'); }); it('should detect BMP from base64', () => { const bmpBase64 = 'Qkabc123'; // Base64 string starting with BMP signature expect( detectMediaType({ data: bmpBase64, signatures: imageMediaTypeSignatures, }), ).toBe('image/bmp'); }); }); describe('TIFF', () => { it('should detect TIFF (little endian) from bytes', () => { const tiffLEBytes = new Uint8Array([0x49, 0x49, 0x2a, 0x00, 0xff]); expect( detectMediaType({ data: tiffLEBytes, signatures: imageMediaTypeSignatures, }), ).toBe('image/tiff'); }); it('should detect TIFF (little endian) from base64', () => { const tiffLEBase64 = 'SUkqAAabc123'; // Base64 string starting with TIFF LE signature expect( detectMediaType({ data: tiffLEBase64, signatures: imageMediaTypeSignatures, }), ).toBe('image/tiff'); }); it('should detect TIFF (big endian) from bytes', () => { const tiffBEBytes = new Uint8Array([0x4d, 0x4d, 0x00, 0x2a, 0xff]); expect( detectMediaType({ data: tiffBEBytes, signatures: imageMediaTypeSignatures, }), ).toBe('image/tiff'); }); it('should detect TIFF (big endian) from base64', () => { const tiffBEBase64 = 'TU0AKgabc123'; // Base64 string starting with TIFF BE signature expect( detectMediaType({ data: tiffBEBase64, signatures: imageMediaTypeSignatures, }), ).toBe('image/tiff'); }); }); describe('AVIF', () => { it('should detect AVIF from bytes', () => { const avifBytes = new Uint8Array([ 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66, 0xff, ]); expect( detectMediaType({ data: avifBytes, signatures: imageMediaTypeSignatures, }), ).toBe('image/avif'); }); it('should detect AVIF from base64', () => { const avifBase64 = 'AAAAIGZ0eXBhdmlmabc123'; // Base64 string starting with AVIF signature expect( detectMediaType({ data: avifBase64, signatures: imageMediaTypeSignatures, }), ).toBe('image/avif'); }); }); describe('HEIC', () => { it('should detect HEIC from bytes', () => { const heicBytes = new Uint8Array([ 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63, 0xff, ]); expect( detectMediaType({ data: heicBytes, signatures: imageMediaTypeSignatures, }), ).toBe('image/heic'); }); it('should detect HEIC from base64', () => { const heicBase64 = 'AAAAIGZ0eXBoZWljabc123'; // Base64 string starting with HEIC signature expect( detectMediaType({ data: heicBase64, signatures: imageMediaTypeSignatures, }), ).toBe('image/heic'); }); }); describe('MP3', () => { it('should detect MP3 from bytes', () => { const mp3Bytes = new Uint8Array([0xff, 0xfb]); expect( detectMediaType({ data: mp3Bytes, signatures: audioMediaTypeSignatures, }), ).toBe('audio/mpeg'); }); it('should detect MP3 from base64', () => { const mp3Base64 = '//s='; // Base64 string starting with MP3 signature expect( detectMediaType({ data: mp3Base64, signatures: audioMediaTypeSignatures, }), ).toBe('audio/mpeg'); }); it('should detect MP3 with ID3v2 tags from bytes', () => { const mp3WithID3Bytes = new Uint8Array([ 0x49, 0x44, 0x33, // 'ID3' 0x03, 0x00, // version 0x00, // flags 0x00, 0x00, 0x00, 0x0a, // size (10 bytes) // 10 bytes of ID3 data 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // MP3 frame header 0xff, 0xfb, 0x00, 0x00, ]); expect( detectMediaType({ data: mp3WithID3Bytes, signatures: audioMediaTypeSignatures, }), ).toBe('audio/mpeg'); }); it('should detect MP3 with ID3v2 tags from base64', () => { const mp3WithID3Bytes = new Uint8Array([ 0x49, 0x44, 0x33, // 'ID3' 0x03, 0x00, // version 0x00, // flags 0x00, 0x00, 0x00, 0x0a, // size (10 bytes) // 10 bytes of ID3 data 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // MP3 frame header 0xff, 0xfb, 0x00, 0x00, ]); const mp3WithID3Base64 = convertUint8ArrayToBase64(mp3WithID3Bytes); expect( detectMediaType({ data: mp3WithID3Base64, signatures: audioMediaTypeSignatures, }), ).toBe('audio/mpeg'); }); }); describe('WAV', () => { it('should detect WAV from bytes', () => { const wavBytes = new Uint8Array([0x52, 0x49, 0x46, 0x46]); expect( detectMediaType({ data: wavBytes, signatures: audioMediaTypeSignatures, }), ).toBe('audio/wav'); }); it('should detect WAV from base64', () => { const wavBase64 = 'UklGRiQ='; // Base64 string starting with WAV signature expect( detectMediaType({ data: wavBase64, signatures: audioMediaTypeSignatures, }), ).toBe('audio/wav'); }); }); describe('OGG', () => { it('should detect OGG from bytes', () => { const oggBytes = new Uint8Array([0x4f, 0x67, 0x67, 0x53]); expect( detectMediaType({ data: oggBytes, signatures: audioMediaTypeSignatures, }), ).toBe('audio/ogg'); }); it('should detect OGG from base64', () => { const oggBase64 = 'T2dnUw'; // Base64 string starting with OGG signature expect( detectMediaType({ data: oggBase64, signatures: audioMediaTypeSignatures, }), ).toBe('audio/ogg'); }); }); describe('FLAC', () => { it('should detect FLAC from bytes', () => { const flacBytes = new Uint8Array([0x66, 0x4c, 0x61, 0x43]); expect( detectMediaType({ data: flacBytes, signatures: audioMediaTypeSignatures, }), ).toBe('audio/flac'); }); it('should detect FLAC from base64', () => { const flacBase64 = 'ZkxhQw'; // Base64 string starting with FLAC signature expect( detectMediaType({ data: flacBase64, signatures: audioMediaTypeSignatures, }), ).toBe('audio/flac'); }); }); describe('AAC', () => { it('should detect AAC from bytes', () => { const aacBytes = new Uint8Array([0x40, 0x15, 0x00, 0x00]); expect( detectMediaType({ data: aacBytes, signatures: audioMediaTypeSignatures, }), ).toBe('audio/aac'); }); it('should detect AAC from base64', () => { const aacBase64 = 'QBUA'; // Base64 string starting with AAC signature expect( detectMediaType({ data: aacBase64, signatures: audioMediaTypeSignatures, }), ).toBe('audio/aac'); }); }); describe('MP4', () => { it('should detect MP4 from bytes', () => { const mp4Bytes = new Uint8Array([0x66, 0x74, 0x79, 0x70]); expect( detectMediaType({ data: mp4Bytes, signatures: audioMediaTypeSignatures, }), ).toBe('audio/mp4'); }); it('should detect MP4 from base64', () => { const mp4Base64 = 'ZnR5cA'; // Base64 string starting with MP4 signature expect( detectMediaType({ data: mp4Base64, signatures: audioMediaTypeSignatures, }), ).toBe('audio/mp4'); }); }); describe('WEBM', () => { it('should detect WEBM from bytes', () => { const webmBytes = new Uint8Array([0x1a, 0x45, 0xdf, 0xa3]); expect( detectMediaType({ data: webmBytes, signatures: audioMediaTypeSignatures, }), ).toBe('audio/webm'); }); it('should detect WEBM from base64', () => { const webmBase64 = 'GkXfow=='; // Base64 string starting with WEBM signature expect( detectMediaType({ data: webmBase64, signatures: audioMediaTypeSignatures, }), ).toBe('audio/webm'); }); }); describe('error cases', () => { it('should return undefined for unknown image formats', () => { const unknownBytes = new Uint8Array([0x00, 0x01, 0x02, 0x03]); expect( detectMediaType({ data: unknownBytes, signatures: imageMediaTypeSignatures, }), ).toBeUndefined(); }); it('should return undefined for unknown audio formats', () => { const unknownBytes = new Uint8Array([0x00, 0x01, 0x02, 0x03]); expect( detectMediaType({ data: unknownBytes, signatures: audioMediaTypeSignatures, }), ).toBeUndefined(); }); it('should return undefined for empty arrays for image', () => { const emptyBytes = new Uint8Array([]); expect( detectMediaType({ data: emptyBytes, signatures: imageMediaTypeSignatures, }), ).toBeUndefined(); }); it('should return undefined for empty arrays for audio', () => { const emptyBytes = new Uint8Array([]); expect( detectMediaType({ data: emptyBytes, signatures: audioMediaTypeSignatures, }), ).toBeUndefined(); }); it('should return undefined for arrays shorter than signature length for image', () => { const shortBytes = new Uint8Array([0x89, 0x50]); // Incomplete PNG signature expect( detectMediaType({ data: shortBytes, signatures: imageMediaTypeSignatures, }), ).toBeUndefined(); }); it('should return undefined for arrays shorter than signature length for audio', () => { const shortBytes = new Uint8Array([0x4f, 0x67]); // Incomplete OGG signature expect( detectMediaType({ data: shortBytes, signatures: audioMediaTypeSignatures, }), ).toBeUndefined(); }); it('should return undefined for invalid base64 strings for image', () => { const invalidBase64 = 'invalid123'; expect( detectMediaType({ data: invalidBase64, signatures: imageMediaTypeSignatures, }), ).toBeUndefined(); }); it('should return undefined for invalid base64 strings for audio', () => { const invalidBase64 = 'invalid123'; expect( detectMediaType({ data: invalidBase64, signatures: audioMediaTypeSignatures, }), ).toBeUndefined(); }); }); }); --- File: /ai/packages/ai/src/util/detect-media-type.ts --- import { convertBase64ToUint8Array } from '@ai-sdk/provider-utils'; export const imageMediaTypeSignatures = [ { mediaType: 'image/gif' as const, bytesPrefix: [0x47, 0x49, 0x46], base64Prefix: 'R0lG', }, { mediaType: 'image/png' as const, bytesPrefix: [0x89, 0x50, 0x4e, 0x47], base64Prefix: 'iVBORw', }, { mediaType: 'image/jpeg' as const, bytesPrefix: [0xff, 0xd8], base64Prefix: '/9j/', }, { mediaType: 'image/webp' as const, bytesPrefix: [0x52, 0x49, 0x46, 0x46], base64Prefix: 'UklGRg', }, { mediaType: 'image/bmp' as const, bytesPrefix: [0x42, 0x4d], base64Prefix: 'Qk', }, { mediaType: 'image/tiff' as const, bytesPrefix: [0x49, 0x49, 0x2a, 0x00], base64Prefix: 'SUkqAA', }, { mediaType: 'image/tiff' as const, bytesPrefix: [0x4d, 0x4d, 0x00, 0x2a], base64Prefix: 'TU0AKg', }, { mediaType: 'image/avif' as const, bytesPrefix: [ 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66, ], base64Prefix: 'AAAAIGZ0eXBhdmlm', }, { mediaType: 'image/heic' as const, bytesPrefix: [ 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63, ], base64Prefix: 'AAAAIGZ0eXBoZWlj', }, ] as const; export const audioMediaTypeSignatures = [ { mediaType: 'audio/mpeg' as const, bytesPrefix: [0xff, 0xfb], base64Prefix: '//s=', }, { mediaType: 'audio/wav' as const, bytesPrefix: [0x52, 0x49, 0x46, 0x46], base64Prefix: 'UklGR', }, { mediaType: 'audio/ogg' as const, bytesPrefix: [0x4f, 0x67, 0x67, 0x53], base64Prefix: 'T2dnUw', }, { mediaType: 'audio/flac' as const, bytesPrefix: [0x66, 0x4c, 0x61, 0x43], base64Prefix: 'ZkxhQw', }, { mediaType: 'audio/aac' as const, bytesPrefix: [0x40, 0x15, 0x00, 0x00], base64Prefix: 'QBUA', }, { mediaType: 'audio/mp4' as const, bytesPrefix: [0x66, 0x74, 0x79, 0x70], base64Prefix: 'ZnR5cA', }, { mediaType: 'audio/webm', bytesPrefix: [0x1a, 0x45, 0xdf, 0xa3], base64Prefix: 'GkXf', }, ] as const; const stripID3 = (data: Uint8Array | string) => { const bytes = typeof data === 'string' ? convertBase64ToUint8Array(data) : data; const id3Size = ((bytes[6] & 0x7f) << 21) | ((bytes[7] & 0x7f) << 14) | ((bytes[8] & 0x7f) << 7) | (bytes[9] & 0x7f); // The raw MP3 starts here return bytes.slice(id3Size + 10); }; function stripID3TagsIfPresent(data: Uint8Array | string): Uint8Array | string { const hasId3 = (typeof data === 'string' && data.startsWith('SUQz')) || (typeof data !== 'string' && data.length > 10 && data[0] === 0x49 && // 'I' data[1] === 0x44 && // 'D' data[2] === 0x33); // '3' return hasId3 ? stripID3(data) : data; } /** * Detect the media IANA media type of a file using a list of signatures. * * @param data - The file data. * @param signatures - The signatures to use for detection. * @returns The media type of the file. */ export function detectMediaType({ data, signatures, }: { data: Uint8Array | string; signatures: typeof audioMediaTypeSignatures | typeof imageMediaTypeSignatures; }): (typeof signatures)[number]['mediaType'] | undefined { const processedData = stripID3TagsIfPresent(data); for (const signature of signatures) { if ( typeof processedData === 'string' ? processedData.startsWith(signature.base64Prefix) : processedData.length >= signature.bytesPrefix.length && signature.bytesPrefix.every( (byte, index) => processedData[index] === byte, ) ) { return signature.mediaType; } } return undefined; } --- File: /ai/packages/ai/src/util/download-error.ts --- import { AISDKError } from '@ai-sdk/provider'; const name = 'AI_DownloadError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class DownloadError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly url: string; readonly statusCode?: number; readonly statusText?: string; constructor({ url, statusCode, statusText, cause, message = cause == null ? `Failed to download ${url}: ${statusCode} ${statusText}` : `Failed to download ${url}: ${cause}`, }: { url: string; statusCode?: number; statusText?: string; message?: string; cause?: unknown; }) { super({ name, message, cause }); this.url = url; this.statusCode = statusCode; this.statusText = statusText; } static isInstance(error: unknown): error is DownloadError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/ai/src/util/download.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { download } from './download'; import { DownloadError } from './download-error'; const server = createTestServer({ 'http://example.com/file': {}, }); describe('download', () => { it('should download data successfully and match expected bytes', async () => { const expectedBytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); server.urls['http://example.com/file'].response = { type: 'binary', headers: { 'content-type': 'application/octet-stream', }, body: Buffer.from(expectedBytes), }; const result = await download({ url: new URL('http://example.com/file'), }); expect(result.data).toEqual(expectedBytes); expect(result.mediaType).toBe('application/octet-stream'); }); it('should throw DownloadError when response is not ok', async () => { server.urls['http://example.com/file'].response = { type: 'error', status: 404, body: 'Not Found', }; try { await download({ url: new URL('http://example.com/file'), }); expect.fail('Expected download to throw'); } catch (error: unknown) { expect(error).toBeInstanceOf(DownloadError); expect((error as DownloadError).statusCode).toBe(404); expect((error as DownloadError).statusText).toBe('Not Found'); } }); it('should throw DownloadError when fetch throws an error', async () => { server.urls['http://example.com/file'].response = { type: 'error', status: 500, body: 'Network error', }; try { await download({ url: new URL('http://example.com/file'), }); expect.fail('Expected download to throw'); } catch (error: unknown) { expect(error).toBeInstanceOf(DownloadError); } }); }); --- File: /ai/packages/ai/src/util/download.ts --- import { DownloadError } from './download-error'; export async function download({ url }: { url: URL }): Promise<{ data: Uint8Array; mediaType: string | undefined; }> { const urlText = url.toString(); try { const response = await fetch(urlText); if (!response.ok) { throw new DownloadError({ url: urlText, statusCode: response.status, statusText: response.statusText, }); } return { data: new Uint8Array(await response.arrayBuffer()), mediaType: response.headers.get('content-type') ?? undefined, }; } catch (error) { if (DownloadError.isInstance(error)) { throw error; } throw new DownloadError({ url: urlText, cause: error }); } } --- File: /ai/packages/ai/src/util/error-handler.ts --- export type ErrorHandler = (error: unknown) => void; --- File: /ai/packages/ai/src/util/filter-stream-errors.ts --- export function filterStreamErrors<T>( readable: ReadableStream<T>, onError: ({ error, controller, }: { error: unknown; controller: ReadableStreamDefaultController<T>; }) => Promise<void> | void, ): ReadableStream<T> { return new ReadableStream<T>({ async start(controller) { const reader = readable.getReader(); try { while (true) { const { done, value } = await reader.read(); if (done) { controller.close(); break; } controller.enqueue(value); } } catch (error) { await onError({ error, controller }); } }, cancel(reason) { return readable.cancel(reason); }, }); } --- File: /ai/packages/ai/src/util/fix-json.test.ts --- import assert from 'node:assert'; import { describe, test } from 'vitest'; import { fixJson } from './fix-json'; test('should handle empty input', () => { assert.strictEqual(fixJson(''), ''); }); describe('literals', () => { test('should handle incomplete null', () => { assert.strictEqual(fixJson('nul'), 'null'); }); test('should handle incomplete true', () => { assert.strictEqual(fixJson('t'), 'true'); }); test('should handle incomplete false', () => { assert.strictEqual(fixJson('fals'), 'false'); }); }); describe('number', () => { test('should handle incomplete numbers', () => { assert.strictEqual(fixJson('12.'), '12'); }); test('should handle numbers with dot', () => { assert.strictEqual(fixJson('12.2'), '12.2'); }); test('should handle negative numbers', () => { assert.strictEqual(fixJson('-12'), '-12'); }); test('should handle incomplete negative numbers', () => { assert.strictEqual(fixJson('-'), ''); }); test('should handle e-notation numbers', () => { assert.strictEqual(fixJson('2.5e'), '2.5'); assert.strictEqual(fixJson('2.5e-'), '2.5'); assert.strictEqual(fixJson('2.5e3'), '2.5e3'); assert.strictEqual(fixJson('-2.5e3'), '-2.5e3'); }); test('should handle uppercase e-notation numbers', () => { assert.strictEqual(fixJson('2.5E'), '2.5'); assert.strictEqual(fixJson('2.5E-'), '2.5'); assert.strictEqual(fixJson('2.5E3'), '2.5E3'); assert.strictEqual(fixJson('-2.5E3'), '-2.5E3'); }); test('should handle incomplete numbers', () => { assert.strictEqual(fixJson('12.e'), '12'); assert.strictEqual(fixJson('12.34e'), '12.34'); assert.strictEqual(fixJson('5e'), '5'); }); }); describe('string', () => { test('should handle incomplete strings', () => { assert.strictEqual(fixJson('"abc'), '"abc"'); }); test('should handle escape sequences', () => { assert.strictEqual( fixJson('"value with \\"quoted\\" text and \\\\ escape'), '"value with \\"quoted\\" text and \\\\ escape"', ); }); test('should handle incomplete escape sequences', () => { assert.strictEqual(fixJson('"value with \\'), '"value with "'); }); test('should handle unicode characters', () => { assert.strictEqual( fixJson('"value with unicode \u003C"'), '"value with unicode \u003C"', ); }); }); describe('array', () => { test('should handle incomplete array', () => { assert.strictEqual(fixJson('['), '[]'); }); test('should handle closing bracket after number in array', () => { assert.strictEqual(fixJson('[[1], [2'), '[[1], [2]]'); }); test('should handle closing bracket after string in array', () => { assert.strictEqual(fixJson(`[["1"], ["2`), `[["1"], ["2"]]`); }); test('should handle closing bracket after literal in array', () => { assert.strictEqual(fixJson('[[false], [nu'), '[[false], [null]]'); }); test('should handle closing bracket after array in array', () => { assert.strictEqual(fixJson('[[[]], [[]'), '[[[]], [[]]]'); }); test('should handle closing bracket after object in array', () => { assert.strictEqual(fixJson('[[{}], [{'), '[[{}], [{}]]'); }); test('should handle trailing comma', () => { assert.strictEqual(fixJson('[1, '), '[1]'); }); test('should handle closing array', () => { assert.strictEqual(fixJson('[[], 123'), '[[], 123]'); }); }); describe('object', () => { test('should handle keys without values', () => { assert.strictEqual(fixJson('{"key":'), '{}'); }); test('should handle closing brace after number in object', () => { assert.strictEqual( fixJson('{"a": {"b": 1}, "c": {"d": 2'), '{"a": {"b": 1}, "c": {"d": 2}}', ); }); test('should handle closing brace after string in object', () => { assert.strictEqual( fixJson('{"a": {"b": "1"}, "c": {"d": 2'), '{"a": {"b": "1"}, "c": {"d": 2}}', ); }); test('should handle closing brace after literal in object', () => { assert.strictEqual( fixJson('{"a": {"b": false}, "c": {"d": 2'), '{"a": {"b": false}, "c": {"d": 2}}', ); }); test('should handle closing brace after array in object', () => { assert.strictEqual( fixJson('{"a": {"b": []}, "c": {"d": 2'), '{"a": {"b": []}, "c": {"d": 2}}', ); }); test('should handle closing brace after object in object', () => { assert.strictEqual( fixJson('{"a": {"b": {}}, "c": {"d": 2'), '{"a": {"b": {}}, "c": {"d": 2}}', ); }); test('should handle partial keys (first key)', () => { assert.strictEqual(fixJson('{"ke'), '{}'); }); test('should handle partial keys (second key)', () => { assert.strictEqual(fixJson('{"k1": 1, "k2'), '{"k1": 1}'); }); test('should handle partial keys with colon (second key)', () => { assert.strictEqual(fixJson('{"k1": 1, "k2":'), '{"k1": 1}'); }); test('should handle trailing whitespace', () => { assert.strictEqual(fixJson('{"key": "value" '), '{"key": "value"}'); }); test('should handle closing after empty object', () => { assert.strictEqual(fixJson('{"a": {"b": {}'), '{"a": {"b": {}}}'); }); }); describe('nesting', () => { test('should handle nested arrays with numbers', () => { assert.strictEqual(fixJson('[1, [2, 3, ['), '[1, [2, 3, []]]'); }); test('should handle nested arrays with literals', () => { assert.strictEqual(fixJson('[false, [true, ['), '[false, [true, []]]'); }); test('should handle nested objects', () => { assert.strictEqual(fixJson('{"key": {"subKey":'), '{"key": {}}'); }); test('should handle nested objects with numbers', () => { assert.strictEqual( fixJson('{"key": 123, "key2": {"subKey":'), '{"key": 123, "key2": {}}', ); }); test('should handle nested objects with literals', () => { assert.strictEqual( fixJson('{"key": null, "key2": {"subKey":'), '{"key": null, "key2": {}}', ); }); test('should handle arrays within objects', () => { assert.strictEqual(fixJson('{"key": [1, 2, {'), '{"key": [1, 2, {}]}'); }); test('should handle objects within arrays', () => { assert.strictEqual( fixJson('[1, 2, {"key": "value",'), '[1, 2, {"key": "value"}]', ); }); test('should handle nested arrays and objects', () => { assert.strictEqual( fixJson('{"a": {"b": ["c", {"d": "e",'), '{"a": {"b": ["c", {"d": "e"}]}}', ); }); test('should handle deeply nested objects', () => { assert.strictEqual( fixJson('{"a": {"b": {"c": {"d":'), '{"a": {"b": {"c": {}}}}', ); }); test('should handle potential nested arrays or objects', () => { assert.strictEqual(fixJson('{"a": 1, "b": ['), '{"a": 1, "b": []}'); assert.strictEqual(fixJson('{"a": 1, "b": {'), '{"a": 1, "b": {}}'); assert.strictEqual(fixJson('{"a": 1, "b": "'), '{"a": 1, "b": ""}'); }); }); describe('regression', () => { test('should handle complex nesting 1', () => { assert.strictEqual( fixJson( [ '{', ' "a": [', ' {', ' "a1": "v1",', ' "a2": "v2",', ` "a3": "v3"`, ' }', ' ],', ' "b": [', ' {', ' "b1": "n', ].join('\n'), ), [ '{', ' "a": [', ' {', ' "a1": "v1",', ' "a2": "v2",', ` "a3": "v3"`, ' }', ' ],', ' "b": [', ' {', ' "b1": "n"}]}', ].join('\n'), ); }); test('should handle empty objects inside nested objects and arrays', () => { assert.strictEqual( fixJson(`{"type":"div","children":[{"type":"Card","props":{}`), `{"type":"div","children":[{"type":"Card","props":{}}]}`, ); }); }); --- File: /ai/packages/ai/src/util/fix-json.ts --- type State = | 'ROOT' | 'FINISH' | 'INSIDE_STRING' | 'INSIDE_STRING_ESCAPE' | 'INSIDE_LITERAL' | 'INSIDE_NUMBER' | 'INSIDE_OBJECT_START' | 'INSIDE_OBJECT_KEY' | 'INSIDE_OBJECT_AFTER_KEY' | 'INSIDE_OBJECT_BEFORE_VALUE' | 'INSIDE_OBJECT_AFTER_VALUE' | 'INSIDE_OBJECT_AFTER_COMMA' | 'INSIDE_ARRAY_START' | 'INSIDE_ARRAY_AFTER_VALUE' | 'INSIDE_ARRAY_AFTER_COMMA'; // Implemented as a scanner with additional fixing // that performs a single linear time scan pass over the partial JSON. // // The states should ideally match relevant states from the JSON spec: // https://www.json.org/json-en.html // // Please note that invalid JSON is not considered/covered, because it // is assumed that the resulting JSON will be processed by a standard // JSON parser that will detect any invalid JSON. export function fixJson(input: string): string { const stack: State[] = ['ROOT']; let lastValidIndex = -1; let literalStart: number | null = null; function processValueStart(char: string, i: number, swapState: State) { { switch (char) { case '"': { lastValidIndex = i; stack.pop(); stack.push(swapState); stack.push('INSIDE_STRING'); break; } case 'f': case 't': case 'n': { lastValidIndex = i; literalStart = i; stack.pop(); stack.push(swapState); stack.push('INSIDE_LITERAL'); break; } case '-': { stack.pop(); stack.push(swapState); stack.push('INSIDE_NUMBER'); break; } case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': { lastValidIndex = i; stack.pop(); stack.push(swapState); stack.push('INSIDE_NUMBER'); break; } case '{': { lastValidIndex = i; stack.pop(); stack.push(swapState); stack.push('INSIDE_OBJECT_START'); break; } case '[': { lastValidIndex = i; stack.pop(); stack.push(swapState); stack.push('INSIDE_ARRAY_START'); break; } } } } function processAfterObjectValue(char: string, i: number) { switch (char) { case ',': { stack.pop(); stack.push('INSIDE_OBJECT_AFTER_COMMA'); break; } case '}': { lastValidIndex = i; stack.pop(); break; } } } function processAfterArrayValue(char: string, i: number) { switch (char) { case ',': { stack.pop(); stack.push('INSIDE_ARRAY_AFTER_COMMA'); break; } case ']': { lastValidIndex = i; stack.pop(); break; } } } for (let i = 0; i < input.length; i++) { const char = input[i]; const currentState = stack[stack.length - 1]; switch (currentState) { case 'ROOT': processValueStart(char, i, 'FINISH'); break; case 'INSIDE_OBJECT_START': { switch (char) { case '"': { stack.pop(); stack.push('INSIDE_OBJECT_KEY'); break; } case '}': { lastValidIndex = i; stack.pop(); break; } } break; } case 'INSIDE_OBJECT_AFTER_COMMA': { switch (char) { case '"': { stack.pop(); stack.push('INSIDE_OBJECT_KEY'); break; } } break; } case 'INSIDE_OBJECT_KEY': { switch (char) { case '"': { stack.pop(); stack.push('INSIDE_OBJECT_AFTER_KEY'); break; } } break; } case 'INSIDE_OBJECT_AFTER_KEY': { switch (char) { case ':': { stack.pop(); stack.push('INSIDE_OBJECT_BEFORE_VALUE'); break; } } break; } case 'INSIDE_OBJECT_BEFORE_VALUE': { processValueStart(char, i, 'INSIDE_OBJECT_AFTER_VALUE'); break; } case 'INSIDE_OBJECT_AFTER_VALUE': { processAfterObjectValue(char, i); break; } case 'INSIDE_STRING': { switch (char) { case '"': { stack.pop(); lastValidIndex = i; break; } case '\\': { stack.push('INSIDE_STRING_ESCAPE'); break; } default: { lastValidIndex = i; } } break; } case 'INSIDE_ARRAY_START': { switch (char) { case ']': { lastValidIndex = i; stack.pop(); break; } default: { lastValidIndex = i; processValueStart(char, i, 'INSIDE_ARRAY_AFTER_VALUE'); break; } } break; } case 'INSIDE_ARRAY_AFTER_VALUE': { switch (char) { case ',': { stack.pop(); stack.push('INSIDE_ARRAY_AFTER_COMMA'); break; } case ']': { lastValidIndex = i; stack.pop(); break; } default: { lastValidIndex = i; break; } } break; } case 'INSIDE_ARRAY_AFTER_COMMA': { processValueStart(char, i, 'INSIDE_ARRAY_AFTER_VALUE'); break; } case 'INSIDE_STRING_ESCAPE': { stack.pop(); lastValidIndex = i; break; } case 'INSIDE_NUMBER': { switch (char) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': { lastValidIndex = i; break; } case 'e': case 'E': case '-': case '.': { break; } case ',': { stack.pop(); if (stack[stack.length - 1] === 'INSIDE_ARRAY_AFTER_VALUE') { processAfterArrayValue(char, i); } if (stack[stack.length - 1] === 'INSIDE_OBJECT_AFTER_VALUE') { processAfterObjectValue(char, i); } break; } case '}': { stack.pop(); if (stack[stack.length - 1] === 'INSIDE_OBJECT_AFTER_VALUE') { processAfterObjectValue(char, i); } break; } case ']': { stack.pop(); if (stack[stack.length - 1] === 'INSIDE_ARRAY_AFTER_VALUE') { processAfterArrayValue(char, i); } break; } default: { stack.pop(); break; } } break; } case 'INSIDE_LITERAL': { const partialLiteral = input.substring(literalStart!, i + 1); if ( !'false'.startsWith(partialLiteral) && !'true'.startsWith(partialLiteral) && !'null'.startsWith(partialLiteral) ) { stack.pop(); if (stack[stack.length - 1] === 'INSIDE_OBJECT_AFTER_VALUE') { processAfterObjectValue(char, i); } else if (stack[stack.length - 1] === 'INSIDE_ARRAY_AFTER_VALUE') { processAfterArrayValue(char, i); } } else { lastValidIndex = i; } break; } } } let result = input.slice(0, lastValidIndex + 1); for (let i = stack.length - 1; i >= 0; i--) { const state = stack[i]; switch (state) { case 'INSIDE_STRING': { result += '"'; break; } case 'INSIDE_OBJECT_KEY': case 'INSIDE_OBJECT_AFTER_KEY': case 'INSIDE_OBJECT_AFTER_COMMA': case 'INSIDE_OBJECT_START': case 'INSIDE_OBJECT_BEFORE_VALUE': case 'INSIDE_OBJECT_AFTER_VALUE': { result += '}'; break; } case 'INSIDE_ARRAY_START': case 'INSIDE_ARRAY_AFTER_COMMA': case 'INSIDE_ARRAY_AFTER_VALUE': { result += ']'; break; } case 'INSIDE_LITERAL': { const partialLiteral = input.substring(literalStart!, input.length); if ('true'.startsWith(partialLiteral)) { result += 'true'.slice(partialLiteral.length); } else if ('false'.startsWith(partialLiteral)) { result += 'false'.slice(partialLiteral.length); } else if ('null'.startsWith(partialLiteral)) { result += 'null'.slice(partialLiteral.length); } } } } return result; } --- File: /ai/packages/ai/src/util/get-potential-start-index.test.ts --- import { getPotentialStartIndex } from './get-potential-start-index'; describe('getPotentialStartIndex', () => { it('should return null when searchedText is empty', () => { const result = getPotentialStartIndex('1234567890', ''); expect(result).toBeNull(); }); it('should return null when searchedText is not in text', () => { const result = getPotentialStartIndex('1234567890', 'a'); expect(result).toBeNull(); }); it('should return index when searchedText is in text', () => { const result = getPotentialStartIndex('1234567890', '1234567890'); expect(result).toBe(0); }); it('should return index when searchedText might start in text', () => { const result = getPotentialStartIndex('1234567890', '0123'); expect(result).toBe(9); }); it('should return index when searchedText might start in text', () => { const result = getPotentialStartIndex('1234567890', '90123'); expect(result).toBe(8); }); it('should return index when searchedText might start in text', () => { const result = getPotentialStartIndex('1234567890', '890123'); expect(result).toBe(7); }); }); --- File: /ai/packages/ai/src/util/get-potential-start-index.ts --- /** * Returns the index of the start of the searchedText in the text, or null if it * is not found. */ export function getPotentialStartIndex( text: string, searchedText: string, ): number | null { // Return null immediately if searchedText is empty. if (searchedText.length === 0) { return null; } // Check if the searchedText exists as a direct substring of text. const directIndex = text.indexOf(searchedText); if (directIndex !== -1) { return directIndex; } // Otherwise, look for the largest suffix of "text" that matches // a prefix of "searchedText". We go from the end of text inward. for (let i = text.length - 1; i >= 0; i--) { const suffix = text.substring(i); if (searchedText.startsWith(suffix)) { return i; } } return null; } --- File: /ai/packages/ai/src/util/index.ts --- export type { AsyncIterableStream } from './async-iterable-stream'; export { consumeStream } from './consume-stream'; export { cosineSimilarity } from './cosine-similarity'; export { getTextFromDataUrl } from './data-url'; export type { DeepPartial } from './deep-partial'; export { type ErrorHandler } from './error-handler'; export { isDeepEqualData } from './is-deep-equal-data'; export { parsePartialJson } from './parse-partial-json'; export { SerialJobExecutor } from './serial-job-executor'; export { simulateReadableStream } from './simulate-readable-stream'; --- File: /ai/packages/ai/src/util/is-deep-equal-data.test.ts --- import assert from 'node:assert'; import { isDeepEqualData } from './is-deep-equal-data'; it('should check if two primitives are equal', async () => { let x = 1; let y = 1; let result = isDeepEqualData(x, y); assert.equal(result, true); x = 1; y = 2; result = isDeepEqualData(x, y); assert.equal(result, false); }); it('should return false for different types', async () => { const obj = { a: 1 }; const num = 1; const result = isDeepEqualData(obj, num); assert.equal(result, false); }); it('should return false for null values compared with objects', async () => { const obj = { a: 1 }; const result = isDeepEqualData(obj, null); assert.equal(result, false); }); it('should identify two equal objects', async () => { const obj1 = { a: 1, b: 2 }; const obj2 = { a: 1, b: 2 }; const result = isDeepEqualData(obj1, obj2); assert.equal(result, true); }); it('should identify two objects with different values', async () => { const obj1 = { a: 1, b: 2 }; const obj2 = { a: 1, b: 3 }; const result = isDeepEqualData(obj1, obj2); assert.equal(result, false); }); it('should identify two objects with different number of keys', async () => { const obj1 = { a: 1, b: 2 }; const obj2 = { a: 1, b: 2, c: 3 }; const result = isDeepEqualData(obj1, obj2); assert.equal(result, false); }); it('should handle nested objects', async () => { const obj1 = { a: { c: 1 }, b: 2 }; const obj2 = { a: { c: 1 }, b: 2 }; const result = isDeepEqualData(obj1, obj2); assert.equal(result, true); }); it('should detect inequality in nested objects', async () => { const obj1 = { a: { c: 1 }, b: 2 }; const obj2 = { a: { c: 2 }, b: 2 }; const result = isDeepEqualData(obj1, obj2); assert.equal(result, false); }); it('should compare arrays correctly', async () => { const arr1 = [1, 2, 3]; const arr2 = [1, 2, 3]; const result = isDeepEqualData(arr1, arr2); assert.equal(result, true); const arr3 = [1, 2, 3]; const arr4 = [1, 2, 4]; const result2 = isDeepEqualData(arr3, arr4); assert.equal(result2, false); }); it('should return false for null comparison with object', () => { const obj = { a: 1 }; const result = isDeepEqualData(obj, null); assert.equal(result, false); }); it('should distinguish between array and object with same enumerable properties', () => { const obj = { 0: 'one', 1: 'two', length: 2 }; const arr = ['one', 'two']; const result = isDeepEqualData(obj, arr); assert.equal(result, false); }); it('should return false when comparing objects with different prototypes', () => { const obj1 = Object.create({ a: 1 }); const obj2 = Object.create(null); obj1.b = 2; obj2.b = 2; const result = isDeepEqualData(obj1, obj2); assert.equal(result, false); }); it('should handle date object comparisons correctly', () => { const date1 = new Date(2000, 0, 1); const date2 = new Date(2000, 0, 1); const date3 = new Date(2000, 0, 2); assert.equal(isDeepEqualData(date1, date2), true); assert.equal(isDeepEqualData(date1, date3), false); }); it('should handle function comparisons', () => { const func1 = () => { console.log('hello'); }; const func2 = () => { console.log('hello'); }; const func3 = () => { console.log('world'); }; assert.equal(isDeepEqualData(func1, func2), false); assert.equal(isDeepEqualData(func1, func3), false); }); --- File: /ai/packages/ai/src/util/is-deep-equal-data.ts --- /** * Performs a deep-equal comparison of two parsed JSON objects. * * @param {any} obj1 - The first object to compare. * @param {any} obj2 - The second object to compare. * @returns {boolean} - Returns true if the two objects are deeply equal, false otherwise. */ export function isDeepEqualData(obj1: any, obj2: any): boolean { // Check for strict equality first if (obj1 === obj2) return true; // Check if either is null or undefined if (obj1 == null || obj2 == null) return false; // Check if both are objects if (typeof obj1 !== 'object' && typeof obj2 !== 'object') return obj1 === obj2; // If they are not strictly equal, they both need to be Objects if (obj1.constructor !== obj2.constructor) return false; // Special handling for Date objects if (obj1 instanceof Date && obj2 instanceof Date) { return obj1.getTime() === obj2.getTime(); } // Handle arrays: compare length and then perform a recursive deep comparison on each item if (Array.isArray(obj1)) { if (obj1.length !== obj2.length) return false; for (let i = 0; i < obj1.length; i++) { if (!isDeepEqualData(obj1[i], obj2[i])) return false; } return true; // All array elements matched } // Compare the set of keys in each object const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2); if (keys1.length !== keys2.length) return false; // Check each key-value pair recursively for (const key of keys1) { if (!keys2.includes(key)) return false; if (!isDeepEqualData(obj1[key], obj2[key])) return false; } return true; // All keys and values matched } --- File: /ai/packages/ai/src/util/is-non-empty-object.ts --- export function isNonEmptyObject( object: Record<string, unknown> | undefined | null, ): object is Record<string, unknown> { return object != null && Object.keys(object).length > 0; } --- File: /ai/packages/ai/src/util/job.ts --- export type Job = () => Promise<void>; --- File: /ai/packages/ai/src/util/merge-objects.test.ts --- import { describe, it, expect } from 'vitest'; import { mergeObjects } from './merge-objects'; describe('mergeObjects', () => { it('should merge two flat objects', () => { const target = { a: 1, b: 2 }; const source = { b: 3, c: 4 }; const result = mergeObjects(target, source); expect(result).toEqual({ a: 1, b: 3, c: 4 }); // Original objects should not be modified expect(target).toEqual({ a: 1, b: 2 }); expect(source).toEqual({ b: 3, c: 4 }); }); it('should deeply merge nested objects', () => { const target = { a: 1, b: { c: 2, d: 3 } }; const source = { b: { c: 4, e: 5 } }; const result = mergeObjects(target, source); expect(result).toEqual({ a: 1, b: { c: 4, d: 3, e: 5 } }); }); it('should replace arrays instead of merging them', () => { const target = { a: [1, 2, 3], b: 2 }; const source = { a: [4, 5] }; const result = mergeObjects(target, source); expect(result).toEqual({ a: [4, 5], b: 2 }); }); it('should handle null and undefined values', () => { const target = { a: 1, b: null, c: undefined }; const source = { a: null, b: 2, d: undefined }; const result = mergeObjects(target, source); expect(result).toEqual({ a: null, b: 2, c: undefined, d: undefined }); }); it('should handle complex nested structures', () => { const target = { a: 1, b: { c: [1, 2, 3], d: { e: 4, f: 5, }, }, }; const source = { b: { c: [4, 5], d: { f: 6, g: 7, }, }, h: 8, }; const result = mergeObjects(target, source); expect(result).toEqual({ a: 1, b: { c: [4, 5], d: { e: 4, f: 6, g: 7, }, }, h: 8, }); }); it('should handle Date objects', () => { const date1 = new Date('2023-01-01'); const date2 = new Date('2023-02-01'); const target = { a: date1 }; const source = { a: date2 }; const result = mergeObjects(target, source); expect(result?.a).toBe(date2); }); it('should handle RegExp objects', () => { const regex1 = /abc/; const regex2 = /def/; const target = { a: regex1 }; const source = { a: regex2 }; const result = mergeObjects(target, source); expect(result?.a).toBe(regex2); }); it('should handle empty objects', () => { const target = {}; const source = { a: 1 }; expect(mergeObjects(target, source)).toEqual({ a: 1 }); const target2 = { a: 1 }; const source2 = {}; expect(mergeObjects(target2, source2)).toEqual({ a: 1 }); }); it('should handle undefined inputs', () => { // Both inputs undefined expect(mergeObjects(undefined, undefined)).toBeUndefined(); // One input undefined expect(mergeObjects({ a: 1 }, undefined)).toEqual({ a: 1 }); expect(mergeObjects(undefined, { b: 2 })).toEqual({ b: 2 }); }); }); --- File: /ai/packages/ai/src/util/merge-objects.ts --- /** * Deeply merges two objects together. * - Properties from the `overrides` object override those in the `base` object with the same key. * - For nested objects, the merge is performed recursively (deep merge). * - Arrays are replaced, not merged. * - Primitive values are replaced. * - If both `base` and `overrides` are undefined, returns undefined. * - If one of `base` or `overrides` is undefined, returns the other. * * @param base The target object to merge into * @param overrides The source object to merge from * @returns A new object with the merged properties, or undefined if both inputs are undefined */ export function mergeObjects<T extends object, U extends object>( base: T | undefined, overrides: U | undefined, ): (T & U) | T | U | undefined { // If both inputs are undefined, return undefined if (base === undefined && overrides === undefined) { return undefined; } // If target is undefined, return source if (base === undefined) { return overrides; } // If source is undefined, return target if (overrides === undefined) { return base; } // Create a new object to avoid mutating the inputs const result = { ...base } as T & U; // Iterate through all keys in the source object for (const key in overrides) { if (Object.prototype.hasOwnProperty.call(overrides, key)) { const overridesValue = overrides[key]; // Skip if the overrides value is undefined if (overridesValue === undefined) continue; // Get the base value if it exists const baseValue = key in base ? base[key as unknown as keyof T] : undefined; // Check if both values are objects that can be deeply merged const isSourceObject = overridesValue !== null && typeof overridesValue === 'object' && !Array.isArray(overridesValue) && !(overridesValue instanceof Date) && !(overridesValue instanceof RegExp); const isTargetObject = baseValue !== null && baseValue !== undefined && typeof baseValue === 'object' && !Array.isArray(baseValue) && !(baseValue instanceof Date) && !(baseValue instanceof RegExp); // If both values are mergeable objects, merge them recursively if (isSourceObject && isTargetObject) { result[key as keyof (T & U)] = mergeObjects( baseValue as object, overridesValue as object, ) as any; } else { // For primitives, arrays, or when one value is not a mergeable object, // simply override with the source value result[key as keyof (T & U)] = overridesValue as any; } } } return result; } --- File: /ai/packages/ai/src/util/now.ts --- // Shim for performance.now() to support environments that don't have it: export function now(): number { return globalThis?.performance?.now() ?? Date.now(); } --- File: /ai/packages/ai/src/util/parse-partial-json.test.ts --- import { safeParseJSON } from '@ai-sdk/provider-utils'; import { fixJson } from './fix-json'; import { parsePartialJson } from './parse-partial-json'; import { JSONParseError } from '@ai-sdk/provider'; vi.mock('@ai-sdk/provider-utils'); vi.mock('./fix-json'); describe('parsePartialJson', () => { it('should handle nullish input', async () => { expect(await parsePartialJson(undefined)).toEqual({ value: undefined, state: 'undefined-input', }); }); it('should parse valid JSON', async () => { const validJson = '{"key": "value"}'; const parsedValue = { key: 'value' }; vi.mocked(safeParseJSON).mockResolvedValueOnce({ success: true, value: parsedValue, rawValue: parsedValue, }); expect(await parsePartialJson(validJson)).toEqual({ value: parsedValue, state: 'successful-parse', }); expect(safeParseJSON).toHaveBeenCalledWith({ text: validJson }); }); it('should repair and parse partial JSON', async () => { const partialJson = '{"key": "value"'; const fixedJson = '{"key": "value"}'; const parsedValue = { key: 'value' }; vi.mocked(safeParseJSON) .mockResolvedValueOnce({ success: false, error: new JSONParseError({ text: partialJson, cause: undefined }), rawValue: partialJson, }) .mockResolvedValueOnce({ success: true, value: parsedValue, rawValue: parsedValue, }); vi.mocked(fixJson).mockReturnValueOnce(fixedJson); expect(await parsePartialJson(partialJson)).toEqual({ value: parsedValue, state: 'repaired-parse', }); expect(safeParseJSON).toHaveBeenCalledWith({ text: partialJson }); expect(fixJson).toHaveBeenCalledWith(partialJson); expect(safeParseJSON).toHaveBeenCalledWith({ text: fixedJson }); }); it('should handle invalid JSON that cannot be repaired', async () => { const invalidJson = 'not json at all'; vi.mocked(safeParseJSON).mockResolvedValue({ success: false, error: new JSONParseError({ text: invalidJson, cause: undefined }), rawValue: invalidJson, }); vi.mocked(fixJson).mockReturnValueOnce(invalidJson); expect(await parsePartialJson(invalidJson)).toEqual({ value: undefined, state: 'failed-parse', }); expect(safeParseJSON).toHaveBeenCalledWith({ text: invalidJson }); expect(fixJson).toHaveBeenCalledWith(invalidJson); expect(safeParseJSON).toHaveBeenCalledWith({ text: invalidJson }); }); }); --- File: /ai/packages/ai/src/util/parse-partial-json.ts --- import { JSONValue } from '@ai-sdk/provider'; import { safeParseJSON } from '@ai-sdk/provider-utils'; import { fixJson } from './fix-json'; export async function parsePartialJson(jsonText: string | undefined): Promise<{ value: JSONValue | undefined; state: | 'undefined-input' | 'successful-parse' | 'repaired-parse' | 'failed-parse'; }> { if (jsonText === undefined) { return { value: undefined, state: 'undefined-input' }; } let result = await safeParseJSON({ text: jsonText }); if (result.success) { return { value: result.value, state: 'successful-parse' }; } result = await safeParseJSON({ text: fixJson(jsonText) }); if (result.success) { return { value: result.value, state: 'repaired-parse' }; } return { value: undefined, state: 'failed-parse' }; } --- File: /ai/packages/ai/src/util/prepare-headers.test.ts --- import { prepareHeaders } from './prepare-headers'; describe('prepareHeaders', () => { it('should set Content-Type header if not present', () => { const headers = prepareHeaders({}, { 'content-type': 'application/json' }); expect(headers.get('Content-Type')).toBe('application/json'); }); it('should not overwrite existing Content-Type header', () => { const headers = prepareHeaders( { 'Content-Type': 'text/html' }, { 'content-type': 'application/json' }, ); expect(headers.get('Content-Type')).toBe('text/html'); }); it('should handle undefined init', () => { const headers = prepareHeaders(undefined, { 'content-type': 'application/json', }); expect(headers.get('Content-Type')).toBe('application/json'); }); it('should handle init headers as Headers object', () => { const headers = prepareHeaders(new Headers({ init: 'foo' }), { 'content-type': 'application/json', }); expect(headers.get('init')).toBe('foo'); expect(headers.get('Content-Type')).toBe('application/json'); }); it('should handle Response object headers', () => { const initHeaders = { init: 'foo' }; const response = new Response(null, { headers: { ...initHeaders, extra: 'bar' }, }); const headers = prepareHeaders(response.headers, { 'content-type': 'application/json', }); expect(headers.get('init')).toBe('foo'); expect(headers.get('extra')).toBe('bar'); expect(headers.get('Content-Type')).toBe('application/json'); }); }); --- File: /ai/packages/ai/src/util/prepare-headers.ts --- export function prepareHeaders( headers: HeadersInit | undefined, defaultHeaders: Record<string, string>, ): Headers { const responseHeaders = new Headers(headers ?? {}); for (const [key, value] of Object.entries(defaultHeaders)) { if (!responseHeaders.has(key)) { responseHeaders.set(key, value); } } return responseHeaders; } --- File: /ai/packages/ai/src/util/prepare-retries.test.ts --- import { expect, it } from 'vitest'; import { prepareRetries } from './prepare-retries'; it('should set default values correctly when no input is provided', () => { const defaultResult = prepareRetries({ maxRetries: undefined, abortSignal: undefined, }); expect(defaultResult.maxRetries).toBe(2); }); --- File: /ai/packages/ai/src/util/prepare-retries.ts --- import { InvalidArgumentError } from '../error/invalid-argument-error'; import { RetryFunction, retryWithExponentialBackoffRespectingRetryHeaders, } from '../util/retry-with-exponential-backoff'; /** * Validate and prepare retries. */ export function prepareRetries({ maxRetries, abortSignal, }: { maxRetries: number | undefined; abortSignal: AbortSignal | undefined; }): { maxRetries: number; retry: RetryFunction; } { if (maxRetries != null) { if (!Number.isInteger(maxRetries)) { throw new InvalidArgumentError({ parameter: 'maxRetries', value: maxRetries, message: 'maxRetries must be an integer', }); } if (maxRetries < 0) { throw new InvalidArgumentError({ parameter: 'maxRetries', value: maxRetries, message: 'maxRetries must be >= 0', }); } } const maxRetriesResult = maxRetries ?? 2; return { maxRetries: maxRetriesResult, retry: retryWithExponentialBackoffRespectingRetryHeaders({ maxRetries: maxRetriesResult, abortSignal, }), }; } --- File: /ai/packages/ai/src/util/retry-error.ts --- import { AISDKError } from '@ai-sdk/provider'; const name = 'AI_RetryError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export type RetryErrorReason = | 'maxRetriesExceeded' | 'errorNotRetryable' | 'abort'; export class RetryError extends AISDKError { private readonly [symbol] = true; // used in isInstance // note: property order determines debugging output readonly reason: RetryErrorReason; readonly lastError: unknown; readonly errors: Array<unknown>; constructor({ message, reason, errors, }: { message: string; reason: RetryErrorReason; errors: Array<unknown>; }) { super({ name, message }); this.reason = reason; this.errors = errors; // separate our last error to make debugging via log easier: this.lastError = errors[errors.length - 1]; } static isInstance(error: unknown): error is RetryError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/ai/src/util/retry-with-exponential-backoff.test.ts --- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { APICallError } from '@ai-sdk/provider'; import { retryWithExponentialBackoffRespectingRetryHeaders } from './retry-with-exponential-backoff'; describe('retryWithExponentialBackoffRespectingRetryHeaders', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('should use rate limit header delay when present and reasonable', async () => { let attempt = 0; const retryAfterMs = 3000; const fn = vi.fn().mockImplementation(async () => { attempt++; if (attempt === 1) { throw new APICallError({ message: 'Rate limited', url: 'https://api.example.com', requestBodyValues: {}, isRetryable: true, data: undefined, responseHeaders: { 'retry-after-ms': retryAfterMs.toString(), }, }); } return 'success'; }); const promise = retryWithExponentialBackoffRespectingRetryHeaders()(fn); // Should use rate limit delay (3000ms) await vi.advanceTimersByTimeAsync(retryAfterMs - 100); expect(fn).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(200); expect(fn).toHaveBeenCalledTimes(2); const result = await promise; expect(result).toBe('success'); }); it('should parse retry-after header in seconds', async () => { let attempt = 0; const retryAfterSeconds = 5; const fn = vi.fn().mockImplementation(async () => { attempt++; if (attempt === 1) { throw new APICallError({ message: 'Rate limited', url: 'https://api.example.com', requestBodyValues: {}, isRetryable: true, data: undefined, responseHeaders: { 'retry-after': retryAfterSeconds.toString(), }, }); } return 'success'; }); const promise = retryWithExponentialBackoffRespectingRetryHeaders()(fn); // Fast-forward to just before the retry delay await vi.advanceTimersByTimeAsync(retryAfterSeconds * 1000 - 100); expect(fn).toHaveBeenCalledTimes(1); // Fast-forward past the retry delay await vi.advanceTimersByTimeAsync(200); expect(fn).toHaveBeenCalledTimes(2); const result = await promise; expect(result).toBe('success'); }); it('should use exponential backoff when rate limit delay is too long', async () => { let attempt = 0; const retryAfterMs = 70000; // 70 seconds - too long const initialDelay = 2000; // Default exponential backoff const fn = vi.fn().mockImplementation(async () => { attempt++; if (attempt === 1) { throw new APICallError({ message: 'Rate limited', url: 'https://api.example.com', requestBodyValues: {}, isRetryable: true, data: undefined, responseHeaders: { 'retry-after-ms': retryAfterMs.toString(), }, }); } return 'success'; }); const promise = retryWithExponentialBackoffRespectingRetryHeaders({ initialDelayInMs: initialDelay, })(fn); // Should use exponential backoff delay (2000ms) not the rate limit (70000ms) await vi.advanceTimersByTimeAsync(initialDelay - 100); expect(fn).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(200); expect(fn).toHaveBeenCalledTimes(2); const result = await promise; expect(result).toBe('success'); }); it('should fall back to exponential backoff when no rate limit headers', async () => { let attempt = 0; const initialDelay = 2000; const fn = vi.fn().mockImplementation(async () => { attempt++; if (attempt === 1) { throw new APICallError({ message: 'Temporary error', url: 'https://api.example.com', requestBodyValues: {}, isRetryable: true, data: undefined, responseHeaders: {}, }); } return 'success'; }); const promise = retryWithExponentialBackoffRespectingRetryHeaders({ initialDelayInMs: initialDelay, })(fn); // Fast-forward to just before the initial delay await vi.advanceTimersByTimeAsync(initialDelay - 100); expect(fn).toHaveBeenCalledTimes(1); // Fast-forward past the initial delay await vi.advanceTimersByTimeAsync(200); expect(fn).toHaveBeenCalledTimes(2); const result = await promise; expect(result).toBe('success'); }); it('should handle invalid rate limit header values', async () => { let attempt = 0; const initialDelay = 2000; const fn = vi.fn().mockImplementation(async () => { attempt++; if (attempt === 1) { throw new APICallError({ message: 'Rate limited', url: 'https://api.example.com', requestBodyValues: {}, isRetryable: true, data: undefined, responseHeaders: { 'retry-after-ms': 'invalid', 'retry-after': 'not-a-number', }, }); } return 'success'; }); const promise = retryWithExponentialBackoffRespectingRetryHeaders({ initialDelayInMs: initialDelay, })(fn); // Should fall back to exponential backoff delay await vi.advanceTimersByTimeAsync(initialDelay - 100); expect(fn).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(200); expect(fn).toHaveBeenCalledTimes(2); const result = await promise; expect(result).toBe('success'); }); describe('with mocked provider responses', () => { it('should handle Anthropic 429 response with retry-after-ms header', async () => { let attempt = 0; const delayMs = 5000; const fn = vi.fn().mockImplementation(async () => { attempt++; if (attempt === 1) { // Simulate actual Anthropic 429 response with retry-after-ms throw new APICallError({ message: 'Rate limit exceeded', url: 'https://api.anthropic.com/v1/messages', requestBodyValues: {}, statusCode: 429, isRetryable: true, data: { error: { type: 'rate_limit_error', message: 'Rate limit exceeded', }, }, responseHeaders: { 'retry-after-ms': delayMs.toString(), 'x-request-id': 'req_123456', }, }); } return { content: 'Hello from Claude!' }; }); const promise = retryWithExponentialBackoffRespectingRetryHeaders()(fn); // Should use the delay from retry-after-ms header await vi.advanceTimersByTimeAsync(delayMs - 100); expect(fn).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(200); expect(fn).toHaveBeenCalledTimes(2); const result = await promise; expect(result).toEqual({ content: 'Hello from Claude!' }); }); it('should handle OpenAI 429 response with retry-after header', async () => { let attempt = 0; const delaySeconds = 30; // 30 seconds const fn = vi.fn().mockImplementation(async () => { attempt++; if (attempt === 1) { // Simulate actual OpenAI 429 response with retry-after throw new APICallError({ message: 'Rate limit reached for requests', url: 'https://api.openai.com/v1/chat/completions', requestBodyValues: {}, statusCode: 429, isRetryable: true, data: { error: { message: 'Rate limit reached for requests', type: 'requests', param: null, code: 'rate_limit_exceeded', }, }, responseHeaders: { 'retry-after': delaySeconds.toString(), 'x-request-id': 'req_abcdef123456', }, }); } return { choices: [{ message: { content: 'Hello from GPT!' } }] }; }); const promise = retryWithExponentialBackoffRespectingRetryHeaders()(fn); // Should use the delay from retry-after header (30 seconds) await vi.advanceTimersByTimeAsync(delaySeconds * 1000 - 100); expect(fn).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(200); expect(fn).toHaveBeenCalledTimes(2); const result = await promise; expect(result).toEqual({ choices: [{ message: { content: 'Hello from GPT!' } }], }); }); it('should handle multiple retries with exponential backoff progression', async () => { let attempt = 0; const baseTime = 1700000000000; vi.setSystemTime(baseTime); const fn = vi.fn().mockImplementation(async () => { attempt++; if (attempt === 1) { // First attempt: 5 second rate limit delay throw new APICallError({ message: 'Rate limited', url: 'https://api.anthropic.com/v1/messages', requestBodyValues: {}, statusCode: 429, isRetryable: true, data: undefined, responseHeaders: { 'retry-after-ms': '5000', }, }); } else if (attempt === 2) { // Second attempt: 2 second rate limit delay, but exponential backoff is 4 seconds throw new APICallError({ message: 'Rate limited', url: 'https://api.anthropic.com/v1/messages', requestBodyValues: {}, statusCode: 429, isRetryable: true, data: undefined, responseHeaders: { 'retry-after-ms': '2000', }, }); } return { content: 'Success after retries!' }; }); const promise = retryWithExponentialBackoffRespectingRetryHeaders({ maxRetries: 3, })(fn); // First retry - uses rate limit delay (5000ms) await vi.advanceTimersByTimeAsync(5000); expect(fn).toHaveBeenCalledTimes(2); // Second retry - uses exponential backoff (4000ms) which is > rate limit delay (2000ms) await vi.advanceTimersByTimeAsync(4000); expect(fn).toHaveBeenCalledTimes(3); const result = await promise; expect(result).toEqual({ content: 'Success after retries!' }); }); it('should prefer retry-after-ms over retry-after when both present', async () => { let attempt = 0; const fn = vi.fn().mockImplementation(async () => { attempt++; if (attempt === 1) { throw new APICallError({ message: 'Rate limited', url: 'https://api.example.com/v1/messages', requestBodyValues: {}, statusCode: 429, isRetryable: true, data: undefined, responseHeaders: { 'retry-after-ms': '3000', // 3 seconds - should use this 'retry-after': '10', // 10 seconds - should ignore }, }); } return 'success'; }); const promise = retryWithExponentialBackoffRespectingRetryHeaders()(fn); // Should use 3 second delay from retry-after-ms await vi.advanceTimersByTimeAsync(3000); expect(fn).toHaveBeenCalledTimes(2); const result = await promise; expect(result).toBe('success'); }); it('should handle retry-after header with HTTP date format', async () => { let attempt = 0; const baseTime = 1700000000000; const delayMs = 5000; vi.setSystemTime(baseTime); const fn = vi.fn().mockImplementation(async () => { attempt++; if (attempt === 1) { const futureDate = new Date(baseTime + delayMs).toUTCString(); throw new APICallError({ message: 'Rate limit exceeded', url: 'https://api.example.com/v1/endpoint', requestBodyValues: {}, statusCode: 429, isRetryable: true, data: undefined, responseHeaders: { 'retry-after': futureDate, }, }); } return { data: 'success' }; }); const promise = retryWithExponentialBackoffRespectingRetryHeaders()(fn); await vi.advanceTimersByTimeAsync(0); expect(fn).toHaveBeenCalledTimes(1); // Should wait for 5 seconds await vi.advanceTimersByTimeAsync(delayMs - 100); expect(fn).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(200); expect(fn).toHaveBeenCalledTimes(2); const result = await promise; expect(result).toEqual({ data: 'success' }); }); it('should fall back to exponential backoff when rate limit delay is negative', async () => { let attempt = 0; const initialDelay = 2000; const fn = vi.fn().mockImplementation(async () => { attempt++; if (attempt === 1) { throw new APICallError({ message: 'Rate limited', url: 'https://api.example.com', requestBodyValues: {}, statusCode: 429, isRetryable: true, data: undefined, responseHeaders: { 'retry-after-ms': '-1000', // Negative value }, }); } return 'success'; }); const promise = retryWithExponentialBackoffRespectingRetryHeaders({ initialDelayInMs: initialDelay, })(fn); // Should use exponential backoff delay (2000ms) not the negative rate limit await vi.advanceTimersByTimeAsync(initialDelay - 100); expect(fn).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(200); expect(fn).toHaveBeenCalledTimes(2); const result = await promise; expect(result).toBe('success'); }); }); }); --- File: /ai/packages/ai/src/util/retry-with-exponential-backoff.ts --- import { APICallError } from '@ai-sdk/provider'; import { delay, getErrorMessage, isAbortError } from '@ai-sdk/provider-utils'; import { RetryError } from './retry-error'; export type RetryFunction = <OUTPUT>( fn: () => PromiseLike<OUTPUT>, ) => PromiseLike<OUTPUT>; function getRetryDelayInMs({ error, exponentialBackoffDelay, }: { error: APICallError; exponentialBackoffDelay: number; }): number { const headers = error.responseHeaders; if (!headers) return exponentialBackoffDelay; let ms: number | undefined; // retry-ms is more precise than retry-after and used by e.g. OpenAI const retryAfterMs = headers['retry-after-ms']; if (retryAfterMs) { const timeoutMs = parseFloat(retryAfterMs); if (!Number.isNaN(timeoutMs)) { ms = timeoutMs; } } // About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After const retryAfter = headers['retry-after']; if (retryAfter && ms === undefined) { const timeoutSeconds = parseFloat(retryAfter); if (!Number.isNaN(timeoutSeconds)) { ms = timeoutSeconds * 1000; } else { ms = Date.parse(retryAfter) - Date.now(); } } // check that the delay is reasonable: if ( ms != null && !Number.isNaN(ms) && 0 <= ms && (ms < 60 * 1000 || ms < exponentialBackoffDelay) ) { return ms; } return exponentialBackoffDelay; } /** The `retryWithExponentialBackoffRespectingRetryHeaders` strategy retries a failed API call with an exponential backoff, while respecting rate limit headers (retry-after-ms and retry-after) if they are provided and reasonable (0-60 seconds). You can configure the maximum number of retries, the initial delay, and the backoff factor. */ export const retryWithExponentialBackoffRespectingRetryHeaders = ({ maxRetries = 2, initialDelayInMs = 2000, backoffFactor = 2, abortSignal, }: { maxRetries?: number; initialDelayInMs?: number; backoffFactor?: number; abortSignal?: AbortSignal; } = {}): RetryFunction => async <OUTPUT>(f: () => PromiseLike<OUTPUT>) => _retryWithExponentialBackoff(f, { maxRetries, delayInMs: initialDelayInMs, backoffFactor, abortSignal, }); async function _retryWithExponentialBackoff<OUTPUT>( f: () => PromiseLike<OUTPUT>, { maxRetries, delayInMs, backoffFactor, abortSignal, }: { maxRetries: number; delayInMs: number; backoffFactor: number; abortSignal: AbortSignal | undefined; }, errors: unknown[] = [], ): Promise<OUTPUT> { try { return await f(); } catch (error) { if (isAbortError(error)) { throw error; // don't retry when the request was aborted } if (maxRetries === 0) { throw error; // don't wrap the error when retries are disabled } const errorMessage = getErrorMessage(error); const newErrors = [...errors, error]; const tryNumber = newErrors.length; if (tryNumber > maxRetries) { throw new RetryError({ message: `Failed after ${tryNumber} attempts. Last error: ${errorMessage}`, reason: 'maxRetriesExceeded', errors: newErrors, }); } if ( error instanceof Error && APICallError.isInstance(error) && error.isRetryable === true && tryNumber <= maxRetries ) { await delay( getRetryDelayInMs({ error, exponentialBackoffDelay: delayInMs, }), { abortSignal }, ); return _retryWithExponentialBackoff( f, { maxRetries, delayInMs: backoffFactor * delayInMs, backoffFactor, abortSignal, }, newErrors, ); } if (tryNumber === 1) { throw error; // don't wrap the error when a non-retryable error occurs on the first try } throw new RetryError({ message: `Failed after ${tryNumber} attempts with non-retryable error: '${errorMessage}'`, reason: 'errorNotRetryable', errors: newErrors, }); } } --- File: /ai/packages/ai/src/util/serial-job-executor.test.ts --- import { expect, describe, it } from 'vitest'; import { SerialJobExecutor } from './serial-job-executor'; import { DelayedPromise } from './delayed-promise'; describe('SerialJobExecutor', () => { it('should execute a single job successfully', async () => { const executor = new SerialJobExecutor(); const result = new DelayedPromise<string>(); const jobPromise = executor.run(async () => { result.resolve('done'); }); await jobPromise; expect(await result.promise).toBe('done'); }); it('should execute multiple jobs in serial order', async () => { const executor = new SerialJobExecutor(); const executionOrder: number[] = []; const job1Promise = new DelayedPromise<void>(); const job2Promise = new DelayedPromise<void>(); const job3Promise = new DelayedPromise<void>(); // Start all jobs const promise1 = executor.run(async () => { executionOrder.push(1); job1Promise.resolve(); }); const promise2 = executor.run(async () => { executionOrder.push(2); job2Promise.resolve(); }); const promise3 = executor.run(async () => { executionOrder.push(3); job3Promise.resolve(); }); // Wait for all jobs to complete await Promise.all([promise1, promise2, promise3]); // Verify execution order expect(executionOrder).toEqual([1, 2, 3]); }); it('should handle job errors correctly', async () => { const executor = new SerialJobExecutor(); const error = new Error('test error'); const promise = executor.run(async () => { throw error; }); await expect(promise).rejects.toThrow(error); }); it('should execute jobs one at a time', async () => { const executor = new SerialJobExecutor(); let concurrentJobs = 0; let maxConcurrentJobs = 0; const job1 = new DelayedPromise<void>(); const job2 = new DelayedPromise<void>(); // Start two jobs const promise1 = executor.run(async () => { concurrentJobs++; maxConcurrentJobs = Math.max(maxConcurrentJobs, concurrentJobs); await job1.promise; concurrentJobs--; }); const promise2 = executor.run(async () => { concurrentJobs++; maxConcurrentJobs = Math.max(maxConcurrentJobs, concurrentJobs); await job2.promise; concurrentJobs--; }); // Let both jobs proceed and complete job1.resolve(); job2.resolve(); await Promise.all([promise1, promise2]); expect(maxConcurrentJobs).toBe(1); }); it('should handle mixed success and failure jobs', async () => { const executor = new SerialJobExecutor(); const results: string[] = []; const error = new Error('test error'); // Queue multiple jobs with mixed success/failure const promise1 = executor.run(async () => { results.push('job1'); }); const promise2 = executor.run(async () => { throw error; }); const promise3 = executor.run(async () => { results.push('job3'); }); // First job should succeed await promise1; expect(results).toEqual(['job1']); // Second job should fail await expect(promise2).rejects.toThrow(error); // Third job should still execute and succeed await promise3; expect(results).toEqual(['job1', 'job3']); }); it('should handle concurrent calls to run()', async () => { const executor = new SerialJobExecutor(); const executionOrder: number[] = []; const startOrder: number[] = []; // Create delayed promises for controlling job execution const job1 = new DelayedPromise<void>(); const job2 = new DelayedPromise<void>(); const job3 = new DelayedPromise<void>(); // Start all jobs concurrently const promises = [ executor.run(async () => { startOrder.push(1); await job1.promise; executionOrder.push(1); }), executor.run(async () => { startOrder.push(2); await job2.promise; executionOrder.push(2); }), executor.run(async () => { startOrder.push(3); await job3.promise; executionOrder.push(3); }), ].map(p => p.catch(e => e)); // Resolve jobs in reverse order to verify execution order is maintained job3.resolve(); job2.resolve(); job1.resolve(); // Wait for all jobs to complete await Promise.all(promises); // Verify that jobs were queued in the order they were submitted expect(startOrder).toEqual([1, 2, 3]); // Verify that jobs were executed in the order they were queued expect(executionOrder).toEqual([1, 2, 3]); }); }); --- File: /ai/packages/ai/src/util/serial-job-executor.ts --- import { Job } from './job'; export class SerialJobExecutor { private queue: Array<Job> = []; private isProcessing = false; private async processQueue() { if (this.isProcessing) { return; } this.isProcessing = true; while (this.queue.length > 0) { await this.queue[0](); this.queue.shift(); } this.isProcessing = false; } async run(job: Job): Promise<void> { return new Promise<void>((resolve, reject) => { this.queue.push(async () => { try { await job(); resolve(); } catch (error) { reject(error); } }); void this.processQueue(); }); } } --- File: /ai/packages/ai/src/util/simulate-readable-stream.test.ts --- import { simulateReadableStream } from './simulate-readable-stream'; import { convertReadableStreamToArray } from '@ai-sdk/provider-utils/test'; describe('simulateReadableStream', () => { let delayValues: (number | null)[] = []; const mockDelay = (ms: number | null) => { delayValues.push(ms); return Promise.resolve(); }; beforeEach(() => { delayValues = []; }); it('should create a readable stream with provided values', async () => { const values = ['a', 'b', 'c']; const stream = simulateReadableStream({ chunks: values }); expect(await convertReadableStreamToArray(stream)).toEqual(values); }); it('should respect the chunkDelayInMs setting', async () => { const stream = simulateReadableStream({ chunks: [1, 2, 3], initialDelayInMs: 500, chunkDelayInMs: 100, _internal: { delay: mockDelay }, }); await convertReadableStreamToArray(stream); // consume stream expect(delayValues).toEqual([500, 100, 100]); }); it('should handle empty values array', async () => { const stream = simulateReadableStream({ chunks: [] }); const reader = stream.getReader(); const { done, value } = await reader.read(); expect(done).toBe(true); expect(value).toBeUndefined(); }); it('should handle different types of values', async () => { const stream = simulateReadableStream({ chunks: [ { id: 1, text: 'hello' }, { id: 2, text: 'world' }, ], }); expect(await convertReadableStreamToArray(stream)).toEqual([ { id: 1, text: 'hello' }, { id: 2, text: 'world' }, ]); }); it('should skip all delays when both delay settings are null', async () => { const stream = simulateReadableStream({ chunks: [1, 2, 3], initialDelayInMs: null, chunkDelayInMs: null, _internal: { delay: mockDelay }, }); await convertReadableStreamToArray(stream); // consume stream expect(delayValues).toEqual([null, null, null]); }); it('should apply chunk delays but skip initial delay when initialDelayInMs is null', async () => { const stream = simulateReadableStream({ chunks: [1, 2, 3], initialDelayInMs: null, chunkDelayInMs: 100, _internal: { delay: mockDelay }, }); await convertReadableStreamToArray(stream); // consume stream expect(delayValues).toEqual([null, 100, 100]); }); it('should apply initial delay but skip chunk delays when chunkDelayInMs is null', async () => { const stream = simulateReadableStream({ chunks: [1, 2, 3], initialDelayInMs: 500, chunkDelayInMs: null, _internal: { delay: mockDelay }, }); await convertReadableStreamToArray(stream); // consume stream expect(delayValues).toEqual([500, null, null]); }); }); --- File: /ai/packages/ai/src/util/simulate-readable-stream.ts --- import { delay as delayFunction } from '@ai-sdk/provider-utils'; /** * Creates a ReadableStream that emits the provided values with an optional delay between each value. * * @param options - The configuration options * @param options.chunks - Array of values to be emitted by the stream * @param options.initialDelayInMs - Optional initial delay in milliseconds before emitting the first value (default: 0). Can be set to `null` to skip the initial delay. The difference between `initialDelayInMs: null` and `initialDelayInMs: 0` is that `initialDelayInMs: null` will emit the values without any delay, while `initialDelayInMs: 0` will emit the values with a delay of 0 milliseconds. * @param options.chunkDelayInMs - Optional delay in milliseconds between emitting each value (default: 0). Can be set to `null` to skip the delay. The difference between `chunkDelayInMs: null` and `chunkDelayInMs: 0` is that `chunkDelayInMs: null` will emit the values without any delay, while `chunkDelayInMs: 0` will emit the values with a delay of 0 milliseconds. * @returns A ReadableStream that emits the provided values */ export function simulateReadableStream<T>({ chunks, initialDelayInMs = 0, chunkDelayInMs = 0, _internal, }: { chunks: T[]; initialDelayInMs?: number | null; chunkDelayInMs?: number | null; _internal?: { delay?: (ms: number | null) => Promise<void>; }; }): ReadableStream<T> { const delay = _internal?.delay ?? delayFunction; let index = 0; return new ReadableStream({ async pull(controller) { if (index < chunks.length) { await delay(index === 0 ? initialDelayInMs : chunkDelayInMs); controller.enqueue(chunks[index++]); } else { controller.close(); } }, }); } --- File: /ai/packages/ai/src/util/split-array.test.ts --- import { expect, it } from 'vitest'; import { splitArray } from './split-array'; it('should split an array into chunks of the specified size', () => { const array = [1, 2, 3, 4, 5]; const size = 2; const result = splitArray(array, size); expect(result).toEqual([[1, 2], [3, 4], [5]]); }); it('should return an empty array when the input array is empty', () => { const array: number[] = []; const size = 2; const result = splitArray(array, size); expect(result).toEqual([]); }); it('should return the original array when the chunk size is greater than the array length', () => { const array = [1, 2, 3]; const size = 5; const result = splitArray(array, size); expect(result).toEqual([[1, 2, 3]]); }); it('should return the original array when the chunk size is equal to the array length', () => { const array = [1, 2, 3]; const size = 3; const result = splitArray(array, size); expect(result).toEqual([[1, 2, 3]]); }); it('should handle chunk size of 1 correctly', () => { const array = [1, 2, 3]; const size = 1; const result = splitArray(array, size); expect(result).toEqual([[1], [2], [3]]); }); it('should throw an error for chunk size of 0', () => { const array = [1, 2, 3]; const size = 0; expect(() => splitArray(array, size)).toThrow( 'chunkSize must be greater than 0', ); }); it('should throw an error for negative chunk size', () => { const array = [1, 2, 3]; const size = -1; expect(() => splitArray(array, size)).toThrow( 'chunkSize must be greater than 0', ); }); it('should handle non-integer chunk size by flooring the size', () => { const array = [1, 2, 3, 4, 5]; const size = 2.5; const result = splitArray(array, Math.floor(size)); expect(result).toEqual([[1, 2], [3, 4], [5]]); }); --- File: /ai/packages/ai/src/util/split-array.ts --- /** * Splits an array into chunks of a specified size. * * @template T - The type of elements in the array. * @param {T[]} array - The array to split. * @param {number} chunkSize - The size of each chunk. * @returns {T[][]} - A new array containing the chunks. */ export function splitArray<T>(array: T[], chunkSize: number): T[][] { if (chunkSize <= 0) { throw new Error('chunkSize must be greater than 0'); } const result = []; for (let i = 0; i < array.length; i += chunkSize) { result.push(array.slice(i, i + chunkSize)); } return result; } --- File: /ai/packages/ai/src/util/value-of.ts --- // License for this File only: // // MIT License // // Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com) // Copyright (c) Vercel, Inc. (https://vercel.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and // to permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial portions // of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. /** Create a union of the given object's values, and optionally specify which keys to get the values from. Please upvote [this issue](https://github.com/microsoft/TypeScript/issues/31438) if you want to have this type as a built-in in TypeScript. @example ``` // data.json { 'foo': 1, 'bar': 2, 'biz': 3 } // main.ts import type {ValueOf} from 'type-fest'; import data = require('./data.json'); export function getData(name: string): ValueOf<typeof data> { return data[name]; } export function onlyBar(name: string): ValueOf<typeof data, 'bar'> { return data[name]; } // file.ts import {getData, onlyBar} from './main'; getData('foo'); //=> 1 onlyBar('foo'); //=> TypeError ... onlyBar('bar'); //=> 2 ``` * @see https://github.com/sindresorhus/type-fest/blob/main/source/value-of.d.ts */ export type ValueOf< ObjectType, ValueType extends keyof ObjectType = keyof ObjectType, > = ObjectType[ValueType]; --- File: /ai/packages/ai/src/util/write-to-server-response.ts --- import { ServerResponse } from 'node:http'; /** * Writes the content of a stream to a server response. */ export function writeToServerResponse({ response, status, statusText, headers, stream, }: { response: ServerResponse; status?: number; statusText?: string; headers?: Record<string, string | number | string[]>; stream: ReadableStream<Uint8Array>; }): void { response.writeHead(status ?? 200, statusText, headers); const reader = stream.getReader(); const read = async () => { try { while (true) { const { done, value } = await reader.read(); if (done) break; response.write(value); } } catch (error) { throw error; } finally { response.end(); } }; read(); } --- File: /ai/packages/ai/src/global.ts --- import { ProviderV2 } from '@ai-sdk/provider'; // add type of the global default provider variable to the globalThis object declare global { var AI_SDK_DEFAULT_PROVIDER: ProviderV2 | undefined; } --- File: /ai/packages/ai/src/index.ts --- // re-exports: export { asSchema, createIdGenerator, dynamicTool, generateId, jsonSchema, tool, zodSchema, type IdGenerator, type InferToolInput, type InferToolOutput, type Schema, type Tool, type ToolCallOptions, type ToolExecuteFunction, } from '@ai-sdk/provider-utils'; // directory exports export * from './agent'; export * from './embed'; export * from './error'; export * from './generate-image'; export * from './generate-object'; export * from './generate-speech'; export * from './generate-text'; export * from './middleware'; export * from './prompt'; export * from './registry'; export * from './text-stream'; export * from './tool'; export * from './transcribe'; export * from './types'; export * from './ui'; export * from './ui-message-stream'; export * from './util'; // telemetry types: export type { TelemetrySettings } from './telemetry/telemetry-settings'; // import globals import './global'; --- File: /ai/packages/ai/test/index.ts --- export { convertArrayToAsyncIterable, convertArrayToReadableStream, convertReadableStreamToArray, mockId, } from '@ai-sdk/provider-utils/test'; export { MockEmbeddingModelV2 } from '../src/test/mock-embedding-model-v2'; export { MockImageModelV2 } from '../src/test/mock-image-model-v2'; export { MockLanguageModelV2 } from '../src/test/mock-language-model-v2'; export { MockProviderV2 } from '../src/test/mock-provider-v2'; export { MockSpeechModelV2 } from '../src/test/mock-speech-model-v2'; export { MockTranscriptionModelV2 } from '../src/test/mock-transcription-model-v2'; export { mockValues } from '../src/test/mock-values'; import { simulateReadableStream as originalSimulateReadableStream } from '../src/util/simulate-readable-stream'; /** * @deprecated Use `simulateReadableStream` from `ai` instead. */ export const simulateReadableStream = originalSimulateReadableStream; --- File: /ai/packages/ai/.eslintrc.js --- module.exports = { root: true, extends: ['vercel-ai'], }; --- File: /ai/packages/ai/index.ts --- // TODO remove once we can set the source folder in tsconfig.json to src/ export * from './src'; --- File: /ai/packages/ai/internal.d.ts --- export * from './dist/internal'; --- File: /ai/packages/ai/mcp-stdio.d.ts --- export * from './dist/mcp-stdio'; --- File: /ai/packages/ai/test.d.ts --- export * from './dist/test'; --- File: /ai/packages/ai/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ // Universal APIs { entry: ['src/index.ts'], format: ['cjs', 'esm'], external: ['react', 'svelte', 'vue'], dts: true, sourcemap: true, }, // Internal APIs { entry: ['internal/index.ts'], outDir: 'dist/internal', format: ['cjs', 'esm'], dts: true, sourcemap: true, }, // Test utilities { entry: ['test/index.ts'], outDir: 'dist/test', format: ['cjs', 'esm'], dts: true, sourcemap: true, }, // MCP stdio { entry: ['mcp-stdio/index.ts'], outDir: 'dist/mcp-stdio', format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/ai/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts{,x}'], exclude: [ '**/*.ui.test.ts{,x}', '**/*.e2e.test.ts{,x}', '**/node_modules/**', ], typecheck: { enabled: true, }, }, }); --- File: /ai/packages/ai/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts{,x}'], exclude: [ '**/*.ui.test.ts{,x}', '**/*.e2e.test.ts{,x}', '**/node_modules/**', ], typecheck: { enabled: true, }, }, }); --- File: /ai/packages/amazon-bedrock/src/bedrock-api-types.ts --- import { JSONObject } from '@ai-sdk/provider'; export interface BedrockConverseInput { system?: BedrockSystemMessages; messages: BedrockMessages; toolConfig?: BedrockToolConfiguration; inferenceConfig?: { maxOutputTokens?: number; temperature?: number; topP?: number; topK?: number; stopSequences?: string[]; }; additionalModelRequestFields?: Record<string, unknown>; guardrailConfig?: | BedrockGuardrailConfiguration | BedrockGuardrailStreamConfiguration | undefined; } export type BedrockSystemMessages = Array<BedrockSystemContentBlock>; export type BedrockMessages = Array< BedrockAssistantMessage | BedrockUserMessage >; export interface BedrockAssistantMessage { role: 'assistant'; content: Array<BedrockContentBlock>; } export interface BedrockUserMessage { role: 'user'; content: Array<BedrockContentBlock>; } export const BEDROCK_CACHE_POINT = { cachePoint: { type: 'default' }, } as const; export type BedrockCachePoint = { cachePoint: { type: 'default' } }; export type BedrockSystemContentBlock = { text: string } | BedrockCachePoint; export interface BedrockGuardrailConfiguration { guardrails?: Array<{ name: string; description?: string; parameters?: Record<string, unknown>; }>; } export type BedrockGuardrailStreamConfiguration = BedrockGuardrailConfiguration; export interface BedrockToolInputSchema { json: Record<string, unknown>; } export interface BedrockTool { toolSpec: { name: string; description?: string; inputSchema: { json: JSONObject }; }; } export interface BedrockToolConfiguration { tools?: Array<BedrockTool | BedrockCachePoint>; toolChoice?: | { tool: { name: string } } | { auto: {} } | { any: {} } | undefined; } export const BEDROCK_STOP_REASONS = [ 'stop', 'stop_sequence', 'end_turn', 'length', 'max_tokens', 'content-filter', 'content_filtered', 'guardrail_intervened', 'tool-calls', 'tool_use', ] as const; export type BedrockStopReason = (typeof BEDROCK_STOP_REASONS)[number]; /** * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ImageBlock.html */ export const BEDROCK_IMAGE_MIME_TYPES = { 'image/jpeg': 'jpeg', 'image/png': 'png', 'image/gif': 'gif', 'image/webp': 'webp', } as const; type BedrockImageFormats = typeof BEDROCK_IMAGE_MIME_TYPES; export type BedrockImageFormat = BedrockImageFormats[keyof BedrockImageFormats]; export type BedrockImageMimeType = keyof BedrockImageFormats; /** * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_DocumentBlock.html */ export const BEDROCK_DOCUMENT_MIME_TYPES = { 'application/pdf': 'pdf', 'text/csv': 'csv', 'application/msword': 'doc', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx', 'application/vnd.ms-excel': 'xls', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx', 'text/html': 'html', 'text/plain': 'txt', 'text/markdown': 'md', } as const; type BedrockDocumentFormats = typeof BEDROCK_DOCUMENT_MIME_TYPES; export type BedrockDocumentFormat = BedrockDocumentFormats[keyof BedrockDocumentFormats]; export type BedrockDocumentMimeType = keyof BedrockDocumentFormats; export interface BedrockDocumentBlock { document: { format: BedrockDocumentFormat; name: string; source: { bytes: string; }; }; } export interface BedrockGuardrailConverseContentBlock { guardContent: unknown; } export interface BedrockImageBlock { image: { format: BedrockImageFormat; source: { bytes: string; }; }; } export interface BedrockToolResultBlock { toolResult: { toolUseId: string; content: Array<BedrockTextBlock | BedrockImageBlock>; }; } export interface BedrockToolUseBlock { toolUse: { toolUseId: string; name: string; input: Record<string, unknown>; }; } export interface BedrockTextBlock { text: string; } export interface BedrockReasoningContentBlock { reasoningContent: { reasoningText: { text: string; signature?: string; }; }; } export interface BedrockRedactedReasoningContentBlock { reasoningContent: { redactedReasoning: { data: string; }; }; } export type BedrockContentBlock = | BedrockDocumentBlock | BedrockGuardrailConverseContentBlock | BedrockImageBlock | BedrockTextBlock | BedrockToolResultBlock | BedrockToolUseBlock | BedrockReasoningContentBlock | BedrockRedactedReasoningContentBlock | BedrockCachePoint; --- File: /ai/packages/amazon-bedrock/src/bedrock-chat-language-model.test.ts --- import { LanguageModelV2Prompt } from '@ai-sdk/provider'; import { createTestServer, convertReadableStreamToArray, } from '@ai-sdk/provider-utils/test'; import { BedrockChatLanguageModel } from './bedrock-chat-language-model'; import { vi } from 'vitest'; import { injectFetchHeaders } from './inject-fetch-headers'; import { BedrockReasoningContentBlock, BedrockRedactedReasoningContentBlock, } from './bedrock-api-types'; import { anthropicTools, prepareTools } from '@ai-sdk/anthropic/internal'; import { z } from 'zod/v4'; const mockPrepareAnthropicTools = vi.mocked(prepareTools); vi.mock('@ai-sdk/anthropic/internal', async importOriginal => { const original = await importOriginal<typeof import('@ai-sdk/anthropic/internal')>(); return { ...original, prepareTools: vi.fn(), anthropicTools: { ...original.anthropicTools, bash_20241022: (args: any) => ({ type: 'provider-defined', id: 'anthropic.bash_20241022', name: 'bash', args, inputSchema: z.object({ command: z.string(), restart: z.boolean().optional(), }), }), }, }; }); const TEST_PROMPT: LanguageModelV2Prompt = [ { role: 'system', content: 'System Prompt' }, { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; const mockTrace = { guardrail: { inputAssessment: { '1abcd2ef34gh': { contentPolicy: { filters: [ { action: 'BLOCKED' as const, confidence: 'LOW' as const, type: 'INSULTS' as const, }, ], }, wordPolicy: { managedWordLists: [ { action: 'BLOCKED' as const, match: '<rude word>', type: 'PROFANITY' as const, }, ], }, }, }, }, }; const fakeFetchWithAuth = injectFetchHeaders({ 'x-amz-auth': 'test-auth' }); const modelId = 'anthropic.claude-3-haiku-20240307-v1:0'; const anthropicModelId = 'anthropic.claude-3-5-sonnet-20240620-v1:0'; // Define at top level const baseUrl = 'https://bedrock-runtime.us-east-1.amazonaws.com'; const streamUrl = `${baseUrl}/model/${encodeURIComponent( modelId, )}/converse-stream`; const generateUrl = `${baseUrl}/model/${encodeURIComponent(modelId)}/converse`; const anthropicGenerateUrl = `${baseUrl}/model/${encodeURIComponent( anthropicModelId, )}/converse`; const server = createTestServer({ [generateUrl]: {}, [streamUrl]: { response: { type: 'stream-chunks', chunks: [], }, }, // Configure the server for the Anthropic model from the start [anthropicGenerateUrl]: {}, }); beforeEach(() => { // Reset stream chunks for the default model server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [], }; // Reset the response for the anthropic model to a default empty state server.urls[anthropicGenerateUrl].response = { type: 'json-value', body: {}, }; mockPrepareAnthropicTools.mockClear(); }); const model = new BedrockChatLanguageModel(modelId, { baseUrl: () => baseUrl, headers: {}, fetch: fakeFetchWithAuth, generateId: () => 'test-id', }); let mockOptions: { success: boolean; errorValue?: any } = { success: true }; describe('doStream', () => { beforeEach(() => { mockOptions = { success: true, errorValue: undefined }; }); vi.mock('./bedrock-event-stream-response-handler', () => ({ createBedrockEventStreamResponseHandler: (schema: any) => { return async ({ response }: { response: Response }) => { let chunks: { success: boolean; value: any }[] = []; if (mockOptions.success) { const text = await response.text(); chunks = text .split('\n') .filter(Boolean) .map(chunk => { const parsedChunk = JSON.parse(chunk); return { success: true, value: parsedChunk, rawValue: parsedChunk, }; }); } const headers = Object.fromEntries<string>([...response.headers]); return { responseHeaders: headers, value: new ReadableStream({ start(controller) { if (mockOptions.success) { chunks.forEach(chunk => controller.enqueue(chunk)); } else { controller.enqueue({ success: false, error: mockOptions.errorValue, }); } controller.close(); }, }), }; }; }, })); function setupMockEventStreamHandler( options: { success?: boolean; errorValue?: any } = { success: true }, ) { mockOptions = { ...mockOptions, ...options }; } it('should stream text deltas with metadata and usage', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [ JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { text: 'Hello' }, }, }) + '\n', JSON.stringify({ contentBlockDelta: { contentBlockIndex: 1, delta: { text: ', ' }, }, }) + '\n', JSON.stringify({ contentBlockDelta: { contentBlockIndex: 2, delta: { text: 'World!' }, }, }) + '\n', JSON.stringify({ metadata: { usage: { inputTokens: 4, outputTokens: 34, totalTokens: 38 }, metrics: { latencyMs: 10 }, }, }) + '\n', JSON.stringify({ messageStop: { stopReason: 'stop_sequence', }, }) + '\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "id": "1", "type": "text-start", }, { "delta": ", ", "id": "1", "type": "text-delta", }, { "id": "2", "type": "text-start", }, { "delta": "World!", "id": "2", "type": "text-delta", }, { "finishReason": "stop", "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 4, "outputTokens": 34, "totalTokens": 38, }, }, ] `); }); it('should stream tool deltas', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [ JSON.stringify({ contentBlockStart: { contentBlockIndex: 0, start: { toolUse: { toolUseId: 'tool-use-id', name: 'test-tool' }, }, }, }) + '\n', JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { toolUse: { input: '{"value":' } }, }, }) + '\n', JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { toolUse: { input: '"Sparkle Day"}' } }, }, }) + '\n', JSON.stringify({ contentBlockStop: { contentBlockIndex: 0 }, }) + '\n', JSON.stringify({ messageStop: { stopReason: 'tool_use', }, }) + '\n', ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], toolChoice: { type: 'tool', toolName: 'test-tool' }, prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "tool-use-id", "toolName": "test-tool", "type": "tool-input-start", }, { "delta": "{"value":", "id": "tool-use-id", "type": "tool-input-delta", }, { "delta": ""Sparkle Day"}", "id": "tool-use-id", "type": "tool-input-delta", }, { "id": "tool-use-id", "type": "tool-input-end", }, { "input": "{"value":"Sparkle Day"}", "toolCallId": "tool-use-id", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "tool-calls", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it('should stream parallel tool calls', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [ JSON.stringify({ contentBlockStart: { contentBlockIndex: 0, start: { toolUse: { toolUseId: 'tool-use-id-1', name: 'test-tool-1' }, }, }, }) + '\n', JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { toolUse: { input: '{"value1":' } }, }, }) + '\n', JSON.stringify({ contentBlockStart: { contentBlockIndex: 1, start: { toolUse: { toolUseId: 'tool-use-id-2', name: 'test-tool-2' }, }, }, }) + '\n', JSON.stringify({ contentBlockDelta: { contentBlockIndex: 1, delta: { toolUse: { input: '{"value2":' } }, }, }) + '\n', JSON.stringify({ contentBlockDelta: { contentBlockIndex: 1, delta: { toolUse: { input: '"Sparkle Day"}' } }, }, }) + '\n', JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { toolUse: { input: '"Sparkle Day"}' } }, }, }) + '\n', JSON.stringify({ contentBlockStop: { contentBlockIndex: 0 }, }) + '\n', JSON.stringify({ contentBlockStop: { contentBlockIndex: 1 }, }) + '\n', JSON.stringify({ messageStop: { stopReason: 'tool_use', }, }) + '\n', ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'test-tool-1', inputSchema: { type: 'object', properties: { value1: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, { type: 'function', name: 'test-tool-2', inputSchema: { type: 'object', properties: { value2: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], toolChoice: { type: 'tool', toolName: 'test-tool' }, prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "tool-use-id-1", "toolName": "test-tool-1", "type": "tool-input-start", }, { "delta": "{"value1":", "id": "tool-use-id-1", "type": "tool-input-delta", }, { "id": "tool-use-id-2", "toolName": "test-tool-2", "type": "tool-input-start", }, { "delta": "{"value2":", "id": "tool-use-id-2", "type": "tool-input-delta", }, { "delta": ""Sparkle Day"}", "id": "tool-use-id-2", "type": "tool-input-delta", }, { "delta": ""Sparkle Day"}", "id": "tool-use-id-1", "type": "tool-input-delta", }, { "id": "tool-use-id-1", "type": "tool-input-end", }, { "input": "{"value1":"Sparkle Day"}", "toolCallId": "tool-use-id-1", "toolName": "test-tool-1", "type": "tool-call", }, { "id": "tool-use-id-2", "type": "tool-input-end", }, { "input": "{"value2":"Sparkle Day"}", "toolCallId": "tool-use-id-2", "toolName": "test-tool-2", "type": "tool-call", }, { "finishReason": "tool-calls", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it('should handle error stream parts', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [ JSON.stringify({ internalServerException: { message: 'Internal Server Error', name: 'InternalServerException', $fault: 'server', $metadata: {}, }, }) + '\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "error": { "$fault": "server", "$metadata": {}, "message": "Internal Server Error", "name": "InternalServerException", }, "type": "error", }, { "finishReason": "error", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it('should handle modelStreamErrorException error', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [ JSON.stringify({ modelStreamErrorException: { message: 'Model Stream Error', name: 'ModelStreamErrorException', $fault: 'server', $metadata: {}, }, }) + '\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "error": { "$fault": "server", "$metadata": {}, "message": "Model Stream Error", "name": "ModelStreamErrorException", }, "type": "error", }, { "finishReason": "error", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it('should handle throttlingException error', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [ JSON.stringify({ throttlingException: { message: 'Throttling Error', name: 'ThrottlingException', $fault: 'server', $metadata: {}, }, }) + '\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "error": { "$fault": "server", "$metadata": {}, "message": "Throttling Error", "name": "ThrottlingException", }, "type": "error", }, { "finishReason": "error", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it('should handle validationException error', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [ JSON.stringify({ validationException: { message: 'Validation Error', name: 'ValidationException', $fault: 'server', $metadata: {}, }, }) + '\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "error": { "$fault": "server", "$metadata": {}, "message": "Validation Error", "name": "ValidationException", }, "type": "error", }, { "finishReason": "error", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it('should handle failed chunk parsing', async () => { setupMockEventStreamHandler({ success: false, errorValue: { message: 'Chunk Parsing Failed' }, }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "error": { "message": "Chunk Parsing Failed", }, "type": "error", }, { "finishReason": "error", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it('should pass the messages and the model', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [], }; await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ messages: [{ role: 'user', content: [{ text: 'Hello' }] }], system: [{ text: 'System Prompt' }], }); }); it('should support guardrails', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [], }; await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, providerOptions: { bedrock: { guardrailConfig: { guardrailIdentifier: '-1', guardrailVersion: '1', trace: 'enabled', streamProcessingMode: 'async', }, }, }, }); expect(await server.calls[0].requestBodyJson).toMatchObject({ messages: [{ role: 'user', content: [{ text: 'Hello' }] }], system: [{ text: 'System Prompt' }], guardrailConfig: { guardrailIdentifier: '-1', guardrailVersion: '1', trace: 'enabled', streamProcessingMode: 'async', }, }); }); it('should include trace information in providerMetadata', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [ JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { text: 'Hello' }, }, }) + '\n', JSON.stringify({ metadata: { usage: { inputTokens: 4, outputTokens: 34, totalTokens: 38 }, metrics: { latencyMs: 10 }, trace: mockTrace, }, }) + '\n', JSON.stringify({ messageStop: { stopReason: 'stop_sequence', }, }) + '\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "finishReason": "stop", "providerMetadata": { "bedrock": { "trace": { "guardrail": { "inputAssessment": { "1abcd2ef34gh": { "contentPolicy": { "filters": [ { "action": "BLOCKED", "confidence": "LOW", "type": "INSULTS", }, ], }, "wordPolicy": { "managedWordLists": [ { "action": "BLOCKED", "match": "<rude word>", "type": "PROFANITY", }, ], }, }, }, }, }, }, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 4, "outputTokens": 34, "totalTokens": 38, }, }, ] `); }); it('should include response headers in rawResponse', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', headers: { 'x-amzn-requestid': 'test-request-id', 'x-amzn-trace-id': 'test-trace-id', }, chunks: [ JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { text: 'Hello' }, }, }) + '\n', JSON.stringify({ messageStop: { stopReason: 'stop_sequence', }, }) + '\n', ], }; const result = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(result.response?.headers).toEqual({ 'cache-control': 'no-cache', connection: 'keep-alive', 'content-type': 'text/event-stream', 'x-amzn-requestid': 'test-request-id', 'x-amzn-trace-id': 'test-trace-id', }); }); it('should properly combine headers from all sources', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', headers: { 'x-amzn-requestid': 'test-request-id', 'x-amzn-trace-id': 'test-trace-id', }, chunks: [ JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { text: 'Hello' }, }, }) + '\n', JSON.stringify({ messageStop: { stopReason: 'stop_sequence', }, }) + '\n', ], }; const optionsHeaders = { 'options-header': 'options-value', 'shared-header': 'options-shared', }; const model = new BedrockChatLanguageModel(modelId, { baseUrl: () => baseUrl, headers: { 'model-header': 'model-value', 'shared-header': 'model-shared', }, fetch: injectFetchHeaders({ 'options-header': 'options-value', 'model-header': 'model-value', 'shared-header': 'options-shared', 'signed-header': 'signed-value', authorization: 'AWS4-HMAC-SHA256...', }), generateId: () => 'test-id', }); await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, headers: optionsHeaders, }); const requestHeaders = server.calls[0].requestHeaders; expect(requestHeaders['options-header']).toBe('options-value'); expect(requestHeaders['model-header']).toBe('model-value'); expect(requestHeaders['signed-header']).toBe('signed-value'); expect(requestHeaders['authorization']).toBe('AWS4-HMAC-SHA256...'); expect(requestHeaders['shared-header']).toBe('options-shared'); }); it('should work with partial headers', async () => { setupMockEventStreamHandler(); const model = new BedrockChatLanguageModel(modelId, { baseUrl: () => baseUrl, headers: { 'model-header': 'model-value', }, fetch: injectFetchHeaders({ 'model-header': 'model-value', 'signed-header': 'signed-value', authorization: 'AWS4-HMAC-SHA256...', }), generateId: () => 'test-id', }); await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const requestHeaders = server.calls[0].requestHeaders; expect(requestHeaders['model-header']).toBe('model-value'); expect(requestHeaders['signed-header']).toBe('signed-value'); expect(requestHeaders['authorization']).toBe('AWS4-HMAC-SHA256...'); }); it('should include providerOptions in the request for streaming calls', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [ JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { text: 'Dummy' }, }, }) + '\n', JSON.stringify({ messageStop: { stopReason: 'stop_sequence' }, }) + '\n', ], }; await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, providerOptions: { bedrock: { foo: 'bar', }, }, }); // Verify the outgoing request body includes "foo" at the top level. const body = await server.calls[0].requestBodyJson; expect(body).toMatchObject({ foo: 'bar' }); }); it('should include cache token usage in providerMetadata', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [ JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { text: 'Hello' }, }, }) + '\n', JSON.stringify({ metadata: { usage: { inputTokens: 4, outputTokens: 34, totalTokens: 38, cacheReadInputTokens: 2, cacheWriteInputTokens: 3, }, }, }) + '\n', JSON.stringify({ messageStop: { stopReason: 'stop_sequence', }, }) + '\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "finishReason": "stop", "providerMetadata": { "bedrock": { "usage": { "cacheWriteInputTokens": 3, }, }, }, "type": "finish", "usage": { "cachedInputTokens": 2, "inputTokens": 4, "outputTokens": 34, "totalTokens": 38, }, }, ] `); }); it('should handle system messages with cache points', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [ JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { text: 'Hello' }, }, }) + '\n', JSON.stringify({ messageStop: { stopReason: 'stop_sequence', }, }) + '\n', ], }; await model.doStream({ prompt: [ { role: 'system', content: 'System Prompt', providerOptions: { bedrock: { cachePoint: { type: 'default' } } }, }, { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ], includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toMatchObject({ system: [{ text: 'System Prompt' }, { cachePoint: { type: 'default' } }], messages: [{ role: 'user', content: [{ text: 'Hello' }] }], }); }); it('should stream reasoning text deltas', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [ JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { reasoningContent: { text: 'I am thinking' }, }, }, }) + '\n', JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { reasoningContent: { text: ' about this problem...' }, }, }, }) + '\n', JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { reasoningContent: { signature: 'abc123signature' }, }, }, }) + '\n', JSON.stringify({ contentBlockDelta: { contentBlockIndex: 1, delta: { text: 'Based on my reasoning, the answer is 42.' }, }, }) + '\n', JSON.stringify({ messageStop: { stopReason: 'stop_sequence', }, }) + '\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "0", "type": "reasoning-start", }, { "delta": "I am thinking", "id": "0", "type": "reasoning-delta", }, { "delta": " about this problem...", "id": "0", "type": "reasoning-delta", }, { "delta": "", "id": "0", "providerMetadata": { "bedrock": { "signature": "abc123signature", }, }, "type": "reasoning-delta", }, { "id": "1", "type": "text-start", }, { "delta": "Based on my reasoning, the answer is 42.", "id": "1", "type": "text-delta", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it('should stream redacted reasoning', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [ JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { reasoningContent: { data: 'redacted-reasoning-data' }, }, }, }) + '\n', JSON.stringify({ contentBlockDelta: { contentBlockIndex: 1, delta: { text: 'Here is my answer.' }, }, }) + '\n', JSON.stringify({ messageStop: { stopReason: 'stop_sequence', }, }) + '\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "delta": "", "id": "0", "providerMetadata": { "bedrock": { "redactedData": "redacted-reasoning-data", }, }, "type": "reasoning-delta", }, { "id": "1", "type": "text-start", }, { "delta": "Here is my answer.", "id": "1", "type": "text-delta", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it('should include raw chunks when includeRawChunks is true', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [ JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { text: 'Hello' }, }, }) + '\n', JSON.stringify({ messageStop: { stopReason: 'stop_sequence', }, }) + '\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: true, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "rawValue": { "contentBlockDelta": { "contentBlockIndex": 0, "delta": { "text": "Hello", }, }, }, "type": "raw", }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "rawValue": { "messageStop": { "stopReason": "stop_sequence", }, }, "type": "raw", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it('should transform reasoningConfig to thinking in stream requests', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [ JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { text: 'Hello' }, }, }) + '\n', JSON.stringify({ messageStop: { stopReason: 'stop_sequence', }, }) + '\n', ], }; await model.doStream({ prompt: TEST_PROMPT, maxOutputTokens: 100, includeRawChunks: false, providerOptions: { bedrock: { reasoningConfig: { type: 'enabled', budgetTokens: 2000, }, }, }, }); const requestBody = await server.calls[0].requestBodyJson; // Should contain thinking in additionalModelRequestFields expect(requestBody).toMatchObject({ additionalModelRequestFields: { thinking: { type: 'enabled', budget_tokens: 2000, }, }, // Should have adjusted maxOutputTokens (100 + 2000) inferenceConfig: { maxOutputTokens: 2100, }, }); // Should NOT contain reasoningConfig at the top level expect(requestBody).not.toHaveProperty('reasoningConfig'); }); it('should handle JSON response format in streaming', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { type: 'stream-chunks', chunks: [ JSON.stringify({ contentBlockStart: { contentBlockIndex: 0, start: { toolUse: { toolUseId: 'json-tool-id', name: 'json' }, }, }, }) + '\n', JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { toolUse: { input: '{"value":' } }, }, }) + '\n', JSON.stringify({ contentBlockDelta: { contentBlockIndex: 0, delta: { toolUse: { input: '"test"}' } }, }, }) + '\n', JSON.stringify({ contentBlockStop: { contentBlockIndex: 0 }, }) + '\n', JSON.stringify({ messageStop: { stopReason: 'tool_use', }, }) + '\n', ], }; const { stream } = await model.doStream({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'Generate JSON' }] }, ], responseFormat: { type: 'json', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], }, }, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "0", "type": "text-start", }, { "delta": "{"value":"test"}", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "tool-calls", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); }); describe('doGenerate', () => { function prepareJsonResponse({ content = [{ type: 'text', text: 'Hello, World!' }], usage = { inputTokens: 4, outputTokens: 34, totalTokens: 38, cacheReadInputTokens: undefined, cacheWriteInputTokens: undefined, }, stopReason = 'stop_sequence', trace, }: { content?: Array< | { type: 'text'; text: string } | { type: 'thinking'; thinking: string; signature: string } | { type: 'tool_use'; id: string; name: string; input: unknown } | BedrockReasoningContentBlock | BedrockRedactedReasoningContentBlock >; toolCalls?: Array<{ id?: string; name: string; args: Record<string, unknown>; }>; usage?: { inputTokens: number; outputTokens: number; totalTokens: number; cacheReadInputTokens?: number; cacheWriteInputTokens?: number; }; stopReason?: string; trace?: typeof mockTrace; reasoningContent?: | BedrockReasoningContentBlock | BedrockRedactedReasoningContentBlock | Array< BedrockReasoningContentBlock | BedrockRedactedReasoningContentBlock >; }) { server.urls[generateUrl].response = { type: 'json-value', body: { output: { message: { role: 'assistant', content, }, }, usage, stopReason, ...(trace ? { trace } : {}), }, }; } it('should extract text response', async () => { prepareJsonResponse({ content: [{ type: 'text', text: 'Hello, World!' }] }); const result = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(result.content).toMatchInlineSnapshot(` [ { "text": "Hello, World!", "type": "text", }, ] `); }); it('should extract usage', async () => { prepareJsonResponse({ usage: { inputTokens: 4, outputTokens: 34, totalTokens: 38 }, }); const { usage } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(usage).toMatchInlineSnapshot(` { "cachedInputTokens": undefined, "inputTokens": 4, "outputTokens": 34, "totalTokens": 38, } `); }); it('should extract finish reason', async () => { prepareJsonResponse({ stopReason: 'stop_sequence' }); const { finishReason } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(finishReason).toStrictEqual('stop'); }); it('should support unknown finish reason', async () => { prepareJsonResponse({ stopReason: 'eos' }); const { finishReason } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(finishReason).toStrictEqual('unknown'); }); it('should pass the model and the messages', async () => { prepareJsonResponse({}); await model.doGenerate({ prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ messages: [{ role: 'user', content: [{ text: 'Hello' }] }], system: [{ text: 'System Prompt' }], }); }); it('should pass settings', async () => { prepareJsonResponse({}); await model.doGenerate({ prompt: TEST_PROMPT, maxOutputTokens: 100, temperature: 0.5, topP: 0.5, topK: 1, }); expect(await server.calls[0].requestBodyJson).toMatchObject({ inferenceConfig: { maxOutputTokens: 100, temperature: 0.5, topP: 0.5, topK: 1, }, }); }); it('should support guardrails', async () => { prepareJsonResponse({}); await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { bedrock: { guardrailConfig: { guardrailIdentifier: '-1', guardrailVersion: '1', trace: 'enabled', }, }, }, }); expect(await server.calls[0].requestBodyJson).toMatchObject({ guardrailConfig: { guardrailIdentifier: '-1', guardrailVersion: '1', trace: 'enabled', }, }); }); it('should include trace information in providerMetadata', async () => { prepareJsonResponse({ trace: mockTrace }); const result = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(result.providerMetadata?.bedrock.trace).toMatchObject(mockTrace); }); it('should include response headers in rawResponse', async () => { server.urls[generateUrl].response = { type: 'json-value', headers: { 'x-amzn-requestid': 'test-request-id', 'x-amzn-trace-id': 'test-trace-id', }, body: { output: { message: { role: 'assistant', content: [{ text: 'Testing' }], }, }, usage: { inputTokens: 4, outputTokens: 34, totalTokens: 38, }, stopReason: 'stop_sequence', }, }; const result = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(result.response?.headers).toEqual({ 'x-amzn-requestid': 'test-request-id', 'x-amzn-trace-id': 'test-trace-id', 'content-type': 'application/json', 'content-length': '164', }); }); it('should pass tools and tool choice correctly', async () => { prepareJsonResponse({}); await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool-1', description: 'A test tool', inputSchema: { type: 'object', properties: { param1: { type: 'string' }, param2: { type: 'number' }, }, required: ['param1'], additionalProperties: false, }, }, ], toolChoice: { type: 'auto' }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toMatchObject({ toolConfig: { tools: [ { toolSpec: { name: 'test-tool-1', description: 'A test tool', inputSchema: { json: { type: 'object', properties: { param1: { type: 'string' }, param2: { type: 'number' }, }, required: ['param1'], additionalProperties: false, }, }, }, }, ], }, }); }); it('should handle Anthropic provider-defined tools', async () => { mockPrepareAnthropicTools.mockReturnValue({ tools: [{ name: 'bash', type: 'bash_20241022' }], toolChoice: { type: 'auto' }, toolWarnings: [], betas: new Set(['computer-use-2024-10-22']), }); // Set up the mock response for this specific URL and test case server.urls[anthropicGenerateUrl].response = { type: 'json-value', body: { output: { message: { role: 'assistant', content: [ { toolUse: { toolUseId: 'tool-use-id', name: 'bash', input: { command: 'ls -l' }, }, }, ], }, }, usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 }, stopReason: 'tool_use', }, }; const anthropicModel = new BedrockChatLanguageModel(anthropicModelId, { baseUrl: () => baseUrl, headers: {}, // No fetch property: defaults to global fetch, which is mocked by the test server. generateId: () => 'test-id', }); const result = await anthropicModel.doGenerate({ prompt: TEST_PROMPT, tools: [ { type: 'provider-defined', id: 'anthropic.bash_20241022', name: 'bash', args: {}, }, ], toolChoice: { type: 'auto' }, }); const requestBody = await server.calls[0].requestBodyJson; const requestHeaders = server.calls[0].requestHeaders; expect(requestHeaders['anthropic-beta']).toBe('computer-use-2024-10-22'); expect(requestBody.additionalModelRequestFields).toEqual({ tool_choice: { type: 'auto' }, }); expect(requestBody.toolConfig).toBeDefined(); expect(requestBody.toolConfig.tools).toHaveLength(1); expect(requestBody.toolConfig.tools[0].toolSpec.name).toBe('bash'); expect(requestBody.toolConfig.tools[0].toolSpec.inputSchema.json).toEqual({ $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { command: { type: 'string' }, restart: { type: 'boolean' }, }, required: ['command'], additionalProperties: false, }); expect(result.warnings).toEqual([]); expect(result.content).toMatchInlineSnapshot(` [ { "input": "{"command":"ls -l"}", "toolCallId": "tool-use-id", "toolName": "bash", "type": "tool-call", }, ] `); }); it('should properly combine headers from all sources', async () => { prepareJsonResponse({}); const optionsHeaders = { 'options-header': 'options-value', 'shared-header': 'options-shared', }; const model = new BedrockChatLanguageModel(modelId, { baseUrl: () => baseUrl, headers: { 'model-header': 'model-value', 'shared-header': 'model-shared', }, fetch: injectFetchHeaders({ 'options-header': 'options-value', 'model-header': 'model-value', 'shared-header': 'options-shared', 'signed-header': 'signed-value', authorization: 'AWS4-HMAC-SHA256...', }), generateId: () => 'test-id', }); await model.doGenerate({ prompt: TEST_PROMPT, headers: optionsHeaders, }); const requestHeaders = server.calls[0].requestHeaders; expect(requestHeaders['options-header']).toBe('options-value'); expect(requestHeaders['model-header']).toBe('model-value'); expect(requestHeaders['signed-header']).toBe('signed-value'); expect(requestHeaders['authorization']).toBe('AWS4-HMAC-SHA256...'); expect(requestHeaders['shared-header']).toBe('options-shared'); }); it('should work with partial headers', async () => { prepareJsonResponse({}); const model = new BedrockChatLanguageModel(modelId, { baseUrl: () => baseUrl, headers: { 'model-header': 'model-value', }, fetch: injectFetchHeaders({ 'model-header': 'model-value', 'signed-header': 'signed-value', authorization: 'AWS4-HMAC-SHA256...', }), generateId: () => 'test-id', }); await model.doGenerate({ prompt: TEST_PROMPT, }); const requestHeaders = server.calls[0].requestHeaders; expect(requestHeaders['model-header']).toBe('model-value'); expect(requestHeaders['signed-header']).toBe('signed-value'); expect(requestHeaders['authorization']).toBe('AWS4-HMAC-SHA256...'); }); it('should include providerOptions in the request for generate calls', async () => { prepareJsonResponse({ content: [{ type: 'text', text: 'Test generation' }], }); await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { bedrock: { foo: 'bar', }, }, }); // Verify that the outgoing request body includes "foo" at its top level. const body = await server.calls[0].requestBodyJson; expect(body).toMatchObject({ foo: 'bar' }); }); it('should include cache token usage in providerMetadata', async () => { prepareJsonResponse({ content: [{ type: 'text', text: 'Testing' }], usage: { inputTokens: 4, outputTokens: 34, totalTokens: 38, cacheReadInputTokens: 2, cacheWriteInputTokens: 3, }, }); const response = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response.providerMetadata).toMatchInlineSnapshot(` { "bedrock": { "usage": { "cacheWriteInputTokens": 3, }, }, } `); expect(response.usage).toMatchInlineSnapshot(` { "cachedInputTokens": 2, "inputTokens": 4, "outputTokens": 34, "totalTokens": 38, } `); }); it('should handle system messages with cache points', async () => { prepareJsonResponse({}); await model.doGenerate({ prompt: [ { role: 'system', content: 'System Prompt', providerOptions: { bedrock: { cachePoint: { type: 'default' } } }, }, { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ], }); expect(await server.calls[0].requestBodyJson).toMatchObject({ system: [{ text: 'System Prompt' }, { cachePoint: { type: 'default' } }], messages: [{ role: 'user', content: [{ text: 'Hello' }] }], }); }); it('should transform reasoningConfig to thinking in additionalModelRequestFields', async () => { prepareJsonResponse({}); await model.doGenerate({ prompt: TEST_PROMPT, maxOutputTokens: 100, providerOptions: { bedrock: { reasoningConfig: { type: 'enabled', budgetTokens: 2000, }, }, }, }); const requestBody = await server.calls[0].requestBodyJson; // Should contain thinking in additionalModelRequestFields expect(requestBody).toMatchObject({ additionalModelRequestFields: { thinking: { type: 'enabled', budget_tokens: 2000, }, }, // Should have adjusted maxOutputTokens (100 + 2000) inferenceConfig: { maxOutputTokens: 2100, }, }); // Should NOT contain reasoningConfig at the top level expect(requestBody).not.toHaveProperty('reasoningConfig'); }); it('should extract reasoning text with signature', async () => { const reasoningText = 'I need to think about this problem carefully...'; const signature = 'abc123signature'; prepareJsonResponse({ content: [ { reasoningContent: { reasoningText: { text: reasoningText, signature, }, }, }, { type: 'text', text: 'The answer is 42.' }, ], }); const result = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(result.content).toMatchInlineSnapshot(` [ { "providerMetadata": { "bedrock": { "signature": "abc123signature", }, }, "text": "I need to think about this problem carefully...", "type": "reasoning", }, { "text": "The answer is 42.", "type": "text", }, ] `); }); it('should extract reasoning text without signature', async () => { const reasoningText = 'I need to think about this problem carefully...'; prepareJsonResponse({ content: [ { reasoningContent: { reasoningText: { text: reasoningText, }, }, }, { type: 'text', text: 'The answer is 42.' }, ], }); const result = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(result.content).toMatchInlineSnapshot(` [ { "text": "I need to think about this problem carefully...", "type": "reasoning", }, { "text": "The answer is 42.", "type": "text", }, ] `); }); it('should extract redacted reasoning', async () => { prepareJsonResponse({ content: [ { reasoningContent: { redactedReasoning: { data: 'redacted-reasoning-data', }, }, }, { type: 'text', text: 'The answer is 42.' }, ], }); const result = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(result.content).toMatchInlineSnapshot(` [ { "providerMetadata": { "bedrock": { "redactedData": "redacted-reasoning-data", }, }, "text": "", "type": "reasoning", }, { "text": "The answer is 42.", "type": "text", }, ] `); }); it('should handle multiple reasoning blocks', async () => { prepareJsonResponse({ content: [ { reasoningContent: { reasoningText: { text: 'First reasoning block', signature: 'sig1', }, }, }, { reasoningContent: { redactedReasoning: { data: 'redacted-data', }, }, }, { type: 'text', text: 'The answer is 42.' }, ], }); const result = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(result.content).toMatchInlineSnapshot(` [ { "providerMetadata": { "bedrock": { "signature": "sig1", }, }, "text": "First reasoning block", "type": "reasoning", }, { "providerMetadata": { "bedrock": { "redactedData": "redacted-data", }, }, "text": "", "type": "reasoning", }, { "text": "The answer is 42.", "type": "text", }, ] `); }); it('should omit toolConfig and filter tool content when conversation has tool calls but no active tools', async () => { prepareJsonResponse({}); const conversationWithToolCalls: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'What is the weather in Toronto?' }], }, { role: 'assistant', content: [ { type: 'tool-call', toolCallId: 'tool-call-1', toolName: 'weather', input: { city: 'Toronto' }, }, ], }, { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'tool-call-1', toolName: 'weather', output: { type: 'text', value: 'The weather in Toronto is 20°C.', }, }, ], }, { role: 'user', content: [{ type: 'text', text: 'Now give me a summary.' }], }, ]; const result = await model.doGenerate({ prompt: conversationWithToolCalls, tools: [], }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.toolConfig).toMatchInlineSnapshot(`undefined`); expect(requestBody.messages).toMatchInlineSnapshot(` [ { "content": [ { "text": "What is the weather in Toronto?", }, { "text": "Now give me a summary.", }, ], "role": "user", }, ] `); expect(result.warnings).toMatchInlineSnapshot(` [ { "details": "Tool calls and results removed from conversation because Bedrock does not support tool content without active tools.", "setting": "toolContent", "type": "unsupported-setting", }, ] `); }); it('should handle JSON response format with schema', async () => { prepareJsonResponse({ content: [ { type: 'tool_use', id: 'json-tool-id', name: 'json', input: { recipe: { name: 'Lasagna', ingredients: ['pasta', 'cheese'] }, }, }, ], }); const result = await model.doGenerate({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'Generate a recipe' }], }, ], responseFormat: { type: 'json', schema: { type: 'object', properties: { recipe: { type: 'object', properties: { name: { type: 'string' }, ingredients: { type: 'array', items: { type: 'string' } }, }, required: ['name', 'ingredients'], }, }, required: ['recipe'], }, }, }); expect(result.content).toMatchInlineSnapshot(`[]`); expect(result.providerMetadata?.bedrock?.isJsonResponseFromTool).toBe(true); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.toolConfig.tools).toHaveLength(1); expect(requestBody.toolConfig.tools[0].toolSpec.name).toBe('json'); expect(requestBody.toolConfig.tools[0].toolSpec.description).toBe( 'Respond with a JSON object.', ); expect(requestBody.toolConfig.toolChoice).toEqual({ tool: { name: 'json' }, }); }); it('should warn when tools are provided with JSON response format', async () => { prepareJsonResponse({ content: [ { type: 'tool_use', id: 'json-tool-id', name: 'json', input: { value: 'test' }, }, ], }); const result = await model.doGenerate({ prompt: [{ role: 'user', content: [{ type: 'text', text: 'Test' }] }], responseFormat: { type: 'json', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], }, }, tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: {} }, }, ], }); expect(result.warnings).toEqual([ { type: 'other', message: 'JSON response format does not support tools. The provided tools are ignored.', }, ]); }); it('should handle unsupported response format types', async () => { prepareJsonResponse({}); const result = await model.doGenerate({ prompt: TEST_PROMPT, responseFormat: { type: 'xml' as any }, }); expect(result.warnings).toEqual([ { type: 'unsupported-setting', setting: 'responseFormat', details: 'Only text and json response formats are supported.', }, ]); }); it('should omit toolConfig when conversation has tool calls but toolChoice is none', async () => { prepareJsonResponse({}); const conversationWithToolCalls: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'What is the weather in Toronto?' }], }, { role: 'assistant', content: [ { type: 'tool-call', toolCallId: 'tool-call-1', toolName: 'weather', input: { city: 'Toronto' }, }, ], }, { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'tool-call-1', toolName: 'weather', output: { type: 'text', value: 'The weather in Toronto is 20°C.', }, }, ], }, { role: 'user', content: [{ type: 'text', text: 'Now give me a summary.' }], }, ]; await model.doGenerate({ prompt: conversationWithToolCalls, tools: [ { type: 'function', name: 'weather', inputSchema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], toolChoice: { type: 'none' }, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.toolConfig).toMatchInlineSnapshot(`undefined`); }); }); --- File: /ai/packages/amazon-bedrock/src/bedrock-chat-language-model.ts --- import { JSONObject, LanguageModelV2, LanguageModelV2CallWarning, LanguageModelV2Content, LanguageModelV2FinishReason, LanguageModelV2Reasoning, LanguageModelV2StreamPart, LanguageModelV2Usage, SharedV2ProviderMetadata, LanguageModelV2FunctionTool, } from '@ai-sdk/provider'; import { FetchFunction, ParseResult, Resolvable, combineHeaders, createJsonErrorResponseHandler, createJsonResponseHandler, parseProviderOptions, postJsonToApi, resolve, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { BEDROCK_STOP_REASONS, BedrockConverseInput, BedrockStopReason, } from './bedrock-api-types'; import { BedrockChatModelId, bedrockProviderOptions, } from './bedrock-chat-options'; import { BedrockErrorSchema } from './bedrock-error'; import { createBedrockEventStreamResponseHandler } from './bedrock-event-stream-response-handler'; import { prepareTools } from './bedrock-prepare-tools'; import { convertToBedrockChatMessages } from './convert-to-bedrock-chat-messages'; import { mapBedrockFinishReason } from './map-bedrock-finish-reason'; type BedrockChatConfig = { baseUrl: () => string; headers: Resolvable<Record<string, string | undefined>>; fetch?: FetchFunction; generateId: () => string; }; export class BedrockChatLanguageModel implements LanguageModelV2 { readonly specificationVersion = 'v2'; readonly provider = 'amazon-bedrock'; constructor( readonly modelId: BedrockChatModelId, private readonly config: BedrockChatConfig, ) {} private async getArgs({ prompt, maxOutputTokens, temperature, topP, topK, frequencyPenalty, presencePenalty, stopSequences, responseFormat, seed, tools, toolChoice, providerOptions, }: Parameters<LanguageModelV2['doGenerate']>[0]): Promise<{ command: BedrockConverseInput; warnings: LanguageModelV2CallWarning[]; usesJsonResponseTool: boolean; betas: Set<string>; }> { // Parse provider options const bedrockOptions = (await parseProviderOptions({ provider: 'bedrock', providerOptions, schema: bedrockProviderOptions, })) ?? {}; const warnings: LanguageModelV2CallWarning[] = []; if (frequencyPenalty != null) { warnings.push({ type: 'unsupported-setting', setting: 'frequencyPenalty', }); } if (presencePenalty != null) { warnings.push({ type: 'unsupported-setting', setting: 'presencePenalty', }); } if (seed != null) { warnings.push({ type: 'unsupported-setting', setting: 'seed', }); } if ( responseFormat != null && responseFormat.type !== 'text' && responseFormat.type !== 'json' ) { warnings.push({ type: 'unsupported-setting', setting: 'responseFormat', details: 'Only text and json response formats are supported.', }); } if (tools != null && responseFormat?.type === 'json') { if (tools.length > 0) { warnings.push({ type: 'other', message: 'JSON response format does not support tools. ' + 'The provided tools are ignored.', }); } } const jsonResponseTool: LanguageModelV2FunctionTool | undefined = responseFormat?.type === 'json' && responseFormat.schema != null ? { type: 'function', name: 'json', description: 'Respond with a JSON object.', inputSchema: responseFormat.schema, } : undefined; const { toolConfig, additionalTools, toolWarnings, betas } = prepareTools({ tools: jsonResponseTool ? [jsonResponseTool, ...(tools ?? [])] : tools, toolChoice: jsonResponseTool != null ? { type: 'tool', toolName: jsonResponseTool.name } : toolChoice, modelId: this.modelId, }); warnings.push(...toolWarnings); if (additionalTools) { bedrockOptions.additionalModelRequestFields = { ...bedrockOptions.additionalModelRequestFields, ...additionalTools, }; } const isThinking = bedrockOptions.reasoningConfig?.type === 'enabled'; const thinkingBudget = bedrockOptions.reasoningConfig?.budgetTokens; const inferenceConfig = { ...(maxOutputTokens != null && { maxOutputTokens }), ...(temperature != null && { temperature }), ...(topP != null && { topP }), ...(topK != null && { topK }), ...(stopSequences != null && { stopSequences }), }; // Adjust maxOutputTokens if thinking is enabled if (isThinking && thinkingBudget != null) { if (inferenceConfig.maxOutputTokens != null) { inferenceConfig.maxOutputTokens += thinkingBudget; } else { inferenceConfig.maxOutputTokens = thinkingBudget + 4096; // Default + thinking budget maxOutputTokens = 4096, TODO update default in v5 } // Add them to additional model request fields // Add thinking config to additionalModelRequestFields bedrockOptions.additionalModelRequestFields = { ...bedrockOptions.additionalModelRequestFields, thinking: { type: bedrockOptions.reasoningConfig?.type, budget_tokens: thinkingBudget, }, }; } // Remove temperature if thinking is enabled if (isThinking && inferenceConfig.temperature != null) { delete inferenceConfig.temperature; warnings.push({ type: 'unsupported-setting', setting: 'temperature', details: 'temperature is not supported when thinking is enabled', }); } // Remove topP if thinking is enabled if (isThinking && inferenceConfig.topP != null) { delete inferenceConfig.topP; warnings.push({ type: 'unsupported-setting', setting: 'topP', details: 'topP is not supported when thinking is enabled', }); } if (isThinking && inferenceConfig.topK != null) { delete inferenceConfig.topK; warnings.push({ type: 'unsupported-setting', setting: 'topK', details: 'topK is not supported when thinking is enabled', }); } // Filter tool content from prompt when no tools are available const hasAnyTools = (toolConfig.tools?.length ?? 0) > 0 || additionalTools; let filteredPrompt = prompt; if (!hasAnyTools) { const hasToolContent = prompt.some( message => 'content' in message && Array.isArray(message.content) && message.content.some( part => part.type === 'tool-call' || part.type === 'tool-result', ), ); if (hasToolContent) { filteredPrompt = prompt .map(message => message.role === 'system' ? message : { ...message, content: message.content.filter( part => part.type !== 'tool-call' && part.type !== 'tool-result', ), }, ) .filter( message => message.role === 'system' || message.content.length > 0, ) as typeof prompt; warnings.push({ type: 'unsupported-setting', setting: 'toolContent', details: 'Tool calls and results removed from conversation because Bedrock does not support tool content without active tools.', }); } } const { system, messages } = await convertToBedrockChatMessages(filteredPrompt); // Filter out reasoningConfig from providerOptions.bedrock to prevent sending it to Bedrock API const { reasoningConfig: _, ...filteredBedrockOptions } = providerOptions?.bedrock || {}; return { command: { system, messages, additionalModelRequestFields: bedrockOptions.additionalModelRequestFields, ...(Object.keys(inferenceConfig).length > 0 && { inferenceConfig, }), ...filteredBedrockOptions, ...(toolConfig.tools !== undefined && toolConfig.tools.length > 0 ? { toolConfig } : {}), }, warnings, usesJsonResponseTool: jsonResponseTool != null, betas, }; } readonly supportedUrls: Record<string, RegExp[]> = { // no supported urls for bedrock }; private async getHeaders({ betas, headers, }: { betas: Set<string>; headers: Record<string, string | undefined> | undefined; }) { return combineHeaders( await resolve(this.config.headers), betas.size > 0 ? { 'anthropic-beta': Array.from(betas).join(',') } : {}, headers, ); } async doGenerate( options: Parameters<LanguageModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>> { const { command: args, warnings, usesJsonResponseTool, betas, } = await this.getArgs(options); const url = `${this.getUrl(this.modelId)}/converse`; const { value: response, responseHeaders } = await postJsonToApi({ url, headers: await this.getHeaders({ betas, headers: options.headers }), body: args, failedResponseHandler: createJsonErrorResponseHandler({ errorSchema: BedrockErrorSchema, errorToMessage: error => `${error.message ?? 'Unknown error'}`, }), successfulResponseHandler: createJsonResponseHandler( BedrockResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const content: Array<LanguageModelV2Content> = []; // map response content to content array for (const part of response.output.message.content) { // text if (part.text) { // when a json response tool is used, the tool call is returned as text, // so we ignore the text content: if (!usesJsonResponseTool) { content.push({ type: 'text', text: part.text }); } } // reasoning if (part.reasoningContent) { if ('reasoningText' in part.reasoningContent) { const reasoning: LanguageModelV2Reasoning = { type: 'reasoning', text: part.reasoningContent.reasoningText.text, }; if (part.reasoningContent.reasoningText.signature) { reasoning.providerMetadata = { bedrock: { signature: part.reasoningContent.reasoningText.signature, } satisfies BedrockReasoningMetadata, }; } content.push(reasoning); } else if ('redactedReasoning' in part.reasoningContent) { content.push({ type: 'reasoning', text: '', providerMetadata: { bedrock: { redactedData: part.reasoningContent.redactedReasoning.data ?? '', } satisfies BedrockReasoningMetadata, }, }); } } // tool calls if (part.toolUse) { content.push( // when a json response tool is used, the tool call becomes the text: usesJsonResponseTool ? { type: 'text', text: JSON.stringify(part.toolUse.input), } : { type: 'tool-call' as const, toolCallId: part.toolUse?.toolUseId ?? this.config.generateId(), toolName: part.toolUse?.name ?? `tool-${this.config.generateId()}`, input: JSON.stringify(part.toolUse?.input ?? ''), }, ); } } // provider metadata: const providerMetadata = response.trace || response.usage || usesJsonResponseTool ? { bedrock: { ...(response.trace && typeof response.trace === 'object' ? { trace: response.trace as JSONObject } : {}), ...(response.usage?.cacheWriteInputTokens != null && { usage: { cacheWriteInputTokens: response.usage.cacheWriteInputTokens, }, }), ...(usesJsonResponseTool && { isJsonResponseFromTool: true }), }, } : undefined; return { content, finishReason: mapBedrockFinishReason( response.stopReason as BedrockStopReason, ), usage: { inputTokens: response.usage?.inputTokens, outputTokens: response.usage?.outputTokens, totalTokens: response.usage?.inputTokens + response.usage?.outputTokens, cachedInputTokens: response.usage?.cacheReadInputTokens ?? undefined, }, response: { // TODO add id, timestamp, etc headers: responseHeaders, }, warnings, ...(providerMetadata && { providerMetadata }), }; } async doStream( options: Parameters<LanguageModelV2['doStream']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>> { const { command: args, warnings, usesJsonResponseTool, betas, } = await this.getArgs(options); const url = `${this.getUrl(this.modelId)}/converse-stream`; const { value: response, responseHeaders } = await postJsonToApi({ url, headers: await this.getHeaders({ betas, headers: options.headers }), body: args, failedResponseHandler: createJsonErrorResponseHandler({ errorSchema: BedrockErrorSchema, errorToMessage: error => `${error.type}: ${error.message}`, }), successfulResponseHandler: createBedrockEventStreamResponseHandler(BedrockStreamSchema), abortSignal: options.abortSignal, fetch: this.config.fetch, }); let finishReason: LanguageModelV2FinishReason = 'unknown'; const usage: LanguageModelV2Usage = { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, }; let providerMetadata: SharedV2ProviderMetadata | undefined = undefined; const contentBlocks: Record< number, | { type: 'tool-call'; toolCallId: string; toolName: string; jsonText: string; } | { type: 'text' | 'reasoning' } > = {}; return { stream: response.pipeThrough( new TransformStream< ParseResult<z.infer<typeof BedrockStreamSchema>>, LanguageModelV2StreamPart >({ start(controller) { controller.enqueue({ type: 'stream-start', warnings }); }, transform(chunk, controller) { function enqueueError(bedrockError: Record<string, any>) { finishReason = 'error'; controller.enqueue({ type: 'error', error: bedrockError }); } // Emit raw chunk if requested (before anything else) if (options.includeRawChunks) { controller.enqueue({ type: 'raw', rawValue: chunk.rawValue }); } // handle failed chunk parsing / validation: if (!chunk.success) { enqueueError(chunk.error); return; } const value = chunk.value; // handle errors: if (value.internalServerException) { enqueueError(value.internalServerException); return; } if (value.modelStreamErrorException) { enqueueError(value.modelStreamErrorException); return; } if (value.throttlingException) { enqueueError(value.throttlingException); return; } if (value.validationException) { enqueueError(value.validationException); return; } if (value.messageStop) { finishReason = mapBedrockFinishReason( value.messageStop.stopReason as BedrockStopReason, ); } if (value.metadata) { usage.inputTokens = value.metadata.usage?.inputTokens ?? usage.inputTokens; usage.outputTokens = value.metadata.usage?.outputTokens ?? usage.outputTokens; usage.totalTokens = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0); usage.cachedInputTokens = value.metadata.usage?.cacheReadInputTokens ?? usage.cachedInputTokens; const cacheUsage = value.metadata.usage?.cacheWriteInputTokens != null ? { usage: { cacheWriteInputTokens: value.metadata.usage.cacheWriteInputTokens, }, } : undefined; const trace = value.metadata.trace ? { trace: value.metadata.trace as JSONObject, } : undefined; if (cacheUsage || trace || usesJsonResponseTool) { providerMetadata = { bedrock: { ...cacheUsage, ...trace, ...(usesJsonResponseTool && { isJsonResponseFromTool: true, }), }, }; } } if ( value.contentBlockStart?.contentBlockIndex != null && !value.contentBlockStart?.start?.toolUse ) { const blockIndex = value.contentBlockStart.contentBlockIndex; contentBlocks[blockIndex] = { type: 'text' }; controller.enqueue({ type: 'text-start', id: String(blockIndex), }); } if ( value.contentBlockDelta?.delta && 'text' in value.contentBlockDelta.delta && value.contentBlockDelta.delta.text ) { const blockIndex = value.contentBlockDelta.contentBlockIndex || 0; if (contentBlocks[blockIndex] == null) { contentBlocks[blockIndex] = { type: 'text' }; // when a json response tool is used, we don't emit text events if (!usesJsonResponseTool) { controller.enqueue({ type: 'text-start', id: String(blockIndex), }); } } // when a json response tool is used, we don't emit text events if (!usesJsonResponseTool) { controller.enqueue({ type: 'text-delta', id: String(blockIndex), delta: value.contentBlockDelta.delta.text, }); } } if (value.contentBlockStop?.contentBlockIndex != null) { const blockIndex = value.contentBlockStop.contentBlockIndex; const contentBlock = contentBlocks[blockIndex]; if (contentBlock != null) { if (contentBlock.type === 'reasoning') { controller.enqueue({ type: 'reasoning-end', id: String(blockIndex), }); } else if (contentBlock.type === 'text') { // when a json response tool is used, we don't emit text events if (!usesJsonResponseTool) { controller.enqueue({ type: 'text-end', id: String(blockIndex), }); } } else if (contentBlock.type === 'tool-call') { if (usesJsonResponseTool) { // when a json response tool is used, emit the tool input as text controller.enqueue({ type: 'text-start', id: String(blockIndex), }); controller.enqueue({ type: 'text-delta', id: String(blockIndex), delta: contentBlock.jsonText, }); controller.enqueue({ type: 'text-end', id: String(blockIndex), }); } else { controller.enqueue({ type: 'tool-input-end', id: contentBlock.toolCallId, }); controller.enqueue({ type: 'tool-call', toolCallId: contentBlock.toolCallId, toolName: contentBlock.toolName, input: contentBlock.jsonText, }); } } delete contentBlocks[blockIndex]; } } if ( value.contentBlockDelta?.delta && 'reasoningContent' in value.contentBlockDelta.delta && value.contentBlockDelta.delta.reasoningContent ) { const blockIndex = value.contentBlockDelta.contentBlockIndex || 0; const reasoningContent = value.contentBlockDelta.delta.reasoningContent; if ('text' in reasoningContent && reasoningContent.text) { if (contentBlocks[blockIndex] == null) { contentBlocks[blockIndex] = { type: 'reasoning' }; controller.enqueue({ type: 'reasoning-start', id: String(blockIndex), }); } controller.enqueue({ type: 'reasoning-delta', id: String(blockIndex), delta: reasoningContent.text, }); } else if ( 'signature' in reasoningContent && reasoningContent.signature ) { controller.enqueue({ type: 'reasoning-delta', id: String(blockIndex), delta: '', providerMetadata: { bedrock: { signature: reasoningContent.signature, } satisfies BedrockReasoningMetadata, }, }); } else if ('data' in reasoningContent && reasoningContent.data) { controller.enqueue({ type: 'reasoning-delta', id: String(blockIndex), delta: '', providerMetadata: { bedrock: { redactedData: reasoningContent.data, } satisfies BedrockReasoningMetadata, }, }); } } const contentBlockStart = value.contentBlockStart; if (contentBlockStart?.start?.toolUse != null) { const toolUse = contentBlockStart.start.toolUse; const blockIndex = contentBlockStart.contentBlockIndex!; contentBlocks[blockIndex] = { type: 'tool-call', toolCallId: toolUse.toolUseId!, toolName: toolUse.name!, jsonText: '', }; // when a json response tool is used, we don't emit tool events if (!usesJsonResponseTool) { controller.enqueue({ type: 'tool-input-start', id: toolUse.toolUseId!, toolName: toolUse.name!, }); } } const contentBlockDelta = value.contentBlockDelta; if ( contentBlockDelta?.delta && 'toolUse' in contentBlockDelta.delta && contentBlockDelta.delta.toolUse ) { const blockIndex = contentBlockDelta.contentBlockIndex!; const contentBlock = contentBlocks[blockIndex]; if (contentBlock?.type === 'tool-call') { const delta = contentBlockDelta.delta.toolUse.input ?? ''; // when a json response tool is used, we don't emit tool events if (!usesJsonResponseTool) { controller.enqueue({ type: 'tool-input-delta', id: contentBlock.toolCallId, delta: delta, }); } contentBlock.jsonText += delta; } } }, flush(controller) { controller.enqueue({ type: 'finish', finishReason, usage, ...(providerMetadata && { providerMetadata }), }); }, }), ), // TODO request? response: { headers: responseHeaders }, }; } private getUrl(modelId: string) { const encodedModelId = encodeURIComponent(modelId); return `${this.config.baseUrl()}/model/${encodedModelId}`; } } const BedrockStopReasonSchema = z.union([ z.enum(BEDROCK_STOP_REASONS), z.string(), ]); const BedrockToolUseSchema = z.object({ toolUseId: z.string(), name: z.string(), input: z.unknown(), }); const BedrockReasoningTextSchema = z.object({ signature: z.string().nullish(), text: z.string(), }); const BedrockRedactedReasoningSchema = z.object({ data: z.string(), }); // limited version of the schema, focused on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const BedrockResponseSchema = z.object({ metrics: z .object({ latencyMs: z.number(), }) .nullish(), output: z.object({ message: z.object({ content: z.array( z.object({ text: z.string().nullish(), toolUse: BedrockToolUseSchema.nullish(), reasoningContent: z .union([ z.object({ reasoningText: BedrockReasoningTextSchema, }), z.object({ redactedReasoning: BedrockRedactedReasoningSchema, }), ]) .nullish(), }), ), role: z.string(), }), }), stopReason: BedrockStopReasonSchema, trace: z.unknown().nullish(), usage: z.object({ inputTokens: z.number(), outputTokens: z.number(), totalTokens: z.number(), cacheReadInputTokens: z.number().nullish(), cacheWriteInputTokens: z.number().nullish(), }), }); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const BedrockStreamSchema = z.object({ contentBlockDelta: z .object({ contentBlockIndex: z.number(), delta: z .union([ z.object({ text: z.string() }), z.object({ toolUse: z.object({ input: z.string() }) }), z.object({ reasoningContent: z.object({ text: z.string() }), }), z.object({ reasoningContent: z.object({ signature: z.string(), }), }), z.object({ reasoningContent: z.object({ data: z.string() }), }), ]) .nullish(), }) .nullish(), contentBlockStart: z .object({ contentBlockIndex: z.number(), start: z .object({ toolUse: BedrockToolUseSchema.nullish(), }) .nullish(), }) .nullish(), contentBlockStop: z .object({ contentBlockIndex: z.number(), }) .nullish(), internalServerException: z.record(z.string(), z.unknown()).nullish(), messageStop: z .object({ additionalModelResponseFields: z .record(z.string(), z.unknown()) .nullish(), stopReason: BedrockStopReasonSchema, }) .nullish(), metadata: z .object({ trace: z.unknown().nullish(), usage: z .object({ cacheReadInputTokens: z.number().nullish(), cacheWriteInputTokens: z.number().nullish(), inputTokens: z.number(), outputTokens: z.number(), }) .nullish(), }) .nullish(), modelStreamErrorException: z.record(z.string(), z.unknown()).nullish(), throttlingException: z.record(z.string(), z.unknown()).nullish(), validationException: z.record(z.string(), z.unknown()).nullish(), }); export const bedrockReasoningMetadataSchema = z.object({ signature: z.string().optional(), redactedData: z.string().optional(), }); export type BedrockReasoningMetadata = z.infer< typeof bedrockReasoningMetadataSchema >; --- File: /ai/packages/amazon-bedrock/src/bedrock-chat-options.ts --- import { z } from 'zod/v4'; // https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html export type BedrockChatModelId = | 'amazon.titan-tg1-large' | 'amazon.titan-text-express-v1' | 'anthropic.claude-v2' | 'anthropic.claude-v2:1' | 'anthropic.claude-instant-v1' | 'anthropic.claude-sonnet-4-20250514-v1:0' | 'anthropic.claude-opus-4-20250514-v1:0' | 'anthropic.claude-opus-4-1-20250805-v1:0' | 'anthropic.claude-3-7-sonnet-20250219-v1:0' | 'anthropic.claude-3-5-sonnet-20240620-v1:0' | 'anthropic.claude-3-5-sonnet-20241022-v2:0' | 'anthropic.claude-3-5-haiku-20241022-v1:0' | 'anthropic.claude-3-sonnet-20240229-v1:0' | 'anthropic.claude-3-haiku-20240307-v1:0' | 'anthropic.claude-3-opus-20240229-v1:0' | 'cohere.command-text-v14' | 'cohere.command-light-text-v14' | 'cohere.command-r-v1:0' | 'cohere.command-r-plus-v1:0' | 'meta.llama3-70b-instruct-v1:0' | 'meta.llama3-8b-instruct-v1:0' | 'meta.llama3-1-405b-instruct-v1:0' | 'meta.llama3-1-70b-instruct-v1:0' | 'meta.llama3-1-8b-instruct-v1:0' | 'meta.llama3-2-11b-instruct-v1:0' | 'meta.llama3-2-1b-instruct-v1:0' | 'meta.llama3-2-3b-instruct-v1:0' | 'meta.llama3-2-90b-instruct-v1:0' | 'mistral.mistral-7b-instruct-v0:2' | 'mistral.mixtral-8x7b-instruct-v0:1' | 'mistral.mistral-large-2402-v1:0' | 'mistral.mistral-small-2402-v1:0' | 'openai.gpt-oss-120b-1:0' | 'openai.gpt-oss-20b-1:0' | 'amazon.titan-text-express-v1' | 'amazon.titan-text-lite-v1' | 'us.amazon.nova-premier-v1:0' | 'us.amazon.nova-pro-v1:0' | 'us.amazon.nova-micro-v1:0' | 'us.amazon.nova-lite-v1:0' | 'us.anthropic.claude-3-sonnet-20240229-v1:0' | 'us.anthropic.claude-3-opus-20240229-v1:0' | 'us.anthropic.claude-3-haiku-20240307-v1:0' | 'us.anthropic.claude-3-5-sonnet-20240620-v1:0' | 'us.anthropic.claude-3-5-haiku-20241022-v1:0' | 'us.anthropic.claude-3-5-sonnet-20241022-v2:0' | 'us.anthropic.claude-3-7-sonnet-20250219-v1:0' | 'us.anthropic.claude-sonnet-4-20250514-v1:0' | 'us.anthropic.claude-opus-4-20250514-v1:0' | 'us.anthropic.claude-opus-4-1-20250805-v1:0' | 'us.meta.llama3-2-11b-instruct-v1:0' | 'us.meta.llama3-2-3b-instruct-v1:0' | 'us.meta.llama3-2-90b-instruct-v1:0' | 'us.meta.llama3-2-1b-instruct-v1:0' | 'us.meta.llama3-1-8b-instruct-v1:0' | 'us.meta.llama3-1-70b-instruct-v1:0' | 'us.meta.llama3-3-70b-instruct-v1:0' | 'us.deepseek.r1-v1:0' | 'us.mistral.pixtral-large-2502-v1:0' | 'us.meta.llama4-scout-17b-instruct-v1:0' | 'us.meta.llama4-maverick-17b-instruct-v1:0' | (string & {}); export const bedrockProviderOptions = z.object({ /** * Additional inference parameters that the model supports, * beyond the base set of inference parameters that Converse * supports in the inferenceConfig field */ additionalModelRequestFields: z.record(z.string(), z.any()).optional(), reasoningConfig: z .object({ type: z.union([z.literal('enabled'), z.literal('disabled')]).optional(), budgetTokens: z.number().optional(), }) .optional(), }); export type BedrockProviderOptions = z.infer<typeof bedrockProviderOptions>; --- File: /ai/packages/amazon-bedrock/src/bedrock-embedding-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { BedrockEmbeddingModel } from './bedrock-embedding-model'; import { injectFetchHeaders } from './inject-fetch-headers'; const mockEmbeddings = [ [-0.09, 0.05, -0.02, 0.01, 0.04], [-0.08, 0.06, -0.03, 0.02, 0.03], ]; const fakeFetchWithAuth = injectFetchHeaders({ 'x-amz-auth': 'test-auth' }); const testValues = ['sunny day at the beach', 'rainy day in the city']; const embedUrl = `https://bedrock-runtime.us-east-1.amazonaws.com/model/${encodeURIComponent( 'amazon.titan-embed-text-v2:0', )}/invoke`; describe('doEmbed', () => { const mockConfigHeaders = { 'config-header': 'config-value', 'shared-header': 'config-shared', }; const server = createTestServer({ [embedUrl]: { response: { type: 'binary', headers: { 'content-type': 'application/json', }, body: Buffer.from( JSON.stringify({ embedding: mockEmbeddings[0], inputTextTokenCount: 8, }), ), }, }, }); const model = new BedrockEmbeddingModel('amazon.titan-embed-text-v2:0', { baseUrl: () => 'https://bedrock-runtime.us-east-1.amazonaws.com', headers: mockConfigHeaders, fetch: fakeFetchWithAuth, }); let callCount = 0; beforeEach(() => { callCount = 0; server.urls[embedUrl].response = { type: 'binary', headers: { 'content-type': 'application/json', }, body: Buffer.from( JSON.stringify({ embedding: mockEmbeddings[0], inputTextTokenCount: 8, }), ), }; }); it('should handle single input value and return embeddings', async () => { const { embeddings } = await model.doEmbed({ values: [testValues[0]], }); expect(embeddings.length).toBe(1); expect(embeddings[0]).toStrictEqual(mockEmbeddings[0]); const body = await server.calls[0].requestBodyJson; expect(body).toEqual({ inputText: testValues[0], dimensions: undefined, normalize: undefined, }); }); it('should handle single input value and extract usage', async () => { const { usage } = await model.doEmbed({ values: [testValues[0]], }); expect(usage?.tokens).toStrictEqual(8); }); it('should properly combine headers from all sources', async () => { const optionsHeaders = { 'options-header': 'options-value', 'shared-header': 'options-shared', }; const modelWithHeaders = new BedrockEmbeddingModel( 'amazon.titan-embed-text-v2:0', { baseUrl: () => 'https://bedrock-runtime.us-east-1.amazonaws.com', headers: { 'model-header': 'model-value', 'shared-header': 'model-shared', }, fetch: injectFetchHeaders({ 'signed-header': 'signed-value', authorization: 'AWS4-HMAC-SHA256...', }), }, ); await modelWithHeaders.doEmbed({ values: [testValues[0]], headers: optionsHeaders, }); const requestHeaders = server.calls[0].requestHeaders; expect(requestHeaders['options-header']).toBe('options-value'); expect(requestHeaders['model-header']).toBe('model-value'); expect(requestHeaders['signed-header']).toBe('signed-value'); expect(requestHeaders['authorization']).toBe('AWS4-HMAC-SHA256...'); expect(requestHeaders['shared-header']).toBe('options-shared'); }); it('should work with partial headers', async () => { const modelWithPartialHeaders = new BedrockEmbeddingModel( 'amazon.titan-embed-text-v2:0', { baseUrl: () => 'https://bedrock-runtime.us-east-1.amazonaws.com', headers: { 'model-header': 'model-value', }, fetch: injectFetchHeaders({ 'signed-header': 'signed-value', authorization: 'AWS4-HMAC-SHA256...', }), }, ); await modelWithPartialHeaders.doEmbed({ values: [testValues[0]], }); const requestHeaders = server.calls[0].requestHeaders; expect(requestHeaders['model-header']).toBe('model-value'); expect(requestHeaders['signed-header']).toBe('signed-value'); expect(requestHeaders['authorization']).toBe('AWS4-HMAC-SHA256...'); }); }); --- File: /ai/packages/amazon-bedrock/src/bedrock-embedding-model.ts --- import { EmbeddingModelV2, TooManyEmbeddingValuesForCallError, } from '@ai-sdk/provider'; import { FetchFunction, Resolvable, combineHeaders, createJsonErrorResponseHandler, createJsonResponseHandler, parseProviderOptions, postJsonToApi, resolve, } from '@ai-sdk/provider-utils'; import { BedrockEmbeddingModelId, bedrockEmbeddingProviderOptions, } from './bedrock-embedding-options'; import { BedrockErrorSchema } from './bedrock-error'; import { z } from 'zod/v4'; type BedrockEmbeddingConfig = { baseUrl: () => string; headers: Resolvable<Record<string, string | undefined>>; fetch?: FetchFunction; }; type DoEmbedResponse = Awaited<ReturnType<EmbeddingModelV2<string>['doEmbed']>>; export class BedrockEmbeddingModel implements EmbeddingModelV2<string> { readonly specificationVersion = 'v2'; readonly provider = 'amazon-bedrock'; readonly maxEmbeddingsPerCall = 1; readonly supportsParallelCalls = true; constructor( readonly modelId: BedrockEmbeddingModelId, private readonly config: BedrockEmbeddingConfig, ) {} private getUrl(modelId: string): string { const encodedModelId = encodeURIComponent(modelId); return `${this.config.baseUrl()}/model/${encodedModelId}/invoke`; } async doEmbed({ values, headers, abortSignal, providerOptions, }: Parameters< EmbeddingModelV2<string>['doEmbed'] >[0]): Promise<DoEmbedResponse> { if (values.length > this.maxEmbeddingsPerCall) { throw new TooManyEmbeddingValuesForCallError({ provider: this.provider, modelId: this.modelId, maxEmbeddingsPerCall: this.maxEmbeddingsPerCall, values, }); } // Parse provider options const bedrockOptions = (await parseProviderOptions({ provider: 'bedrock', providerOptions, schema: bedrockEmbeddingProviderOptions, })) ?? {}; // https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_InvokeModel.html const args = { inputText: values[0], dimensions: bedrockOptions.dimensions, normalize: bedrockOptions.normalize, }; const url = this.getUrl(this.modelId); const { value: response } = await postJsonToApi({ url, headers: await resolve( combineHeaders(await resolve(this.config.headers), headers), ), body: args, failedResponseHandler: createJsonErrorResponseHandler({ errorSchema: BedrockErrorSchema, errorToMessage: error => `${error.type}: ${error.message}`, }), successfulResponseHandler: createJsonResponseHandler( BedrockEmbeddingResponseSchema, ), fetch: this.config.fetch, abortSignal, }); return { embeddings: [response.embedding], usage: { tokens: response.inputTextTokenCount }, }; } } const BedrockEmbeddingResponseSchema = z.object({ embedding: z.array(z.number()), inputTextTokenCount: z.number(), }); --- File: /ai/packages/amazon-bedrock/src/bedrock-embedding-options.ts --- import { z } from 'zod/v4'; export type BedrockEmbeddingModelId = | 'amazon.titan-embed-text-v1' | 'amazon.titan-embed-text-v2:0' | 'cohere.embed-english-v3' | 'cohere.embed-multilingual-v3' | (string & {}); export const bedrockEmbeddingProviderOptions = z.object({ /** The number of dimensions the resulting output embeddings should have (defaults to 1024). Only supported in amazon.titan-embed-text-v2:0. */ dimensions: z .union([z.literal(1024), z.literal(512), z.literal(256)]) .optional(), /** Flag indicating whether or not to normalize the output embeddings. Defaults to true Only supported in amazon.titan-embed-text-v2:0. */ normalize: z.boolean().optional(), }); --- File: /ai/packages/amazon-bedrock/src/bedrock-error.ts --- import { z } from 'zod/v4'; export const BedrockErrorSchema = z.object({ message: z.string(), type: z.string().nullish(), }); --- File: /ai/packages/amazon-bedrock/src/bedrock-event-stream-response-handler.test.ts --- import { EmptyResponseBodyError } from '@ai-sdk/provider'; import { createBedrockEventStreamResponseHandler } from './bedrock-event-stream-response-handler'; import { EventStreamCodec } from '@smithy/eventstream-codec'; import { z } from 'zod/v4'; import { describe, it, expect, vi, MockInstance } from 'vitest'; // Helper that constructs a properly framed message. // The first 4 bytes will contain the frame total length (big-endian). const createFrame = (payload: Uint8Array): Uint8Array => { const totalLength = 4 + payload.length; const frame = new Uint8Array(totalLength); new DataView(frame.buffer).setUint32(0, totalLength, false); frame.set(payload, 4); return frame; }; // Mock EventStreamCodec vi.mock('@smithy/eventstream-codec', () => ({ EventStreamCodec: vi.fn(), })); describe('createEventSourceResponseHandler', () => { // Define a sample schema for testing const testSchema = z.object({ chunk: z.object({ content: z.string(), }), }); it('throws EmptyResponseBodyError when response body is null', async () => { const response = new Response(null); const handler = createBedrockEventStreamResponseHandler(testSchema); await expect( handler({ response, url: 'test-url', requestBodyValues: {}, }), ).rejects.toThrow(EmptyResponseBodyError); }); it('successfully processes valid event stream data', async () => { // Prepare the message we wish to simulate. // Our decoded message will contain headers and a body that is valid JSON. const message = { headers: { ':message-type': { value: 'event' }, ':event-type': { value: 'chunk' }, }, body: new TextEncoder().encode( JSON.stringify({ content: 'test message' }), ), }; // Create a frame that properly encapsulates the message. const dummyPayload = new Uint8Array([1, 2, 3, 4]); // arbitrary payload that makes the length check pass const frame = createFrame(dummyPayload); const mockDecode = vi.fn().mockReturnValue(message); (EventStreamCodec as unknown as MockInstance).mockImplementation(() => ({ decode: mockDecode, })); // Create a stream that enqueues the complete frame. const stream = new ReadableStream({ start(controller) { controller.enqueue(frame); controller.close(); }, }); const response = new Response(stream); const handler = createBedrockEventStreamResponseHandler(testSchema); const result = await handler({ response, url: 'test-url', requestBodyValues: {}, }); const reader = result.value.getReader(); const { done, value } = await reader.read(); expect(done).toBe(false); expect(value).toEqual({ success: true, value: { chunk: { content: 'test message' } }, rawValue: { chunk: { content: 'test message' } }, }); }); it('handles invalid JSON data', async () => { // Our mock decode returns a body that is not valid JSON. const message = { headers: { ':message-type': { value: 'event' }, ':event-type': { value: 'chunk' }, }, body: new TextEncoder().encode('invalid json'), }; const dummyPayload = new Uint8Array([5, 6, 7, 8]); const frame = createFrame(dummyPayload); const mockDecode = vi.fn().mockReturnValue(message); (EventStreamCodec as unknown as MockInstance).mockImplementation(() => ({ decode: mockDecode, })); const stream = new ReadableStream({ start(controller) { controller.enqueue(frame); controller.close(); }, }); const response = new Response(stream); const handler = createBedrockEventStreamResponseHandler(testSchema); const result = await handler({ response, url: 'test-url', requestBodyValues: {}, }); const reader = result.value.getReader(); const { done, value } = await reader.read(); expect(done).toBe(false); // When JSON is invalid, safeParseJSON returns a result with success: false. expect(value?.success).toBe(false); expect((value as { success: false; error: Error }).error).toBeDefined(); }); it('handles schema validation failures', async () => { // The decoded message returns valid JSON but that does not meet our schema. const message = { headers: { ':message-type': { value: 'event' }, ':event-type': { value: 'chunk' }, }, body: new TextEncoder().encode(JSON.stringify({ invalid: 'data' })), }; const dummyPayload = new Uint8Array([9, 10, 11, 12]); const frame = createFrame(dummyPayload); const mockDecode = vi.fn().mockReturnValue(message); (EventStreamCodec as unknown as MockInstance).mockImplementation(() => ({ decode: mockDecode, })); const stream = new ReadableStream({ start(controller) { controller.enqueue(frame); controller.close(); }, }); const response = new Response(stream); const handler = createBedrockEventStreamResponseHandler(testSchema); const result = await handler({ response, url: 'test-url', requestBodyValues: {}, }); const reader = result.value.getReader(); const { done, value } = await reader.read(); expect(done).toBe(false); // The schema does not match so safeParseJSON with the schema should yield success: false. expect(value?.success).toBe(false); expect((value as { success: false; error: Error }).error).toBeDefined(); }); it('handles partial messages correctly', async () => { // In this test, we simulate a partial message followed by a complete one. // The first invocation of decode will throw an error (simulated incomplete message), // and the subsequent invocation returns a valid event. const message = { headers: { ':message-type': { value: 'event' }, ':event-type': { value: 'chunk' }, }, body: new TextEncoder().encode( JSON.stringify({ content: 'complete message' }), ), }; const dummyPayload1 = new Uint8Array([13, 14]); // too short, part of a frame const frame1 = createFrame(dummyPayload1); const dummyPayload2 = new Uint8Array([15, 16, 17, 18]); const frame2 = createFrame(dummyPayload2); const mockDecode = vi .fn() .mockImplementationOnce(() => { throw new Error('Incomplete data'); }) .mockReturnValue(message); (EventStreamCodec as unknown as MockInstance).mockImplementation(() => ({ decode: mockDecode, })); const stream = new ReadableStream({ start(controller) { // Send first, incomplete frame (decode will throw error). controller.enqueue(frame1); // Then send a proper frame. controller.enqueue(frame2); controller.close(); }, }); const response = new Response(stream); const handler = createBedrockEventStreamResponseHandler(testSchema); const result = await handler({ response, url: 'test-url', requestBodyValues: {}, }); const reader = result.value.getReader(); const { done, value } = await reader.read(); expect(done).toBe(false); expect(value).toEqual({ success: true, value: { chunk: { content: 'complete message' } }, rawValue: { chunk: { content: 'complete message' } }, }); }); }); --- File: /ai/packages/amazon-bedrock/src/bedrock-event-stream-response-handler.ts --- import { EmptyResponseBodyError } from '@ai-sdk/provider'; import { ParseResult, safeParseJSON, extractResponseHeaders, ResponseHandler, safeValidateTypes, } from '@ai-sdk/provider-utils'; import { EventStreamCodec } from '@smithy/eventstream-codec'; import { toUtf8, fromUtf8 } from '@smithy/util-utf8'; import { ZodType } from 'zod/v4'; // https://docs.aws.amazon.com/lexv2/latest/dg/event-stream-encoding.html export const createBedrockEventStreamResponseHandler = <T>( chunkSchema: ZodType<T, any>, ): ResponseHandler<ReadableStream<ParseResult<T>>> => async ({ response }: { response: Response }) => { const responseHeaders = extractResponseHeaders(response); if (response.body == null) { throw new EmptyResponseBodyError({}); } const codec = new EventStreamCodec(toUtf8, fromUtf8); let buffer = new Uint8Array(0); const textDecoder = new TextDecoder(); return { responseHeaders, value: response.body.pipeThrough( new TransformStream<Uint8Array, ParseResult<T>>({ async transform(chunk, controller) { // Append new chunk to buffer. const newBuffer = new Uint8Array(buffer.length + chunk.length); newBuffer.set(buffer); newBuffer.set(chunk, buffer.length); buffer = newBuffer; // Try to decode messages from buffer. while (buffer.length >= 4) { // The first 4 bytes are the total length (big-endian). const totalLength = new DataView( buffer.buffer, buffer.byteOffset, buffer.byteLength, ).getUint32(0, false); // If we don't have the full message yet, wait for more chunks. if (buffer.length < totalLength) { break; } try { // Decode exactly the sub-slice for this event. const subView = buffer.subarray(0, totalLength); const decoded = codec.decode(subView); // Slice the used bytes out of the buffer, removing this message. buffer = buffer.slice(totalLength); // Process the message. if (decoded.headers[':message-type']?.value === 'event') { const data = textDecoder.decode(decoded.body); // Wrap the data in the `:event-type` field to match the expected schema. const parsedDataResult = await safeParseJSON({ text: data }); if (!parsedDataResult.success) { controller.enqueue(parsedDataResult); break; } // The `p` field appears to be padding or some other non-functional field. delete (parsedDataResult.value as any).p; let wrappedData = { [decoded.headers[':event-type']?.value as string]: parsedDataResult.value, }; // Re-validate with the expected schema. const validatedWrappedData = await safeValidateTypes<T>({ value: wrappedData, schema: chunkSchema, }); if (!validatedWrappedData.success) { controller.enqueue(validatedWrappedData); } else { controller.enqueue({ success: true, value: validatedWrappedData.value, rawValue: wrappedData, }); } } } catch (e) { // If we can't decode a complete message, wait for more data break; } } }, }), ), }; }; --- File: /ai/packages/amazon-bedrock/src/bedrock-image-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { createAmazonBedrock } from './bedrock-provider'; import { BedrockImageModel } from './bedrock-image-model'; import { injectFetchHeaders } from './inject-fetch-headers'; const prompt = 'A cute baby sea otter'; const provider = createAmazonBedrock(); const fakeFetchWithAuth = injectFetchHeaders({ 'x-amz-auth': 'test-auth' }); const invokeUrl = `https://bedrock-runtime.us-east-1.amazonaws.com/model/${encodeURIComponent( 'amazon.nova-canvas-v1:0', )}/invoke`; describe('doGenerate', () => { const mockConfigHeaders = { 'config-header': 'config-value', 'shared-header': 'config-shared', }; const server = createTestServer({ [invokeUrl]: { response: { type: 'binary', headers: { 'content-type': 'application/json', }, body: Buffer.from( JSON.stringify({ images: ['base64-image-1', 'base64-image-2'], }), ), }, }, }); const model = new BedrockImageModel('amazon.nova-canvas-v1:0', { baseUrl: () => 'https://bedrock-runtime.us-east-1.amazonaws.com', headers: mockConfigHeaders, fetch: fakeFetchWithAuth, }); it('should pass the model and the settings', async () => { await model.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: 1234, providerOptions: { bedrock: { negativeText: 'bad', quality: 'premium', cfgScale: 1.2, }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ taskType: 'TEXT_IMAGE', textToImageParams: { text: prompt, negativeText: 'bad', }, imageGenerationConfig: { numberOfImages: 1, seed: 1234, quality: 'premium', cfgScale: 1.2, width: 1024, height: 1024, }, }); }); it('should properly combine headers from all sources', async () => { const optionsHeaders = { 'options-header': 'options-value', 'shared-header': 'options-shared', }; const modelWithHeaders = new BedrockImageModel('amazon.nova-canvas-v1:0', { baseUrl: () => 'https://bedrock-runtime.us-east-1.amazonaws.com', headers: { 'model-header': 'model-value', 'shared-header': 'model-shared', }, fetch: injectFetchHeaders({ 'signed-header': 'signed-value', authorization: 'AWS4-HMAC-SHA256...', }), }); await modelWithHeaders.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, headers: optionsHeaders, }); const requestHeaders = server.calls[0].requestHeaders; expect(requestHeaders['options-header']).toBe('options-value'); expect(requestHeaders['model-header']).toBe('model-value'); expect(requestHeaders['signed-header']).toBe('signed-value'); expect(requestHeaders['authorization']).toBe('AWS4-HMAC-SHA256...'); expect(requestHeaders['shared-header']).toBe('options-shared'); }); it('should respect maxImagesPerCall setting', async () => { const defaultModel = provider.image('amazon.nova-canvas-v1:0'); expect(defaultModel.maxImagesPerCall).toBe(5); // 'amazon.nova-canvas-v1:0','s default from settings const unknownModel = provider.image('unknown-model' as any); expect(unknownModel.maxImagesPerCall).toBe(1); // fallback for unknown models }); it('should return warnings for unsupported settings', async () => { const result = await model.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: '1:1', seed: undefined, providerOptions: {}, }); expect(result.warnings).toStrictEqual([ { type: 'unsupported-setting', setting: 'aspectRatio', details: 'This model does not support aspect ratio. Use `size` instead.', }, ]); }); it('should extract the generated images', async () => { const result = await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.images).toStrictEqual(['base64-image-1', 'base64-image-2']); }); it('should include response data with timestamp, modelId and headers', async () => { const testDate = new Date('2024-03-15T12:00:00Z'); const customModel = new BedrockImageModel('amazon.nova-canvas-v1:0', { baseUrl: () => 'https://bedrock-runtime.us-east-1.amazonaws.com', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.response).toStrictEqual({ timestamp: testDate, modelId: 'amazon.nova-canvas-v1:0', headers: { 'content-length': '46', 'content-type': 'application/json', }, }); }); it('should use real date when no custom date provider is specified', async () => { const beforeDate = new Date(); const result = await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: 1234, providerOptions: {}, }); const afterDate = new Date(); expect(result.response.timestamp.getTime()).toBeGreaterThanOrEqual( beforeDate.getTime(), ); expect(result.response.timestamp.getTime()).toBeLessThanOrEqual( afterDate.getTime(), ); expect(result.response.modelId).toBe('amazon.nova-canvas-v1:0'); }); it('should pass the style parameter when provided', async () => { await model.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: 1234, providerOptions: { bedrock: { negativeText: 'bad', quality: 'premium', cfgScale: 1.2, style: 'PHOTOREALISM', }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ taskType: 'TEXT_IMAGE', textToImageParams: { text: prompt, negativeText: 'bad', style: 'PHOTOREALISM', }, imageGenerationConfig: { numberOfImages: 1, seed: 1234, quality: 'premium', cfgScale: 1.2, width: 1024, height: 1024, }, }); }); it('should not include style parameter when not provided', async () => { await model.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: 1234, providerOptions: { bedrock: { quality: 'standard', }, }, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.textToImageParams).not.toHaveProperty('style'); }); }); --- File: /ai/packages/amazon-bedrock/src/bedrock-image-model.ts --- import { ImageModelV2, ImageModelV2CallWarning } from '@ai-sdk/provider'; import { FetchFunction, Resolvable, combineHeaders, createJsonErrorResponseHandler, createJsonResponseHandler, postJsonToApi, resolve, } from '@ai-sdk/provider-utils'; import { BedrockImageModelId, modelMaxImagesPerCall, } from './bedrock-image-settings'; import { BedrockErrorSchema } from './bedrock-error'; import { z } from 'zod/v4'; type BedrockImageModelConfig = { baseUrl: () => string; headers: Resolvable<Record<string, string | undefined>>; fetch?: FetchFunction; _internal?: { currentDate?: () => Date; }; }; export class BedrockImageModel implements ImageModelV2 { readonly specificationVersion = 'v2'; readonly provider = 'amazon-bedrock'; get maxImagesPerCall(): number { return modelMaxImagesPerCall[this.modelId] ?? 1; } private getUrl(modelId: string): string { const encodedModelId = encodeURIComponent(modelId); return `${this.config.baseUrl()}/model/${encodedModelId}/invoke`; } constructor( readonly modelId: BedrockImageModelId, private readonly config: BedrockImageModelConfig, ) {} async doGenerate({ prompt, n, size, aspectRatio, seed, providerOptions, headers, abortSignal, }: Parameters<ImageModelV2['doGenerate']>[0]): Promise< Awaited<ReturnType<ImageModelV2['doGenerate']>> > { const warnings: Array<ImageModelV2CallWarning> = []; const [width, height] = size ? size.split('x').map(Number) : []; const args = { taskType: 'TEXT_IMAGE', textToImageParams: { text: prompt, ...(providerOptions?.bedrock?.negativeText ? { negativeText: providerOptions.bedrock.negativeText, } : {}), ...(providerOptions?.bedrock?.style ? { style: providerOptions.bedrock.style, } : {}), }, imageGenerationConfig: { ...(width ? { width } : {}), ...(height ? { height } : {}), ...(seed ? { seed } : {}), ...(n ? { numberOfImages: n } : {}), ...(providerOptions?.bedrock?.quality ? { quality: providerOptions.bedrock.quality } : {}), ...(providerOptions?.bedrock?.cfgScale ? { cfgScale: providerOptions.bedrock.cfgScale } : {}), }, }; if (aspectRatio != undefined) { warnings.push({ type: 'unsupported-setting', setting: 'aspectRatio', details: 'This model does not support aspect ratio. Use `size` instead.', }); } const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { value: response, responseHeaders } = await postJsonToApi({ url: this.getUrl(this.modelId), headers: await resolve( combineHeaders(await resolve(this.config.headers), headers), ), body: args, failedResponseHandler: createJsonErrorResponseHandler({ errorSchema: BedrockErrorSchema, errorToMessage: error => `${error.type}: ${error.message}`, }), successfulResponseHandler: createJsonResponseHandler( bedrockImageResponseSchema, ), abortSignal, fetch: this.config.fetch, }); return { images: response.images, warnings, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, }, }; } } // minimal version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const bedrockImageResponseSchema = z.object({ images: z.array(z.string()), }); --- File: /ai/packages/amazon-bedrock/src/bedrock-image-settings.ts --- export type BedrockImageModelId = 'amazon.nova-canvas-v1:0' | (string & {}); // https://docs.aws.amazon.com/nova/latest/userguide/image-gen-req-resp-structure.html export const modelMaxImagesPerCall: Record<BedrockImageModelId, number> = { 'amazon.nova-canvas-v1:0': 5, }; --- File: /ai/packages/amazon-bedrock/src/bedrock-prepare-tools.ts --- import { JSONObject, LanguageModelV2CallOptions, LanguageModelV2CallWarning, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { asSchema } from '@ai-sdk/provider-utils'; import { anthropicTools, prepareTools as prepareAnthropicTools, } from '@ai-sdk/anthropic/internal'; import { BedrockTool, BedrockToolConfiguration } from './bedrock-api-types'; export function prepareTools({ tools, toolChoice, modelId, }: { tools: LanguageModelV2CallOptions['tools']; toolChoice?: LanguageModelV2CallOptions['toolChoice']; modelId: string; }): { toolConfig: BedrockToolConfiguration; additionalTools: Record<string, unknown> | undefined; betas: Set<string>; toolWarnings: LanguageModelV2CallWarning[]; } { const toolWarnings: LanguageModelV2CallWarning[] = []; const betas = new Set<string>(); if (tools == null || tools.length === 0) { return { toolConfig: {}, additionalTools: undefined, betas, toolWarnings, }; } // Filter out unsupported web_search tool and add a warning const supportedTools = tools.filter(tool => { if ( tool.type === 'provider-defined' && tool.id === 'anthropic.web_search_20250305' ) { toolWarnings.push({ type: 'unsupported-tool', tool, details: 'The web_search_20250305 tool is not supported on Amazon Bedrock.', }); return false; // Exclude this tool } return true; // Include all other tools }); if (supportedTools.length === 0) { return { toolConfig: {}, additionalTools: undefined, betas, toolWarnings, }; } const isAnthropicModel = modelId.includes('anthropic.'); const providerDefinedTools = supportedTools.filter( t => t.type === 'provider-defined', ); const functionTools = supportedTools.filter(t => t.type === 'function'); let additionalTools: Record<string, unknown> | undefined = undefined; const bedrockTools: BedrockTool[] = []; const usingAnthropicTools = isAnthropicModel && providerDefinedTools.length > 0; // Handle Anthropic provider-defined tools for Anthropic models on Bedrock if (usingAnthropicTools) { if (functionTools.length > 0) { toolWarnings.push({ type: 'unsupported-setting', setting: 'tools', details: 'Mixed Anthropic provider-defined tools and standard function tools are not supported in a single call to Bedrock. Only Anthropic tools will be used.', }); } const { toolChoice: preparedAnthropicToolChoice, toolWarnings: anthropicToolWarnings, betas: anthropicBetas, } = prepareAnthropicTools({ tools: providerDefinedTools, toolChoice, }); toolWarnings.push(...anthropicToolWarnings); anthropicBetas.forEach(beta => betas.add(beta)); // For Anthropic tools on Bedrock, only the 'tool_choice' goes into additional fields. // The tool definitions themselves are sent in the standard 'toolConfig'. if (preparedAnthropicToolChoice) { additionalTools = { tool_choice: preparedAnthropicToolChoice, }; } // Create a standard Bedrock tool representation for validation purposes for (const tool of providerDefinedTools) { const toolFactory = Object.values(anthropicTools).find(factory => { const instance = (factory as (args: any) => any)({}); return instance.id === tool.id; }); if (toolFactory != null) { const fullToolDefinition = (toolFactory as (args: any) => any)({}); bedrockTools.push({ toolSpec: { name: tool.name, inputSchema: { json: asSchema(fullToolDefinition.inputSchema) .jsonSchema as JSONObject, }, }, }); } else { toolWarnings.push({ type: 'unsupported-tool', tool }); } } } else { // Report unsupported provider-defined tools for non-anthropic models for (const tool of providerDefinedTools) { toolWarnings.push({ type: 'unsupported-tool', tool }); } } // Handle standard function tools for all models for (const tool of functionTools) { bedrockTools.push({ toolSpec: { name: tool.name, description: tool.description, inputSchema: { json: tool.inputSchema as JSONObject, }, }, }); } // Handle toolChoice for standard Bedrock tools, but NOT for Anthropic provider-defined tools let bedrockToolChoice: BedrockToolConfiguration['toolChoice'] = undefined; if (!usingAnthropicTools && bedrockTools.length > 0 && toolChoice) { const type = toolChoice.type; switch (type) { case 'auto': bedrockToolChoice = { auto: {} }; break; case 'required': bedrockToolChoice = { any: {} }; break; case 'none': bedrockTools.length = 0; bedrockToolChoice = undefined; break; case 'tool': bedrockToolChoice = { tool: { name: toolChoice.toolName } }; break; default: { const _exhaustiveCheck: never = type; throw new UnsupportedFunctionalityError({ functionality: `tool choice type: ${_exhaustiveCheck}`, }); } } } const toolConfig: BedrockToolConfiguration = bedrockTools.length > 0 ? { tools: bedrockTools, toolChoice: bedrockToolChoice } : {}; return { toolConfig, additionalTools, betas, toolWarnings, }; } --- File: /ai/packages/amazon-bedrock/src/bedrock-provider.test.ts --- import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; import { createAmazonBedrock } from './bedrock-provider'; import { BedrockChatLanguageModel } from './bedrock-chat-language-model'; import { BedrockEmbeddingModel } from './bedrock-embedding-model'; import { BedrockImageModel } from './bedrock-image-model'; import { anthropicTools } from '@ai-sdk/anthropic/internal'; // Add type assertions for the mocked classes const BedrockChatLanguageModelMock = BedrockChatLanguageModel as unknown as Mock; const BedrockEmbeddingModelMock = BedrockEmbeddingModel as unknown as Mock; const BedrockImageModelMock = BedrockImageModel as unknown as Mock; vi.mock('./bedrock-chat-language-model', () => ({ BedrockChatLanguageModel: vi.fn(), })); vi.mock('./bedrock-embedding-model', () => ({ BedrockEmbeddingModel: vi.fn(), })); vi.mock('./bedrock-image-model', () => ({ BedrockImageModel: vi.fn(), })); vi.mock('./bedrock-sigv4-fetch', () => ({ createSigV4FetchFunction: vi.fn(), createApiKeyFetchFunction: vi.fn(), })); vi.mock('@ai-sdk/anthropic', async importOriginal => { const original = await importOriginal<typeof import('@ai-sdk/anthropic')>(); return { ...original, anthropicTools: { mock: 'tools' }, prepareTools: vi.fn(), }; }); vi.mock('@ai-sdk/provider-utils', async importOriginal => { const original = await importOriginal<typeof import('@ai-sdk/provider-utils')>(); return { ...original, loadSetting: vi .fn() .mockImplementation(({ settingValue }) => settingValue || 'us-east-1'), loadOptionalSetting: vi .fn() .mockImplementation(({ settingValue }) => settingValue), withoutTrailingSlash: vi.fn(url => url), generateId: vi.fn().mockReturnValue('mock-id'), createJsonErrorResponseHandler: vi.fn(), createJsonResponseHandler: vi.fn(), postJsonToApi: vi.fn(), resolve: vi.fn(val => Promise.resolve(val)), combineHeaders: vi.fn((...headers) => Object.assign({}, ...headers)), parseProviderOptions: vi.fn(), asSchema: vi.fn(schema => ({ jsonSchema: schema })), }; }); // Import mocked modules to get references import { createSigV4FetchFunction, createApiKeyFetchFunction, } from './bedrock-sigv4-fetch'; import { loadOptionalSetting } from '@ai-sdk/provider-utils'; const mockCreateSigV4FetchFunction = vi.mocked(createSigV4FetchFunction); const mockCreateApiKeyFetchFunction = vi.mocked(createApiKeyFetchFunction); const mockLoadOptionalSetting = vi.mocked(loadOptionalSetting); describe('AmazonBedrockProvider', () => { beforeEach(() => { vi.clearAllMocks(); // Reset mock implementations mockCreateSigV4FetchFunction.mockReturnValue(vi.fn()); mockCreateApiKeyFetchFunction.mockReturnValue(vi.fn()); mockLoadOptionalSetting.mockImplementation( ({ settingValue }) => settingValue, ); }); describe('createAmazonBedrock', () => { it('should create a provider instance with default options', () => { const provider = createAmazonBedrock(); const model = provider('anthropic.claude-v2'); const constructorCall = BedrockChatLanguageModelMock.mock.calls[0]; expect(constructorCall[0]).toBe('anthropic.claude-v2'); expect(constructorCall[1].headers).toEqual({}); expect(constructorCall[1].baseUrl()).toBe( 'https://bedrock-runtime.us-east-1.amazonaws.com', ); }); it('should create a provider instance with custom options', () => { const customHeaders = { 'Custom-Header': 'value' }; const options = { region: 'eu-west-1', baseURL: 'https://custom.url', headers: customHeaders, }; const provider = createAmazonBedrock(options); provider('anthropic.claude-v2'); const constructorCall = BedrockChatLanguageModelMock.mock.calls[0]; expect(constructorCall[1].headers).toEqual(customHeaders); expect(constructorCall[1].baseUrl()).toBe('https://custom.url'); }); it('should accept a credentialProvider in options', () => { const mockCredentialProvider = vi.fn().mockResolvedValue({ accessKeyId: 'dynamic-access-key', secretAccessKey: 'dynamic-secret-key', sessionToken: 'dynamic-session-token', }); const provider = createAmazonBedrock({ credentialProvider: mockCredentialProvider, }); provider('anthropic.claude-v2'); const constructorCall = BedrockChatLanguageModelMock.mock.calls[0]; expect(constructorCall[0]).toBe('anthropic.claude-v2'); expect(constructorCall[1].headers).toEqual({}); expect(constructorCall[1].baseUrl()).toBe( 'https://bedrock-runtime.us-east-1.amazonaws.com', ); }); it('should prioritize credentialProvider over static credentials', () => { const mockCredentialProvider = vi.fn().mockResolvedValue({ accessKeyId: 'dynamic-access-key', secretAccessKey: 'dynamic-secret-key', sessionToken: 'dynamic-session-token', }); const provider = createAmazonBedrock({ accessKeyId: 'static-access-key', secretAccessKey: 'static-secret-key', sessionToken: 'static-session-token', credentialProvider: mockCredentialProvider, }); provider('anthropic.claude-v2'); const constructorCall = BedrockChatLanguageModelMock.mock.calls[0]; expect(constructorCall[0]).toBe('anthropic.claude-v2'); }); it('should pass headers to embedding model', () => { const customHeaders = { 'Custom-Header': 'value' }; const provider = createAmazonBedrock({ headers: customHeaders, }); provider.embedding('amazon.titan-embed-text-v1'); const constructorCall = BedrockEmbeddingModelMock.mock.calls[0]; expect(constructorCall[1].headers).toEqual(customHeaders); }); it('should throw error when called with new keyword', () => { const provider = createAmazonBedrock(); expect(() => { new (provider as any)(); }).toThrow( 'The Amazon Bedrock model function cannot be called with the new keyword.', ); }); describe('API Key Authentication', () => { it('should use API key when provided in options', () => { const provider = createAmazonBedrock({ apiKey: 'test-api-key', region: 'us-east-1', }); // Verify that createApiKeyFetchFunction was called with the correct API key expect(mockCreateApiKeyFetchFunction).toHaveBeenCalledWith( 'test-api-key', undefined, // fetch function ); expect(mockCreateSigV4FetchFunction).not.toHaveBeenCalled(); provider('anthropic.claude-v2'); const constructorCall = BedrockChatLanguageModelMock.mock.calls[0]; expect(constructorCall[0]).toBe('anthropic.claude-v2'); expect(constructorCall[1].headers).toEqual({}); expect(constructorCall[1].baseUrl()).toBe( 'https://bedrock-runtime.us-east-1.amazonaws.com', ); }); it('should use API key from environment variable', () => { // Mock loadOptionalSetting to return environment variable value mockLoadOptionalSetting.mockImplementation( ({ settingValue, environmentVariableName }) => { if (environmentVariableName === 'AWS_BEARER_TOKEN_BEDROCK') { return 'env-api-key'; } return settingValue; }, ); const provider = createAmazonBedrock({ region: 'us-east-1', }); // Verify that createApiKeyFetchFunction was called with the environment variable value expect(mockCreateApiKeyFetchFunction).toHaveBeenCalledWith( 'env-api-key', undefined, ); expect(mockCreateSigV4FetchFunction).not.toHaveBeenCalled(); }); it('should prioritize options.apiKey over environment variable', () => { // Mock loadOptionalSetting to return environment variable value when no settingValue mockLoadOptionalSetting.mockImplementation( ({ settingValue, environmentVariableName }) => { if (settingValue) { return settingValue; } if (environmentVariableName === 'AWS_BEARER_TOKEN_BEDROCK') { return 'env-api-key'; } return undefined; }, ); const provider = createAmazonBedrock({ apiKey: 'options-api-key', region: 'us-east-1', }); // Verify that options.apiKey takes precedence expect(mockCreateApiKeyFetchFunction).toHaveBeenCalledWith( 'options-api-key', undefined, ); expect(mockCreateSigV4FetchFunction).not.toHaveBeenCalled(); }); it('should fall back to SigV4 when no API key provided', () => { // Mock loadOptionalSetting to return undefined (no API key) mockLoadOptionalSetting.mockImplementation(() => undefined); const provider = createAmazonBedrock({ region: 'us-east-1', accessKeyId: 'test-access-key', secretAccessKey: 'test-secret-key', }); // Verify that SigV4 authentication is used as fallback expect(mockCreateApiKeyFetchFunction).not.toHaveBeenCalled(); expect(mockCreateSigV4FetchFunction).toHaveBeenCalled(); provider('anthropic.claude-v2'); const constructorCall = BedrockChatLanguageModelMock.mock.calls[0]; expect(constructorCall[0]).toBe('anthropic.claude-v2'); }); it('should pass custom fetch function to API key authentication', () => { const customFetch = vi.fn(); const provider = createAmazonBedrock({ apiKey: 'test-api-key', region: 'us-east-1', fetch: customFetch, }); // Verify that custom fetch function is passed to createApiKeyFetchFunction expect(mockCreateApiKeyFetchFunction).toHaveBeenCalledWith( 'test-api-key', customFetch, ); }); it('should pass custom fetch function to SigV4 authentication', () => { // Mock loadOptionalSetting to return undefined (no API key) mockLoadOptionalSetting.mockImplementation(() => undefined); const customFetch = vi.fn(); const provider = createAmazonBedrock({ region: 'us-east-1', accessKeyId: 'test-access-key', secretAccessKey: 'test-secret-key', fetch: customFetch, }); // Verify that custom fetch function is passed to createSigV4FetchFunction expect(mockCreateSigV4FetchFunction).toHaveBeenCalledWith( expect.any(Function), // credentials function customFetch, ); }); it('should work with embedding models when using API key', () => { const provider = createAmazonBedrock({ apiKey: 'test-api-key', region: 'us-east-1', headers: { 'Custom-Header': 'value' }, }); provider.embedding('amazon.titan-embed-text-v1'); const constructorCall = BedrockEmbeddingModelMock.mock.calls[0]; expect(constructorCall[0]).toBe('amazon.titan-embed-text-v1'); expect(constructorCall[1].headers).toEqual({ 'Custom-Header': 'value', }); expect(mockCreateApiKeyFetchFunction).toHaveBeenCalledWith( 'test-api-key', undefined, ); }); it('should work with image models when using API key', () => { const provider = createAmazonBedrock({ apiKey: 'test-api-key', region: 'us-east-1', headers: { 'Custom-Header': 'value' }, }); provider.image('amazon.titan-image-generator'); const constructorCall = BedrockImageModelMock.mock.calls[0]; expect(constructorCall[0]).toBe('amazon.titan-image-generator'); expect(constructorCall[1].headers).toEqual({ 'Custom-Header': 'value', }); expect(mockCreateApiKeyFetchFunction).toHaveBeenCalledWith( 'test-api-key', undefined, ); }); it('should maintain backward compatibility with existing SigV4 authentication', () => { // Mock loadOptionalSetting to return undefined (no API key) mockLoadOptionalSetting.mockImplementation(() => undefined); const provider = createAmazonBedrock({ region: 'eu-west-1', accessKeyId: 'test-access-key', secretAccessKey: 'test-secret-key', sessionToken: 'test-session-token', }); provider('anthropic.claude-v2'); // Verify SigV4 is used when no API key is provided expect(mockCreateSigV4FetchFunction).toHaveBeenCalled(); expect(mockCreateApiKeyFetchFunction).not.toHaveBeenCalled(); const constructorCall = BedrockChatLanguageModelMock.mock.calls[0]; expect(constructorCall[0]).toBe('anthropic.claude-v2'); expect(constructorCall[1].baseUrl()).toBe( 'https://bedrock-runtime.eu-west-1.amazonaws.com', ); }); it('should work with credential provider when no API key is provided', () => { // Mock loadOptionalSetting to return undefined (no API key) mockLoadOptionalSetting.mockImplementation(() => undefined); const mockCredentialProvider = vi.fn().mockResolvedValue({ accessKeyId: 'dynamic-access-key', secretAccessKey: 'dynamic-secret-key', sessionToken: 'dynamic-session-token', }); const provider = createAmazonBedrock({ region: 'us-east-1', credentialProvider: mockCredentialProvider, }); provider('anthropic.claude-v2'); // Verify SigV4 is used with credential provider when no API key expect(mockCreateSigV4FetchFunction).toHaveBeenCalled(); expect(mockCreateApiKeyFetchFunction).not.toHaveBeenCalled(); }); }); }); describe('provider methods', () => { it('should create an embedding model', () => { const provider = createAmazonBedrock(); const modelId = 'amazon.titan-embed-text-v1'; const model = provider.embedding(modelId); const constructorCall = BedrockEmbeddingModelMock.mock.calls[0]; expect(constructorCall[0]).toBe(modelId); expect(model).toBeInstanceOf(BedrockEmbeddingModel); }); it('should create an image model', () => { const provider = createAmazonBedrock(); const modelId = 'amazon.titan-image-generator'; const model = provider.image(modelId); const constructorCall = BedrockImageModelMock.mock.calls[0]; expect(constructorCall[0]).toBe(modelId); expect(model).toBeInstanceOf(BedrockImageModel); }); it('should create an image model via imageModel method', () => { const provider = createAmazonBedrock(); const modelId = 'amazon.titan-image-generator'; const model = provider.imageModel(modelId); const constructorCall = BedrockImageModelMock.mock.calls[0]; expect(constructorCall[0]).toBe(modelId); expect(model).toBeInstanceOf(BedrockImageModel); }); it('should expose anthropicTools', () => { const provider = createAmazonBedrock(); expect(provider.tools).toBe(anthropicTools); }); }); }); --- File: /ai/packages/amazon-bedrock/src/bedrock-provider.ts --- import { EmbeddingModelV2, ImageModelV2, LanguageModelV2, ProviderV2, } from '@ai-sdk/provider'; import { FetchFunction, generateId, loadOptionalSetting, loadSetting, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { anthropicTools } from '@ai-sdk/anthropic/internal'; import { BedrockChatLanguageModel } from './bedrock-chat-language-model'; import { BedrockChatModelId } from './bedrock-chat-options'; import { BedrockEmbeddingModel } from './bedrock-embedding-model'; import { BedrockEmbeddingModelId } from './bedrock-embedding-options'; import { BedrockImageModel } from './bedrock-image-model'; import { BedrockImageModelId } from './bedrock-image-settings'; import { BedrockCredentials, createSigV4FetchFunction, createApiKeyFetchFunction, } from './bedrock-sigv4-fetch'; export interface AmazonBedrockProviderSettings { /** The AWS region to use for the Bedrock provider. Defaults to the value of the `AWS_REGION` environment variable. */ region?: string; /** API key for authenticating requests using Bearer token authentication. When provided, this will be used instead of AWS SigV4 authentication. Defaults to the value of the `AWS_BEARER_TOKEN_BEDROCK` environment variable. @example ```typescript // Using API key directly const bedrock = createAmazonBedrock({ apiKey: 'your-api-key-here', region: 'us-east-1' }); // Using environment variable AWS_BEARER_TOKEN_BEDROCK const bedrock = createAmazonBedrock({ region: 'us-east-1' }); ``` Note: When `apiKey` is provided, it takes precedence over AWS SigV4 authentication. If neither `apiKey` nor `AWS_BEARER_TOKEN_BEDROCK` environment variable is set, the provider will fall back to AWS SigV4 authentication using AWS credentials. */ apiKey?: string; /** The AWS access key ID to use for the Bedrock provider. Defaults to the value of the `AWS_ACCESS_KEY_ID` environment variable. */ accessKeyId?: string; /** The AWS secret access key to use for the Bedrock provider. Defaults to the value of the `AWS_SECRET_ACCESS_KEY` environment variable. */ secretAccessKey?: string; /** The AWS session token to use for the Bedrock provider. Defaults to the value of the `AWS_SESSION_TOKEN` environment variable. */ sessionToken?: string; /** Base URL for the Bedrock API calls. */ baseURL?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; /** The AWS credential provider to use for the Bedrock provider to get dynamic credentials similar to the AWS SDK. Setting a provider here will cause its credential values to be used instead of the `accessKeyId`, `secretAccessKey`, and `sessionToken` settings. */ credentialProvider?: () => PromiseLike<Omit<BedrockCredentials, 'region'>>; // for testing generateId?: () => string; } export interface AmazonBedrockProvider extends ProviderV2 { (modelId: BedrockChatModelId): LanguageModelV2; languageModel(modelId: BedrockChatModelId): LanguageModelV2; embedding(modelId: BedrockEmbeddingModelId): EmbeddingModelV2<string>; /** Creates a model for image generation. */ image(modelId: BedrockImageModelId): ImageModelV2; /** Creates a model for image generation. */ imageModel(modelId: BedrockImageModelId): ImageModelV2; /** Anthropic-specific tools that can be used with Anthropic models on Bedrock. */ tools: typeof anthropicTools; } /** Create an Amazon Bedrock provider instance. */ export function createAmazonBedrock( options: AmazonBedrockProviderSettings = {}, ): AmazonBedrockProvider { // Check for API key authentication first const rawApiKey = loadOptionalSetting({ settingValue: options.apiKey, environmentVariableName: 'AWS_BEARER_TOKEN_BEDROCK', }); // FIX 1: Validate API key to ensure proper fallback to SigV4 // Only use API key if it's a non-empty, non-whitespace string const apiKey = rawApiKey && rawApiKey.trim().length > 0 ? rawApiKey.trim() : undefined; // Use API key authentication if available, otherwise fall back to SigV4 const fetchFunction = apiKey ? createApiKeyFetchFunction(apiKey, options.fetch) : createSigV4FetchFunction(async () => { const region = loadSetting({ settingValue: options.region, settingName: 'region', environmentVariableName: 'AWS_REGION', description: 'AWS region', }); // If a credential provider is provided, use it to get the credentials. if (options.credentialProvider) { try { return { ...(await options.credentialProvider()), region, }; } catch (error) { // Error handling for credential provider failures const errorMessage = error instanceof Error ? error.message : String(error); throw new Error( `AWS credential provider failed: ${errorMessage}. ` + 'Please ensure your credential provider returns valid AWS credentials ' + 'with accessKeyId and secretAccessKey properties.', ); } } // Enhanced error handling for SigV4 credential loading try { return { region, accessKeyId: loadSetting({ settingValue: options.accessKeyId, settingName: 'accessKeyId', environmentVariableName: 'AWS_ACCESS_KEY_ID', description: 'AWS access key ID', }), secretAccessKey: loadSetting({ settingValue: options.secretAccessKey, settingName: 'secretAccessKey', environmentVariableName: 'AWS_SECRET_ACCESS_KEY', description: 'AWS secret access key', }), sessionToken: loadOptionalSetting({ settingValue: options.sessionToken, environmentVariableName: 'AWS_SESSION_TOKEN', }), }; } catch (error) { // Provide helpful error message for missing AWS credentials const errorMessage = error instanceof Error ? error.message : String(error); if ( errorMessage.includes('AWS_ACCESS_KEY_ID') || errorMessage.includes('accessKeyId') ) { throw new Error( 'AWS SigV4 authentication requires AWS credentials. Please provide either:\n' + '1. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables\n' + '2. Provide accessKeyId and secretAccessKey in options\n' + '3. Use a credentialProvider function\n' + '4. Use API key authentication with AWS_BEARER_TOKEN_BEDROCK or apiKey option\n' + `Original error: ${errorMessage}`, ); } if ( errorMessage.includes('AWS_SECRET_ACCESS_KEY') || errorMessage.includes('secretAccessKey') ) { throw new Error( 'AWS SigV4 authentication requires both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. ' + 'Please ensure both credentials are provided.\n' + `Original error: ${errorMessage}`, ); } // Re-throw other errors as-is throw error; } }, options.fetch); const getBaseUrl = (): string => withoutTrailingSlash( options.baseURL ?? `https://bedrock-runtime.${loadSetting({ settingValue: options.region, settingName: 'region', environmentVariableName: 'AWS_REGION', description: 'AWS region', })}.amazonaws.com`, ) ?? `https://bedrock-runtime.us-east-1.amazonaws.com`; const createChatModel = (modelId: BedrockChatModelId) => new BedrockChatLanguageModel(modelId, { baseUrl: getBaseUrl, headers: options.headers ?? {}, fetch: fetchFunction, generateId, }); const provider = function (modelId: BedrockChatModelId) { if (new.target) { throw new Error( 'The Amazon Bedrock model function cannot be called with the new keyword.', ); } return createChatModel(modelId); }; const createEmbeddingModel = (modelId: BedrockEmbeddingModelId) => new BedrockEmbeddingModel(modelId, { baseUrl: getBaseUrl, headers: options.headers ?? {}, fetch: fetchFunction, }); const createImageModel = (modelId: BedrockImageModelId) => new BedrockImageModel(modelId, { baseUrl: getBaseUrl, headers: options.headers ?? {}, fetch: fetchFunction, }); provider.languageModel = createChatModel; provider.embedding = createEmbeddingModel; provider.textEmbedding = createEmbeddingModel; provider.textEmbeddingModel = createEmbeddingModel; provider.image = createImageModel; provider.imageModel = createImageModel; provider.tools = anthropicTools; return provider; } /** Default Bedrock provider instance. */ export const bedrock = createAmazonBedrock(); --- File: /ai/packages/amazon-bedrock/src/bedrock-sigv4-fetch.test.ts --- import { createSigV4FetchFunction, createApiKeyFetchFunction, } from './bedrock-sigv4-fetch'; import { vi, describe, it, expect, afterEach } from 'vitest'; // Mock AwsV4Signer so that no real crypto calls are made. vi.mock('aws4fetch', () => { class MockAwsV4Signer { options: any; constructor(options: any) { this.options = options; } async sign() { // Return a fake Headers instance with predetermined signing headers. const headers = new Headers(); headers.set('x-amz-date', '20240315T000000Z'); headers.set('authorization', 'AWS4-HMAC-SHA256 Credential=test'); if (this.options.sessionToken) { headers.set('x-amz-security-token', this.options.sessionToken); } return { headers }; } } return { AwsV4Signer: MockAwsV4Signer }; }); const createFetchFunction = (dummyFetch: any) => createSigV4FetchFunction( () => ({ region: 'us-west-2', accessKeyId: 'test-access-key', secretAccessKey: 'test-secret', }), dummyFetch, ); describe('createSigV4FetchFunction', () => { afterEach(() => { vi.restoreAllMocks(); }); it('should bypass signing for non-POST requests', async () => { const dummyResponse = new Response('OK', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const fetchFn = createFetchFunction(dummyFetch); const response = await fetchFn('http://example.com', { method: 'GET' }); expect(dummyFetch).toHaveBeenCalledWith('http://example.com', { method: 'GET', }); expect(response).toBe(dummyResponse); }); it('should bypass signing if POST request has no body', async () => { const dummyResponse = new Response('OK', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const fetchFn = createFetchFunction(dummyFetch); const response = await fetchFn('http://example.com', { method: 'POST' }); expect(dummyFetch).toHaveBeenCalledWith('http://example.com', { method: 'POST', }); expect(response).toBe(dummyResponse); }); it('should handle a POST request with a string body and merge signed headers', async () => { const dummyResponse = new Response('Signed', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); // Provide settings (including a sessionToken) so that the signer includes that header.\ const fetchFn = createSigV4FetchFunction( () => ({ region: 'us-west-2', accessKeyId: 'test-access-key', secretAccessKey: 'test-secret', sessionToken: 'test-session-token', }), dummyFetch, ); const inputUrl = 'http://example.com'; const init: RequestInit = { method: 'POST', body: '{"test": "data"}', headers: { 'Content-Type': 'application/json', 'Custom-Header': 'value', }, }; await fetchFn(inputUrl, init); expect(dummyFetch).toHaveBeenCalled(); const calledInit = dummyFetch.mock.calls[0][1] as RequestInit; // `combinedHeaders` should merge the original headers with the signing // headers added by the AwsV4Signer mock. const headers = calledInit.headers as Record<string, string>; expect(headers['content-type']).toEqual('application/json'); expect(headers['custom-header']).toEqual('value'); expect(headers['empty-header']).toBeUndefined(); expect(headers['x-amz-date']).toEqual('20240315T000000Z'); expect(headers['authorization']).toEqual( 'AWS4-HMAC-SHA256 Credential=test', ); expect(headers['x-amz-security-token']).toEqual('test-session-token'); // Body is left unmodified for a string body. expect(calledInit.body).toEqual('{"test": "data"}'); }); it('should handle non-string body by stringifying it', async () => { const dummyResponse = new Response('Signed', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const fetchFn = createFetchFunction(dummyFetch); const inputUrl = 'http://example.com'; const jsonBody = { field: 'value' }; await fetchFn(inputUrl, { method: 'POST', body: jsonBody as unknown as BodyInit, headers: {}, }); expect(dummyFetch).toHaveBeenCalled(); const calledInit = dummyFetch.mock.calls[0][1] as RequestInit; // The body should be stringified. expect(calledInit.body).toEqual(JSON.stringify(jsonBody)); }); it('should handle Uint8Array body', async () => { const dummyResponse = new Response('Signed', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const fetchFn = createFetchFunction(dummyFetch); const inputUrl = 'http://example.com'; const uint8Body = new TextEncoder().encode('binaryTest'); await fetchFn(inputUrl, { method: 'POST', body: uint8Body, headers: {}, }); expect(dummyFetch).toHaveBeenCalled(); const calledInit = dummyFetch.mock.calls[0][1] as RequestInit; // The Uint8Array body should have been decoded to a string. expect(calledInit.body).toEqual('binaryTest'); }); it('should handle ArrayBuffer body', async () => { const dummyResponse = new Response('Signed', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const fetchFn = createFetchFunction(dummyFetch); const inputUrl = 'http://example.com'; const text = 'bufferTest'; const buffer = new TextEncoder().encode(text).buffer; await fetchFn(inputUrl, { method: 'POST', body: buffer, headers: {}, }); expect(dummyFetch).toHaveBeenCalled(); const calledInit = dummyFetch.mock.calls[0][1] as RequestInit; expect(calledInit.body).toEqual(text); }); it('should extract headers from a Headers instance', async () => { const dummyResponse = new Response('Signed', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const fetchFn = createFetchFunction(dummyFetch); const h = new Headers(); h.set('A', 'value-a'); h.set('B', 'value-b'); await fetchFn('http://example.com', { method: 'POST', body: '{"test": "data"}', headers: h, }); expect(dummyFetch).toHaveBeenCalled(); const calledInit = dummyFetch.mock.calls[0][1] as RequestInit; const headers = calledInit.headers as Record<string, string>; expect(headers['a'] || headers['A']).toEqual('value-a'); expect(headers['b'] || headers['B']).toEqual('value-b'); }); it('should handle headers provided as an array', async () => { const dummyResponse = new Response('Signed', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const fetchFn = createFetchFunction(dummyFetch); const headersArray: [string, string][] = [ ['Array-Header', 'array-value'], ['Another-Header', 'another-value'], ]; await fetchFn('http://example.com', { method: 'POST', body: '{"test": "data"}', headers: headersArray, }); expect(dummyFetch).toHaveBeenCalled(); const calledInit = dummyFetch.mock.calls[0][1] as RequestInit; const headers = calledInit.headers as Record<string, string>; expect(headers['array-header'] || headers['Array-Header']).toEqual( 'array-value', ); expect(headers['another-header'] || headers['Another-Header']).toEqual( 'another-value', ); // Also check that the signing headers are included. expect(headers['x-amz-date']).toEqual('20240315T000000Z'); expect(headers['authorization']).toEqual( 'AWS4-HMAC-SHA256 Credential=test', ); }); it('should call original fetch if init is undefined', async () => { const dummyResponse = new Response('OK', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const fetchFn = createFetchFunction(dummyFetch); const response = await fetchFn('http://example.com'); expect(dummyFetch).toHaveBeenCalledWith('http://example.com', undefined); expect(response).toBe(dummyResponse); }); it('should correctly handle async credential providers', async () => { const dummyResponse = new Response('Signed', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); // Create a function that returns a Promise of credentials const asyncCredentialsProvider = () => Promise.resolve({ region: 'us-east-1', accessKeyId: 'async-access-key', secretAccessKey: 'async-secret-key', sessionToken: 'async-session-token', }); const fetchFn = createSigV4FetchFunction( asyncCredentialsProvider, dummyFetch, ); await fetchFn('http://example.com', { method: 'POST', body: '{"test": "async"}', headers: { 'Content-Type': 'application/json', }, }); // Verify the request was properly signed expect(dummyFetch).toHaveBeenCalled(); const calledInit = dummyFetch.mock.calls[0][1] as RequestInit; const headers = calledInit.headers as Record<string, string>; // Check that the signing headers were added expect(headers['x-amz-date']).toEqual('20240315T000000Z'); expect(headers['authorization']).toEqual( 'AWS4-HMAC-SHA256 Credential=test', ); expect(headers['x-amz-security-token']).toEqual('async-session-token'); expect(headers['content-type']).toEqual('application/json'); }); it('should handle async credential providers that reject', async () => { const dummyFetch = vi.fn(); const errorMessage = 'Failed to get credentials'; // Create a function that returns a rejected Promise const failingCredentialsProvider = () => Promise.reject(new Error(errorMessage)); const fetchFn = createSigV4FetchFunction( failingCredentialsProvider, dummyFetch, ); // The fetch call should propagate the rejection await expect( fetchFn('http://example.com', { method: 'POST', body: '{"test": "data"}', }), ).rejects.toThrow(errorMessage); // The underlying fetch should not be called expect(dummyFetch).not.toHaveBeenCalled(); }); }); describe('createApiKeyFetchFunction', () => { afterEach(() => { vi.restoreAllMocks(); }); it('should add Authorization header with Bearer token', async () => { const dummyResponse = new Response('OK', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const apiKey = 'test-api-key-123'; const fetchFn = createApiKeyFetchFunction(apiKey, dummyFetch); const response = await fetchFn('http://example.com', { method: 'POST', body: '{"test": "data"}', headers: { 'Content-Type': 'application/json', }, }); expect(dummyFetch).toHaveBeenCalledWith('http://example.com', { method: 'POST', body: '{"test": "data"}', headers: { 'content-type': 'application/json', Authorization: 'Bearer test-api-key-123', }, }); expect(response).toBe(dummyResponse); }); it('should merge Authorization header with existing headers', async () => { const dummyResponse = new Response('OK', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const apiKey = 'test-api-key-456'; const fetchFn = createApiKeyFetchFunction(apiKey, dummyFetch); await fetchFn('http://example.com', { method: 'POST', body: '{"test": "data"}', headers: { 'Content-Type': 'application/json', 'Custom-Header': 'custom-value', 'X-Request-ID': 'req-123', }, }); expect(dummyFetch).toHaveBeenCalledWith('http://example.com', { method: 'POST', body: '{"test": "data"}', headers: { 'content-type': 'application/json', 'custom-header': 'custom-value', 'x-request-id': 'req-123', Authorization: 'Bearer test-api-key-456', }, }); }); it('should work with Headers instance', async () => { const dummyResponse = new Response('OK', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const apiKey = 'test-api-key-789'; const fetchFn = createApiKeyFetchFunction(apiKey, dummyFetch); const headers = new Headers(); headers.set('Content-Type', 'application/json'); headers.set('X-Custom', 'value'); await fetchFn('http://example.com', { method: 'POST', body: '{"test": "data"}', headers, }); expect(dummyFetch).toHaveBeenCalledWith('http://example.com', { method: 'POST', body: '{"test": "data"}', headers: { 'content-type': 'application/json', 'x-custom': 'value', Authorization: 'Bearer test-api-key-789', }, }); }); it('should work with headers as array', async () => { const dummyResponse = new Response('OK', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const apiKey = 'test-api-key-array'; const fetchFn = createApiKeyFetchFunction(apiKey, dummyFetch); const headersArray: [string, string][] = [ ['Content-Type', 'application/json'], ['X-Array-Header', 'array-value'], ]; await fetchFn('http://example.com', { method: 'POST', body: '{"test": "data"}', headers: headersArray, }); expect(dummyFetch).toHaveBeenCalledWith('http://example.com', { method: 'POST', body: '{"test": "data"}', headers: { 'content-type': 'application/json', 'x-array-header': 'array-value', Authorization: 'Bearer test-api-key-array', }, }); }); it('should work with GET requests', async () => { const dummyResponse = new Response('OK', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const apiKey = 'test-api-key-get'; const fetchFn = createApiKeyFetchFunction(apiKey, dummyFetch); await fetchFn('http://example.com', { method: 'GET', headers: { Accept: 'application/json', }, }); expect(dummyFetch).toHaveBeenCalledWith('http://example.com', { method: 'GET', headers: { accept: 'application/json', Authorization: 'Bearer test-api-key-get', }, }); }); it('should work when no headers are provided', async () => { const dummyResponse = new Response('OK', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const apiKey = 'test-api-key-no-headers'; const fetchFn = createApiKeyFetchFunction(apiKey, dummyFetch); await fetchFn('http://example.com', { method: 'POST', body: '{"test": "data"}', }); expect(dummyFetch).toHaveBeenCalledWith('http://example.com', { method: 'POST', body: '{"test": "data"}', headers: { Authorization: 'Bearer test-api-key-no-headers', }, }); }); it('should work when init is undefined', async () => { const dummyResponse = new Response('OK', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const apiKey = 'test-api-key-undefined'; const fetchFn = createApiKeyFetchFunction(apiKey, dummyFetch); await fetchFn('http://example.com'); expect(dummyFetch).toHaveBeenCalledWith('http://example.com', { headers: { Authorization: 'Bearer test-api-key-undefined', }, }); }); it('should override existing Authorization header', async () => { const dummyResponse = new Response('OK', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const apiKey = 'test-api-key-override'; const fetchFn = createApiKeyFetchFunction(apiKey, dummyFetch); await fetchFn('http://example.com', { method: 'POST', body: '{"test": "data"}', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer old-token', }, }); expect(dummyFetch).toHaveBeenCalledWith('http://example.com', { method: 'POST', body: '{"test": "data"}', headers: { 'content-type': 'application/json', Authorization: 'Bearer test-api-key-override', authorization: 'Bearer old-token', }, }); }); it('should use default fetch when no custom fetch provided', async () => { const originalFetch = globalThis.fetch; const mockGlobalFetch = vi.fn().mockResolvedValue(new Response('OK')); globalThis.fetch = mockGlobalFetch; try { const apiKey = 'test-api-key-default'; const fetchFn = createApiKeyFetchFunction(apiKey); await fetchFn('http://example.com', { method: 'POST', body: '{"test": "data"}', }); expect(mockGlobalFetch).toHaveBeenCalledWith('http://example.com', { method: 'POST', body: '{"test": "data"}', headers: { Authorization: 'Bearer test-api-key-default', }, }); } finally { globalThis.fetch = originalFetch; } }); it('should handle empty string API key', async () => { const dummyResponse = new Response('OK', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const apiKey = ''; const fetchFn = createApiKeyFetchFunction(apiKey, dummyFetch); await fetchFn('http://example.com', { method: 'POST', body: '{"test": "data"}', }); expect(dummyFetch).toHaveBeenCalledWith('http://example.com', { method: 'POST', body: '{"test": "data"}', headers: { Authorization: 'Bearer ', }, }); }); it('should preserve request body and other properties', async () => { const dummyResponse = new Response('OK', { status: 200 }); const dummyFetch = vi.fn().mockResolvedValue(dummyResponse); const apiKey = 'test-api-key-preserve'; const fetchFn = createApiKeyFetchFunction(apiKey, dummyFetch); const requestBody = JSON.stringify({ data: 'test' }); await fetchFn('http://example.com', { method: 'PUT', body: requestBody, headers: { 'Content-Type': 'application/json', }, credentials: 'include', cache: 'no-cache', }); expect(dummyFetch).toHaveBeenCalledWith('http://example.com', { method: 'PUT', body: requestBody, headers: { 'content-type': 'application/json', Authorization: 'Bearer test-api-key-preserve', }, credentials: 'include', cache: 'no-cache', }); }); }); --- File: /ai/packages/amazon-bedrock/src/bedrock-sigv4-fetch.ts --- import { convertHeadersToRecord, extractHeaders } from './headers-utils'; import { FetchFunction, combineHeaders, removeUndefinedEntries, } from '@ai-sdk/provider-utils'; import { AwsV4Signer } from 'aws4fetch'; export interface BedrockCredentials { region: string; accessKeyId: string; secretAccessKey: string; sessionToken?: string; } /** Creates a fetch function that applies AWS Signature Version 4 signing. @param getCredentials - Function that returns the AWS credentials to use when signing. @param fetch - Optional original fetch implementation to wrap. Defaults to global fetch. @returns A FetchFunction that signs requests before passing them to the underlying fetch. */ export function createSigV4FetchFunction( getCredentials: () => BedrockCredentials | PromiseLike<BedrockCredentials>, fetch: FetchFunction = globalThis.fetch, ): FetchFunction { return async ( input: RequestInfo | URL, init?: RequestInit, ): Promise<Response> => { if (init?.method?.toUpperCase() !== 'POST' || !init?.body) { return fetch(input, init); } const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url; const originalHeaders = extractHeaders(init.headers); const body = prepareBodyString(init.body); const credentials = await getCredentials(); const signer = new AwsV4Signer({ url, method: 'POST', headers: Object.entries(removeUndefinedEntries(originalHeaders)), body, region: credentials.region, accessKeyId: credentials.accessKeyId, secretAccessKey: credentials.secretAccessKey, sessionToken: credentials.sessionToken, service: 'bedrock', }); const signingResult = await signer.sign(); const signedHeaders = convertHeadersToRecord(signingResult.headers); return fetch(input, { ...init, body, headers: removeUndefinedEntries( combineHeaders(originalHeaders, signedHeaders), ), }); }; } function prepareBodyString(body: BodyInit | undefined): string { if (typeof body === 'string') { return body; } else if (body instanceof Uint8Array) { return new TextDecoder().decode(body); } else if (body instanceof ArrayBuffer) { return new TextDecoder().decode(new Uint8Array(body)); } else { return JSON.stringify(body); } } /** Creates a fetch function that applies Bearer token authentication. @param apiKey - The API key to use for Bearer token authentication. @param fetch - Optional original fetch implementation to wrap. Defaults to global fetch. @returns A FetchFunction that adds Authorization header with Bearer token to requests. */ export function createApiKeyFetchFunction( apiKey: string, fetch: FetchFunction = globalThis.fetch, ): FetchFunction { return async ( input: RequestInfo | URL, init?: RequestInit, ): Promise<Response> => { const originalHeaders = extractHeaders(init?.headers); return fetch(input, { ...init, headers: removeUndefinedEntries( combineHeaders(originalHeaders, { Authorization: `Bearer ${apiKey}`, }), ), }); }; } --- File: /ai/packages/amazon-bedrock/src/convert-to-bedrock-chat-messages.test.ts --- import { BedrockReasoningMetadata } from './bedrock-chat-language-model'; import { convertToBedrockChatMessages } from './convert-to-bedrock-chat-messages'; describe('system messages', () => { it('should combine multiple leading system messages into a single system message', async () => { const { system } = await convertToBedrockChatMessages([ { role: 'system', content: 'Hello' }, { role: 'system', content: 'World' }, ]); expect(system).toEqual([{ text: 'Hello' }, { text: 'World' }]); }); it('should throw an error if a system message is provided after a non-system message', async () => { await expect( convertToBedrockChatMessages([ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, { role: 'system', content: 'World' }, ]), ).rejects.toThrowError(); }); it('should set isSystemCachePoint when system message has cache point', async () => { const result = await convertToBedrockChatMessages([ { role: 'system', content: 'Hello', providerOptions: { bedrock: { cachePoint: { type: 'default' } } }, }, ]); expect(result).toEqual({ system: [{ text: 'Hello' }, { cachePoint: { type: 'default' } }], messages: [], }); }); }); describe('user messages', () => { it('should convert messages with image parts', async () => { const imageData = new Uint8Array([0, 1, 2, 3]); const { messages } = await convertToBedrockChatMessages([ { role: 'user', content: [ { type: 'text', text: 'Hello' }, { type: 'file', data: Buffer.from(imageData).toString('base64'), mediaType: 'image/png', }, ], }, ]); expect(messages).toEqual([ { role: 'user', content: [ { text: 'Hello' }, { image: { format: 'png', source: { bytes: 'AAECAw==' }, }, }, ], }, ]); }); it('should convert messages with document parts', async () => { const fileData = new Uint8Array([0, 1, 2, 3]); const { messages } = await convertToBedrockChatMessages([ { role: 'user', content: [ { type: 'text', text: 'Hello' }, { type: 'file', data: Buffer.from(fileData).toString('base64'), mediaType: 'application/pdf', }, ], }, ]); expect(messages).toMatchInlineSnapshot(` [ { "content": [ { "text": "Hello", }, { "document": { "format": "pdf", "name": "document-1", "source": { "bytes": "AAECAw==", }, }, }, ], "role": "user", }, ] `); }); it('should use consistent document names for prompt cache effectiveness', async () => { const fileData1 = new Uint8Array([0, 1, 2, 3]); const fileData2 = new Uint8Array([4, 5, 6, 7]); const { messages } = await convertToBedrockChatMessages([ { role: 'user', content: [ { type: 'file', data: Buffer.from(fileData1).toString('base64'), mediaType: 'application/pdf', }, { type: 'file', data: Buffer.from(fileData2).toString('base64'), mediaType: 'application/pdf', }, ], }, { role: 'assistant', content: [{ type: 'text', text: 'OK' }], }, { role: 'user', content: [ { type: 'file', data: Buffer.from(fileData1).toString('base64'), mediaType: 'application/pdf', }, ], }, ]); expect(messages).toMatchInlineSnapshot(` [ { "content": [ { "document": { "format": "pdf", "name": "document-1", "source": { "bytes": "AAECAw==", }, }, }, { "document": { "format": "pdf", "name": "document-2", "source": { "bytes": "BAUGBw==", }, }, }, ], "role": "user", }, { "content": [ { "text": "OK", }, ], "role": "assistant", }, { "content": [ { "document": { "format": "pdf", "name": "document-3", "source": { "bytes": "AAECAw==", }, }, }, ], "role": "user", }, ] `); }); it('should extract the system message', async () => { const { system } = await convertToBedrockChatMessages([ { role: 'system', content: 'Hello', }, ]); expect(system).toEqual([{ text: 'Hello' }]); }); it('should add cache point to user message content when specified', async () => { const result = await convertToBedrockChatMessages([ { role: 'user', content: [{ type: 'text', text: 'Hello' }], providerOptions: { bedrock: { cachePoint: { type: 'default' } } }, }, ]); expect(result).toEqual({ messages: [ { role: 'user', content: [{ text: 'Hello' }, { cachePoint: { type: 'default' } }], }, ], system: [], }); }); }); describe('assistant messages', () => { it('should remove trailing whitespace from last assistant message when there is no further user message', async () => { const result = await convertToBedrockChatMessages([ { role: 'user', content: [{ type: 'text', text: 'user content' }], }, { role: 'assistant', content: [{ type: 'text', text: 'assistant content ' }], }, ]); expect(result).toEqual({ messages: [ { role: 'user', content: [{ text: 'user content' }], }, { role: 'assistant', content: [{ text: 'assistant content' }], }, ], system: [], }); }); it('should remove trailing whitespace from last assistant message with multi-part content when there is no further user message', async () => { const result = await convertToBedrockChatMessages([ { role: 'user', content: [{ type: 'text', text: 'user content' }], }, { role: 'assistant', content: [ { type: 'text', text: 'assistant ' }, { type: 'text', text: 'content ' }, ], }, ]); expect(result).toEqual({ messages: [ { role: 'user', content: [{ text: 'user content' }], }, { role: 'assistant', content: [{ text: 'assistant ' }, { text: 'content' }], }, ], system: [], }); }); it('should keep trailing whitespace from assistant message when there is a further user message', async () => { const result = await convertToBedrockChatMessages([ { role: 'user', content: [{ type: 'text', text: 'user content' }], }, { role: 'assistant', content: [{ type: 'text', text: 'assistant content ' }], }, { role: 'user', content: [{ type: 'text', text: 'user content 2' }], }, ]); expect(result).toEqual({ messages: [ { role: 'user', content: [{ text: 'user content' }], }, { role: 'assistant', content: [{ text: 'assistant content ' }], }, { role: 'user', content: [{ text: 'user content 2' }], }, ], system: [], }); }); it('should combine multiple sequential assistant messages into a single message', async () => { const result = await convertToBedrockChatMessages([ { role: 'user', content: [{ type: 'text', text: 'Hi!' }] }, { role: 'assistant', content: [{ type: 'text', text: 'Hello' }] }, { role: 'assistant', content: [{ type: 'text', text: 'World' }] }, { role: 'assistant', content: [{ type: 'text', text: '!' }] }, ]); expect(result).toEqual({ messages: [ { role: 'user', content: [{ text: 'Hi!' }] }, { role: 'assistant', content: [{ text: 'Hello' }, { text: 'World' }, { text: '!' }], }, ], system: [], }); }); it('should add cache point to assistant message content when specified', async () => { const result = await convertToBedrockChatMessages([ { role: 'assistant', content: [{ type: 'text', text: 'Hello' }], providerOptions: { bedrock: { cachePoint: { type: 'default' } } }, }, ]); expect(result).toEqual({ messages: [ { role: 'assistant', content: [{ text: 'Hello' }, { cachePoint: { type: 'default' } }], }, ], system: [], }); }); it('should properly convert reasoning content type', async () => { const result = await convertToBedrockChatMessages([ { role: 'user', content: [{ type: 'text', text: 'Explain your reasoning' }], }, { role: 'assistant', content: [ { type: 'reasoning', text: 'This is my step-by-step reasoning process', providerOptions: { bedrock: { signature: 'test-signature', } satisfies BedrockReasoningMetadata, }, }, ], }, ]); expect(result).toEqual({ messages: [ { role: 'user', content: [{ text: 'Explain your reasoning' }], }, { role: 'assistant', content: [ { reasoningContent: { reasoningText: { text: 'This is my step-by-step reasoning process', signature: 'test-signature', }, }, }, ], }, ], system: [], }); }); it('should properly convert redacted-reasoning content type', async () => { const reasoningData = 'Redacted reasoning information'; const result = await convertToBedrockChatMessages([ { role: 'user', content: [{ type: 'text', text: 'Explain your reasoning' }], }, { role: 'assistant', content: [ { type: 'reasoning', text: '', providerOptions: { bedrock: { redactedData: reasoningData } }, }, ], }, ]); expect(result).toEqual({ messages: [ { role: 'user', content: [{ text: 'Explain your reasoning' }], }, { role: 'assistant', content: [ { reasoningContent: { redactedReasoning: { data: reasoningData, }, }, }, ], }, ], system: [], }); }); it('should trim trailing whitespace from reasoning content when it is the last part', async () => { const result = await convertToBedrockChatMessages([ { role: 'user', content: [{ type: 'text', text: 'Explain your reasoning' }], }, { role: 'assistant', content: [ { type: 'reasoning', text: 'This is my reasoning with trailing space ', providerOptions: { bedrock: { signature: 'test-signature', } satisfies BedrockReasoningMetadata, }, }, ], }, ]); expect(result).toEqual({ messages: [ { role: 'user', content: [{ text: 'Explain your reasoning' }], }, { role: 'assistant', content: [ { reasoningContent: { reasoningText: { text: 'This is my reasoning with trailing space', signature: 'test-signature', }, }, }, ], }, ], system: [], }); }); it('should handle a mix of text and reasoning content types', async () => { const result = await convertToBedrockChatMessages([ { role: 'user', content: [{ type: 'text', text: 'Explain your reasoning' }], }, { role: 'assistant', content: [ { type: 'text', text: 'My answer is 42.' }, { type: 'reasoning', text: 'I calculated this by analyzing the meaning of life', providerOptions: { bedrock: { signature: 'reasoning-process', } satisfies BedrockReasoningMetadata, }, }, ], }, ]); expect(result).toEqual({ messages: [ { role: 'user', content: [{ text: 'Explain your reasoning' }], }, { role: 'assistant', content: [ { text: 'My answer is 42.' }, { reasoningContent: { reasoningText: { text: 'I calculated this by analyzing the meaning of life', signature: 'reasoning-process', }, }, }, ], }, ], system: [], }); }); }); describe('tool messages', () => { it('should convert tool result with content array containing text', async () => { const result = await convertToBedrockChatMessages([ { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'call-123', toolName: 'calculator', output: { type: 'content', value: [{ type: 'text', text: 'The result is 42' }], }, }, ], }, ]); expect(result.messages[0]).toEqual({ role: 'user', content: [ { toolResult: { toolUseId: 'call-123', content: [{ text: 'The result is 42' }], }, }, ], }); }); it('should convert tool result with content array containing image', async () => { const result = await convertToBedrockChatMessages([ { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'call-123', toolName: 'image-generator', output: { type: 'content', value: [ { type: 'media', data: 'base64data', mediaType: 'image/jpeg', }, ], }, }, ], }, ]); expect(result.messages[0]).toEqual({ role: 'user', content: [ { toolResult: { toolUseId: 'call-123', content: [ { image: { format: 'jpeg', source: { bytes: 'base64data' }, }, }, ], }, }, ], }); }); it('should throw error for unsupported image format in tool result content', async () => { await expect( convertToBedrockChatMessages([ { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'call-123', toolName: 'image-generator', output: { type: 'content', value: [ { type: 'media', data: 'base64data', mediaType: 'image/avif', // unsupported format }, ], }, }, ], }, ]), ).rejects.toThrowErrorMatchingInlineSnapshot( `[AI_UnsupportedFunctionalityError: Unsupported image mime type: image/avif, expected one of: image/jpeg, image/png, image/gif, image/webp]`, ); }); it('should throw error for unsupported mime type in tool result image content', async () => { await expect( convertToBedrockChatMessages([ { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'call-123', toolName: 'image-generator', output: { type: 'content', value: [ { type: 'media', data: 'base64data', mediaType: 'unsupported/mime-type', }, ], }, }, ], }, ]), ).rejects.toThrowErrorMatchingInlineSnapshot( `[AI_UnsupportedFunctionalityError: 'media type: unsupported/mime-type' functionality not supported.]`, ); }); it('should fallback to stringified result when content is undefined', async () => { const result = await convertToBedrockChatMessages([ { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'call-123', toolName: 'calculator', output: { type: 'json', value: { value: 42 } }, }, ], }, ]); expect(result.messages[0]).toEqual({ role: 'user', content: [ { toolResult: { toolUseId: 'call-123', content: [{ text: '{"value":42}' }], }, }, ], }); }); }); describe('additional file format tests', () => { it('should throw an error for unsupported file mime type in user message content', async () => { await expect( convertToBedrockChatMessages([ { role: 'user', content: [ { type: 'file', data: 'base64data', mediaType: 'application/rtf', }, ], }, ]), ).rejects.toThrowErrorMatchingInlineSnapshot( `[AI_UnsupportedFunctionalityError: Unsupported file mime type: application/rtf, expected one of: application/pdf, text/csv, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, text/html, text/plain, text/markdown]`, ); }); it('should handle xlsx files correctly', async () => { const result = await convertToBedrockChatMessages([ { role: 'user', content: [ { type: 'file', data: 'base64data', mediaType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }, ], }, ]); expect(result).toMatchInlineSnapshot(` { "messages": [ { "content": [ { "document": { "format": "xlsx", "name": "document-1", "source": { "bytes": "base64data", }, }, }, ], "role": "user", }, ], "system": [], } `); }); it('should handle docx files correctly', async () => { const result = await convertToBedrockChatMessages([ { role: 'user', content: [ { type: 'file', data: 'base64data', mediaType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }, ], }, ]); expect(result).toMatchInlineSnapshot(` { "messages": [ { "content": [ { "document": { "format": "docx", "name": "document-1", "source": { "bytes": "base64data", }, }, }, ], "role": "user", }, ], "system": [], } `); }); }); --- File: /ai/packages/amazon-bedrock/src/convert-to-bedrock-chat-messages.ts --- import { JSONObject, LanguageModelV2Message, LanguageModelV2Prompt, SharedV2ProviderMetadata, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { convertToBase64, parseProviderOptions } from '@ai-sdk/provider-utils'; import { BEDROCK_CACHE_POINT, BEDROCK_DOCUMENT_MIME_TYPES, BEDROCK_IMAGE_MIME_TYPES, BedrockAssistantMessage, BedrockCachePoint, BedrockDocumentFormat, BedrockDocumentMimeType, BedrockImageFormat, BedrockImageMimeType, BedrockMessages, BedrockSystemMessages, BedrockUserMessage, } from './bedrock-api-types'; import { bedrockReasoningMetadataSchema } from './bedrock-chat-language-model'; function getCachePoint( providerMetadata: SharedV2ProviderMetadata | undefined, ): BedrockCachePoint | undefined { return providerMetadata?.bedrock?.cachePoint as BedrockCachePoint | undefined; } export async function convertToBedrockChatMessages( prompt: LanguageModelV2Prompt, ): Promise<{ system: BedrockSystemMessages; messages: BedrockMessages; }> { const blocks = groupIntoBlocks(prompt); let system: BedrockSystemMessages = []; const messages: BedrockMessages = []; let documentCounter = 0; const generateDocumentName = () => `document-${++documentCounter}`; for (let i = 0; i < blocks.length; i++) { const block = blocks[i]; const isLastBlock = i === blocks.length - 1; const type = block.type; switch (type) { case 'system': { if (messages.length > 0) { throw new UnsupportedFunctionalityError({ functionality: 'Multiple system messages that are separated by user/assistant messages', }); } for (const message of block.messages) { system.push({ text: message.content }); if (getCachePoint(message.providerOptions)) { system.push(BEDROCK_CACHE_POINT); } } break; } case 'user': { // combines all user and tool messages in this block into a single message: const bedrockContent: BedrockUserMessage['content'] = []; for (const message of block.messages) { const { role, content, providerOptions } = message; switch (role) { case 'user': { for (let j = 0; j < content.length; j++) { const part = content[j]; switch (part.type) { case 'text': { bedrockContent.push({ text: part.text, }); break; } case 'file': { if (part.data instanceof URL) { // The AI SDK automatically downloads files for user file parts with URLs throw new UnsupportedFunctionalityError({ functionality: 'File URL data', }); } if (part.mediaType.startsWith('image/')) { bedrockContent.push({ image: { format: getBedrockImageFormat(part.mediaType), source: { bytes: convertToBase64(part.data) }, }, }); } else { if (!part.mediaType) { throw new UnsupportedFunctionalityError({ functionality: 'file without mime type', message: 'File mime type is required in user message part content', }); } bedrockContent.push({ document: { format: getBedrockDocumentFormat(part.mediaType), name: generateDocumentName(), source: { bytes: convertToBase64(part.data) }, }, }); } break; } } } break; } case 'tool': { for (const part of content) { let toolResultContent; const output = part.output; switch (output.type) { case 'content': { toolResultContent = output.value.map(contentPart => { switch (contentPart.type) { case 'text': return { text: contentPart.text }; case 'media': if (!contentPart.mediaType.startsWith('image/')) { throw new UnsupportedFunctionalityError({ functionality: `media type: ${contentPart.mediaType}`, }); } const format = getBedrockImageFormat( contentPart.mediaType, ); return { image: { format, source: { bytes: contentPart.data }, }, }; } }); break; } case 'text': case 'error-text': toolResultContent = [{ text: output.value }]; break; case 'json': case 'error-json': default: toolResultContent = [ { text: JSON.stringify(output.value) }, ]; break; } bedrockContent.push({ toolResult: { toolUseId: part.toolCallId, content: toolResultContent, }, }); } break; } default: { const _exhaustiveCheck: never = role; throw new Error(`Unsupported role: ${_exhaustiveCheck}`); } } if (getCachePoint(providerOptions)) { bedrockContent.push(BEDROCK_CACHE_POINT); } } messages.push({ role: 'user', content: bedrockContent }); break; } case 'assistant': { // combines multiple assistant messages in this block into a single message: const bedrockContent: BedrockAssistantMessage['content'] = []; for (let j = 0; j < block.messages.length; j++) { const message = block.messages[j]; const isLastMessage = j === block.messages.length - 1; const { content } = message; for (let k = 0; k < content.length; k++) { const part = content[k]; const isLastContentPart = k === content.length - 1; switch (part.type) { case 'text': { bedrockContent.push({ text: // trim the last text part if it's the last message in the block // because Bedrock does not allow trailing whitespace // in pre-filled assistant responses trimIfLast( isLastBlock, isLastMessage, isLastContentPart, part.text, ), }); break; } case 'reasoning': { const reasoningMetadata = await parseProviderOptions({ provider: 'bedrock', providerOptions: part.providerOptions, schema: bedrockReasoningMetadataSchema, }); if (reasoningMetadata != null) { if (reasoningMetadata.signature != null) { bedrockContent.push({ reasoningContent: { reasoningText: { // trim the last text part if it's the last message in the block // because Bedrock does not allow trailing whitespace // in pre-filled assistant responses text: trimIfLast( isLastBlock, isLastMessage, isLastContentPart, part.text, ), signature: reasoningMetadata.signature, }, }, }); } else if (reasoningMetadata.redactedData != null) { bedrockContent.push({ reasoningContent: { redactedReasoning: { data: reasoningMetadata.redactedData, }, }, }); } } break; } case 'tool-call': { bedrockContent.push({ toolUse: { toolUseId: part.toolCallId, name: part.toolName, input: part.input as JSONObject, }, }); break; } } } if (getCachePoint(message.providerOptions)) { bedrockContent.push(BEDROCK_CACHE_POINT); } } messages.push({ role: 'assistant', content: bedrockContent }); break; } default: { const _exhaustiveCheck: never = type; throw new Error(`Unsupported type: ${_exhaustiveCheck}`); } } } return { system, messages }; } function isBedrockImageFormat(format: string): format is BedrockImageFormat { return Object.values(BEDROCK_IMAGE_MIME_TYPES).includes( format as BedrockImageFormat, ); } function getBedrockImageFormat(mimeType?: string): BedrockImageFormat { if (!mimeType) { throw new UnsupportedFunctionalityError({ functionality: 'image without mime type', message: 'Image mime type is required in user message part content', }); } const format = BEDROCK_IMAGE_MIME_TYPES[mimeType as BedrockImageMimeType]; if (!format) { throw new UnsupportedFunctionalityError({ functionality: `image mime type: ${mimeType}`, message: `Unsupported image mime type: ${mimeType}, expected one of: ${Object.keys(BEDROCK_IMAGE_MIME_TYPES).join(', ')}`, }); } return format; } function getBedrockDocumentFormat(mimeType: string): BedrockDocumentFormat { const format = BEDROCK_DOCUMENT_MIME_TYPES[mimeType as BedrockDocumentMimeType]; if (!format) { throw new UnsupportedFunctionalityError({ functionality: `file mime type: ${mimeType}`, message: `Unsupported file mime type: ${mimeType}, expected one of: ${Object.keys(BEDROCK_DOCUMENT_MIME_TYPES).join(', ')}`, }); } return format; } function trimIfLast( isLastBlock: boolean, isLastMessage: boolean, isLastContentPart: boolean, text: string, ) { return isLastBlock && isLastMessage && isLastContentPart ? text.trim() : text; } type SystemBlock = { type: 'system'; messages: Array<LanguageModelV2Message & { role: 'system' }>; }; type AssistantBlock = { type: 'assistant'; messages: Array<LanguageModelV2Message & { role: 'assistant' }>; }; type UserBlock = { type: 'user'; messages: Array<LanguageModelV2Message & { role: 'user' | 'tool' }>; }; function groupIntoBlocks( prompt: LanguageModelV2Prompt, ): Array<SystemBlock | AssistantBlock | UserBlock> { const blocks: Array<SystemBlock | AssistantBlock | UserBlock> = []; let currentBlock: SystemBlock | AssistantBlock | UserBlock | undefined = undefined; for (const message of prompt) { const { role } = message; switch (role) { case 'system': { if (currentBlock?.type !== 'system') { currentBlock = { type: 'system', messages: [] }; blocks.push(currentBlock); } currentBlock.messages.push(message); break; } case 'assistant': { if (currentBlock?.type !== 'assistant') { currentBlock = { type: 'assistant', messages: [] }; blocks.push(currentBlock); } currentBlock.messages.push(message); break; } case 'user': { if (currentBlock?.type !== 'user') { currentBlock = { type: 'user', messages: [] }; blocks.push(currentBlock); } currentBlock.messages.push(message); break; } case 'tool': { if (currentBlock?.type !== 'user') { currentBlock = { type: 'user', messages: [] }; blocks.push(currentBlock); } currentBlock.messages.push(message); break; } default: { const _exhaustiveCheck: never = role; throw new Error(`Unsupported role: ${_exhaustiveCheck}`); } } } return blocks; } --- File: /ai/packages/amazon-bedrock/src/headers-utils.test.ts --- import { extractHeaders, convertHeadersToRecord } from './headers-utils'; describe('extractHeaders', () => { it('should handle undefined headers', () => { const result = extractHeaders(undefined); expect(result).toEqual({}); }); it('should handle Headers instance', () => { const headers = new Headers(); headers.append('Content-Type', 'application/json'); headers.append('X-Custom-Header', 'test-value'); const result = extractHeaders(headers); expect(result).toEqual({ 'content-type': 'application/json', 'x-custom-header': 'test-value', }); }); it('should handle array of header tuples', () => { const headers: [string, string][] = [ ['Content-Type', 'application/json'], ['X-Custom-Header', 'test-value'], ]; const result = extractHeaders(headers); expect(result).toEqual({ 'content-type': 'application/json', 'x-custom-header': 'test-value', }); }); it('should handle plain object headers', () => { const headers = { 'Content-Type': 'application/json', 'X-Custom-Header': 'test-value', }; const result = extractHeaders(headers); expect(result).toEqual({ 'content-type': 'application/json', 'x-custom-header': 'test-value', }); }); }); describe('convertHeadersToRecord', () => { it('should convert Headers to Record object', () => { const headers = new Headers(); headers.append('Content-Type', 'application/json'); headers.append('X-Custom-Header', 'test-value'); const result = convertHeadersToRecord(headers); expect(result).toEqual({ 'content-type': 'application/json', 'x-custom-header': 'test-value', }); }); it('should handle empty Headers', () => { const headers = new Headers(); const result = convertHeadersToRecord(headers); expect(result).toEqual({}); }); it('should convert headers to lowercase keys', () => { const headers = new Headers(); headers.append('CONTENT-TYPE', 'application/json'); headers.append('X-CUSTOM-HEADER', 'test-value'); const result = convertHeadersToRecord(headers); expect(result).toEqual({ 'content-type': 'application/json', 'x-custom-header': 'test-value', }); }); }); --- File: /ai/packages/amazon-bedrock/src/headers-utils.ts --- /** * Extract headers from a `HeadersInit` object and convert them to a record of * lowercase keys and (preserving original case) values. * @param headers - The `HeadersInit` object to extract headers from. * @returns A record of lowercase keys and (preserving original case) values. */ export function extractHeaders( headers: HeadersInit | undefined, ): Record<string, string | undefined> { let originalHeaders: Record<string, string | undefined> = {}; if (headers) { if (headers instanceof Headers) { originalHeaders = convertHeadersToRecord(headers); } else if (Array.isArray(headers)) { for (const [k, v] of headers) { originalHeaders[k.toLowerCase()] = v; } } else { originalHeaders = Object.fromEntries( Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]), ) as Record<string, string>; } } return originalHeaders; } /** * Convert a Headers object to a record of lowercase keys and (preserving * original case) values. * @param headers - The Headers object to convert. * @returns A record of lowercase keys and values. */ export function convertHeadersToRecord(headers: Headers) { return Object.fromEntries<string>([...headers]); } --- File: /ai/packages/amazon-bedrock/src/index.ts --- export { bedrock, createAmazonBedrock } from './bedrock-provider'; export type { AmazonBedrockProvider, AmazonBedrockProviderSettings, } from './bedrock-provider'; export type { BedrockProviderOptions } from './bedrock-chat-options'; --- File: /ai/packages/amazon-bedrock/src/inject-fetch-headers.test.ts --- import { injectFetchHeaders } from './inject-fetch-headers'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; describe('injectFetchHeaders', () => { const originalFetch = globalThis.fetch; beforeEach(() => { globalThis.fetch = vi.fn(); }); afterEach(() => { globalThis.fetch = originalFetch; vi.clearAllMocks(); }); it('should inject custom headers into fetch request', async () => { const mockFetch = vi.fn().mockResolvedValue('response'); globalThis.fetch = mockFetch; const customHeaders = { 'X-Custom-Header': 'custom-value', Authorization: 'Bearer token', }; const enhancedFetch = injectFetchHeaders(customHeaders); await enhancedFetch('https://api.example.com'); expect(mockFetch).toHaveBeenCalledWith( 'https://api.example.com', expect.objectContaining({ headers: customHeaders, }), ); }); it('should merge custom headers with existing headers', async () => { const mockFetch = vi.fn().mockResolvedValue('response'); globalThis.fetch = mockFetch; const customHeaders = { 'X-Custom-Header': 'custom-value', }; const existingHeaders = { 'Content-Type': 'application/json', }; const enhancedFetch = injectFetchHeaders(customHeaders); await enhancedFetch('https://api.example.com', { headers: existingHeaders, }); expect(mockFetch).toHaveBeenCalledWith( 'https://api.example.com', expect.objectContaining({ headers: { 'content-type': 'application/json', 'X-Custom-Header': 'custom-value', }, }), ); }); it('should handle undefined headers in init', async () => { const mockFetch = vi.fn().mockResolvedValue('response'); globalThis.fetch = mockFetch; const customHeaders = { 'X-Custom-Header': 'custom-value', }; const enhancedFetch = injectFetchHeaders(customHeaders); await enhancedFetch('https://api.example.com', { headers: undefined, }); expect(mockFetch).toHaveBeenCalledWith( 'https://api.example.com', expect.objectContaining({ headers: customHeaders, }), ); }); it('should handle Headers instance in init', async () => { const mockFetch = vi.fn().mockResolvedValue('response'); globalThis.fetch = mockFetch; const customHeaders = { 'X-Custom-Header': 'custom-value', }; const existingHeaders = new Headers({ 'Content-Type': 'application/json', }); const enhancedFetch = injectFetchHeaders(customHeaders); await enhancedFetch('https://api.example.com', { headers: existingHeaders, }); expect(mockFetch).toHaveBeenCalledWith( 'https://api.example.com', expect.objectContaining({ headers: { 'content-type': 'application/json', 'X-Custom-Header': 'custom-value', }, }), ); }); }); --- File: /ai/packages/amazon-bedrock/src/inject-fetch-headers.ts --- import { extractHeaders } from './headers-utils'; import { FetchFunction, removeUndefinedEntries } from '@ai-sdk/provider-utils'; /** * Test helper to inject custom headers into a fetch request. * @param customHeaders - The headers to inject. * @returns A fetch function that injects the custom headers. */ export function injectFetchHeaders( customHeaders: Record<string, string>, ): FetchFunction { return async (input, init = {}) => await globalThis.fetch(input, { ...init, headers: removeUndefinedEntries({ ...extractHeaders(init.headers), ...customHeaders, }), }); } --- File: /ai/packages/amazon-bedrock/src/map-bedrock-finish-reason.ts --- import { LanguageModelV2FinishReason } from '@ai-sdk/provider'; import { BedrockStopReason } from './bedrock-api-types'; export function mapBedrockFinishReason( finishReason?: BedrockStopReason, ): LanguageModelV2FinishReason { switch (finishReason) { case 'stop_sequence': case 'end_turn': return 'stop'; case 'max_tokens': return 'length'; case 'content_filtered': case 'guardrail_intervened': return 'content-filter'; case 'tool_use': return 'tool-calls'; default: return 'unknown'; } } --- File: /ai/packages/amazon-bedrock/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/amazon-bedrock/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/amazon-bedrock/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/angular/src/lib/chat.ng.test.ts --- import { createTestServer, mockId, TestResponseController, } from '@ai-sdk/provider-utils/test'; import { DefaultChatTransport, isToolUIPart, TextStreamChatTransport, } from 'ai'; import { Chat } from './chat.ng'; function formatStreamPart(part: object) { return `data: ${JSON.stringify(part)}\n\n`; } function createFileList(...files: File[]): FileList { // file lists are really hard to create :( const input = document.createElement('input'); input.setAttribute('type', 'file'); input.setAttribute('name', 'file-upload'); input.multiple = true; const fileList: FileList = Object.create(input.files); for (let i = 0; i < files.length; i++) { fileList[i] = files[i]; } Object.defineProperty(fileList, 'length', { value: files.length }); return fileList; } const server = createTestServer({ '/api/chat': {}, }); describe('data protocol stream', () => { let chat: Chat; beforeEach(() => { chat = new Chat({ generateId: mockId(), }); }); it('should correctly manage streamed response in messages', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatStreamPart({ type: 'text-start', id: '0' }), formatStreamPart({ type: 'text-delta', id: '0', delta: 'Hello' }), formatStreamPart({ type: 'text-delta', id: '0', delta: ',' }), formatStreamPart({ type: 'text-delta', id: '0', delta: ' world' }), formatStreamPart({ type: 'text-delta', id: '0', delta: '.' }), formatStreamPart({ type: 'text-end', id: '0' }), ], }; await chat.sendMessage({ parts: [{ text: 'hi', type: 'text' }], }); expect(chat.messages.at(0)).toStrictEqual( expect.objectContaining({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }), ); expect(chat.messages.at(1)).toStrictEqual( expect.objectContaining({ role: 'assistant', parts: [{ type: 'text', text: 'Hello, world.', state: 'done' }], }), ); }); it('should show error response when there is a server error', async () => { server.urls['/api/chat'].response = { type: 'error', status: 404, body: 'Not found', }; await chat.sendMessage({ text: 'hi', }); expect(chat.error).toBeInstanceOf(Error); expect(chat.error?.message).toBe('Not found'); }); it('should show error response when there is a streaming error', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatStreamPart({ type: 'error', errorText: 'custom error message', }), ], }; await chat.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); expect(chat.error).toBeInstanceOf(Error); expect(chat.error?.message).toBe('custom error message'); }); describe('status', () => { it('should show status', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; const appendOperation = chat.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); await vi.waitFor(() => expect(chat.status).toBe('submitted')); controller.write(formatStreamPart({ type: 'text-start', id: '0' })); controller.write( formatStreamPart({ type: 'text-delta', id: '0', delta: 'Hello' }), ); controller.write(formatStreamPart({ type: 'text-end', id: '0' })); await vi.waitFor(() => expect(chat.status).toBe('streaming')); controller.close(); await appendOperation; expect(chat.status).toBe('ready'); }); it('should set status to error when there is a server error', async () => { server.urls['/api/chat'].response = { type: 'error', status: 404, body: 'Not found', }; chat.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); await vi.waitFor(() => expect(chat.status).toBe('error')); }); }); it('should invoke onFinish when the stream finishes', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatStreamPart({ type: 'text-start', id: '0' }), formatStreamPart({ type: 'text-delta', id: '0', delta: 'Hello' }), formatStreamPart({ type: 'text-delta', id: '0', delta: ',' }), formatStreamPart({ type: 'text-delta', id: '0', delta: ' world' }), formatStreamPart({ type: 'text-delta', id: '0', delta: '.' }), formatStreamPart({ type: 'text-end', id: '0' }), formatStreamPart({ type: 'finish', messageMetadata: { example: 'metadata', }, }), ], }; const onFinish = vi.fn(); const chatWithOnFinish = new Chat({ onFinish, generateId: mockId(), }); await chatWithOnFinish.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); expect(onFinish).toHaveBeenCalledExactlyOnceWith({ message: { id: 'id-2', metadata: { example: 'metadata', }, parts: [ { text: 'Hello, world.', type: 'text', state: 'done', }, ], role: 'assistant', }, }); }); describe('id', () => { it('should send the id to the server', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: ['0:"Hello"\n', '0:","\n', '0:" world"\n', '0:"."\n'], }; await chat.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "text": "hi", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); }); }); describe('text stream', () => { let chat: Chat; beforeEach(() => { const generateId = mockId(); chat = new Chat({ generateId, transport: new TextStreamChatTransport({ api: '/api/chat', }), }); }); it('should show streamed response', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: ['Hello', ',', ' world', '.'], }; await chat.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); expect(chat.messages).toMatchInlineSnapshot(` [ { "id": "id-1", "metadata": undefined, "parts": [ { "text": "hi", "type": "text", }, ], "role": "user", }, { "id": "id-2", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Hello, world.", "type": "text", }, ], "role": "assistant", }, ] `); }); it('should have stable message ids', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; const appendOperation = chat.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); controller.write('He'); await vi.waitFor(() => expect(chat.messages.at(1)).toStrictEqual( expect.objectContaining({ id: expect.any(String), role: 'assistant', metadata: undefined, parts: [ { type: 'step-start' }, { text: 'He', type: 'text', state: 'streaming' }, ], }), ), ); const id = chat.messages.at(1)?.id; controller.write('llo'); controller.close(); await appendOperation; expect(id).toBeDefined(); expect(chat.messages.at(1)).toStrictEqual( expect.objectContaining({ id, role: 'assistant', }), ); }); it('should invoke onFinish when the stream finishes', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: ['Hello', ',', ' world', '.'], }; const onFinish = vi.fn(); const chatWithOnFinish = new Chat({ onFinish, transport: new TextStreamChatTransport({ api: '/api/chat', }), }); await chatWithOnFinish.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); expect(onFinish).toHaveBeenCalledExactlyOnceWith({ message: { id: expect.any(String), role: 'assistant', metadata: undefined, parts: [ { type: 'step-start' }, { text: 'Hello, world.', type: 'text', state: 'done' }, ], }, }); }); }); describe('onToolCall', () => { let resolve: () => void; let toolCallPromise: Promise<void>; let chat: Chat; function promiseWithResolvers<T>() { let resolve!: (v: T | PromiseLike<T>) => void; let reject!: (reason?: unknown) => void; const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; } beforeEach(() => { ({ resolve, promise: toolCallPromise } = promiseWithResolvers<void>()); chat = new Chat({ async onToolCall({ toolCall }) { await toolCallPromise; chat.addToolResult({ tool: 'test-tool', toolCallId: toolCall.toolCallId, output: `test-tool-response: ${toolCall.toolName} ${ toolCall.toolCallId } ${JSON.stringify(toolCall.input)}`, }); }, }); }); it("should invoke onToolCall when a tool call is received from the server's response", async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatStreamPart({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), ], }; const appendOperation = chat.sendMessage({ text: 'hi' }); await vi.waitFor(() => { expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'input-available', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: undefined, providerExecuted: undefined, }, ]); }); resolve(); await appendOperation; expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'output-available', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: 'test-tool-response: test-tool tool-call-0 {"testArg":"test-value"}', providerExecuted: undefined, }, ]); }); }); describe('tool invocations', () => { let chat: Chat; beforeEach(() => { const generateId = mockId(); chat = new Chat({ generateId, transport: new DefaultChatTransport({ api: '/api/chat', }), }); }); it('should display partial tool call, tool call, and tool result', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; const appendOperation = chat.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); controller.write( formatStreamPart({ type: 'tool-input-start', toolCallId: 'tool-call-0', toolName: 'test-tool', }), ); await vi.waitFor(() => { expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'input-streaming', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: undefined, output: undefined, providerExecuted: undefined, }, ]); }); controller.write( formatStreamPart({ type: 'tool-input-delta', toolCallId: 'tool-call-0', inputTextDelta: '{"testArg":"t', }), ); await vi.waitFor(() => { expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'input-streaming', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 't' }, output: undefined, providerExecuted: undefined, }, ]); }); controller.write( formatStreamPart({ type: 'tool-input-delta', toolCallId: 'tool-call-0', inputTextDelta: 'est-value"}}', }), ); await vi.waitFor(() => { expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'input-streaming', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: undefined, providerExecuted: undefined, }, ]); }); controller.write( formatStreamPart({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), ); await vi.waitFor(() => { expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'input-available', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: undefined, providerExecuted: undefined, }, ]); }); controller.write( formatStreamPart({ type: 'tool-output-available', toolCallId: 'tool-call-0', output: 'test-result', }), ); controller.close(); await appendOperation; expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'output-available', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: 'test-result', providerExecuted: undefined, }, ]); }); it('should display partial tool call and tool result (when there is no tool call streaming)', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; const appendOperation = chat.sendMessage({ text: 'hi' }); controller.write( formatStreamPart({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), ); await vi.waitFor(() => { expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'input-available', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: undefined, providerExecuted: undefined, }, ]); }); controller.write( formatStreamPart({ type: 'tool-output-available', toolCallId: 'tool-call-0', output: 'test-result', }), ); controller.close(); await appendOperation; expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'output-available', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: 'test-result', providerExecuted: undefined, }, ]); }); it('should update tool call to result when addToolResult is called', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatStreamPart({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), ], }; await chat.sendMessage({ text: 'hi', }); await vi.waitFor(() => { expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'input-available', rawInput: undefined, errorText: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: undefined, providerExecuted: undefined, }, ]); }); chat.addToolResult({ tool: 'test-tool', toolCallId: 'tool-call-0', output: 'test-result', }); await vi.waitFor(() => { expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'output-available', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: 'test-result', providerExecuted: undefined, }, ]); }); }); }); describe('file attachments with data url', () => { let chat: Chat; beforeEach(() => { chat = new Chat({ generateId: mockId(), }); }); it('should handle text file attachment and submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatStreamPart({ type: 'text-start', id: '0', }), formatStreamPart({ type: 'text-delta', id: '0', delta: 'Response to message with text attachment', }), formatStreamPart({ type: 'text-end', id: '0', }), ], }; await chat.sendMessage({ text: 'Message with text attachment', files: createFileList( new File(['test file content'], 'test.txt', { type: 'text/plain', }), ), }); expect(chat.messages).toMatchInlineSnapshot(` [ { "id": "id-1", "metadata": undefined, "parts": [ { "filename": "test.txt", "mediaType": "text/plain", "type": "file", "url": "data:text/plain;base64,dGVzdCBmaWxlIGNvbnRlbnQ=", }, { "text": "Message with text attachment", "type": "text", }, ], "role": "user", }, { "id": "id-2", "metadata": undefined, "parts": [ { "providerMetadata": undefined, "state": "done", "text": "Response to message with text attachment", "type": "text", }, ], "role": "assistant", }, ] `); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "filename": "test.txt", "mediaType": "text/plain", "type": "file", "url": "data:text/plain;base64,dGVzdCBmaWxlIGNvbnRlbnQ=", }, { "text": "Message with text attachment", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); it('should handle image file attachment and submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatStreamPart({ type: 'text-start', id: '0', }), formatStreamPart({ type: 'text-delta', id: '0', delta: 'Response to message with image attachment', }), formatStreamPart({ type: 'text-end', id: '0', }), ], }; await chat.sendMessage({ text: 'Message with image attachment', files: createFileList( new File(['test image content'], 'test.png', { type: 'image/png', }), ), }); expect(chat.messages).toMatchInlineSnapshot(` [ { "id": "id-1", "metadata": undefined, "parts": [ { "filename": "test.png", "mediaType": "image/png", "type": "file", "url": "data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50", }, { "text": "Message with image attachment", "type": "text", }, ], "role": "user", }, { "id": "id-2", "metadata": undefined, "parts": [ { "providerMetadata": undefined, "state": "done", "text": "Response to message with image attachment", "type": "text", }, ], "role": "assistant", }, ] `); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "filename": "test.png", "mediaType": "image/png", "type": "file", "url": "data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50", }, { "text": "Message with image attachment", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); }); describe('file attachments with url', () => { let chat: Chat; beforeEach(() => { chat = new Chat({ generateId: mockId(), }); }); it('should handle image file attachment and submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatStreamPart({ type: 'text-start', id: '0', }), formatStreamPart({ type: 'text-delta', id: '0', delta: 'Response to message with image attachment', }), formatStreamPart({ type: 'text-end', id: '0', }), ], }; await chat.sendMessage({ text: 'Message with image attachment', files: createFileList( new File(['test image content'], 'test.png', { type: 'image/png', }), ), }); expect(chat.messages).toMatchInlineSnapshot(` [ { "id": "id-1", "metadata": undefined, "parts": [ { "filename": "test.png", "mediaType": "image/png", "type": "file", "url": "data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50", }, { "text": "Message with image attachment", "type": "text", }, ], "role": "user", }, { "id": "id-2", "metadata": undefined, "parts": [ { "providerMetadata": undefined, "state": "done", "text": "Response to message with image attachment", "type": "text", }, ], "role": "assistant", }, ] `); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "filename": "test.png", "mediaType": "image/png", "type": "file", "url": "data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50", }, { "text": "Message with image attachment", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); }); describe('file attachments with empty text content', () => { let chat: Chat; beforeEach(() => { chat = new Chat({ generateId: mockId(), }); }); it('should handle image file attachment and submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatStreamPart({ type: 'text-start', id: '0', }), formatStreamPart({ type: 'text-delta', id: '0', delta: 'Response to message with image attachment', }), formatStreamPart({ type: 'text-end', id: '0', }), ], }; await chat.sendMessage({ files: createFileList( new File(['test image content'], 'test.png', { type: 'image/png', }), ), }); expect(chat.messages).toMatchInlineSnapshot(` [ { "id": "id-1", "metadata": undefined, "parts": [ { "filename": "test.png", "mediaType": "image/png", "type": "file", "url": "data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50", }, ], "role": "user", }, { "id": "id-2", "metadata": undefined, "parts": [ { "providerMetadata": undefined, "state": "done", "text": "Response to message with image attachment", "type": "text", }, ], "role": "assistant", }, ] `); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "filename": "test.png", "mediaType": "image/png", "type": "file", "url": "data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); }); describe('reload', () => { let chat: Chat; beforeEach(() => { chat = new Chat({ generateId: mockId(), }); }); it('should show streamed response', async () => { server.urls['/api/chat'].response = [ { type: 'stream-chunks', chunks: [ formatStreamPart({ type: 'text-start', id: '0' }), formatStreamPart({ type: 'text-delta', id: '0', delta: 'first response', }), formatStreamPart({ type: 'text-end', id: '0' }), ], }, { type: 'stream-chunks', chunks: [ formatStreamPart({ type: 'text-start', id: '0' }), formatStreamPart({ type: 'text-delta', id: '0', delta: 'second response', }), formatStreamPart({ type: 'text-end', id: '0' }), ], }, ]; await chat.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); expect(chat.messages.at(0)).toStrictEqual( expect.objectContaining({ role: 'user', }), ); expect(chat.messages.at(1)).toStrictEqual( expect.objectContaining({ role: 'assistant', parts: [{ text: 'first response', type: 'text', state: 'done' }], }), ); // Setup done, call regenerate: await chat.regenerate({ body: { 'request-body-key': 'request-body-value' }, headers: { 'header-key': 'header-value' }, }); expect(await server.calls[1].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "text": "hi", "type": "text", }, ], "role": "user", }, ], "request-body-key": "request-body-value", "trigger": "regenerate-message", } `); expect(server.calls[1].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'header-key': 'header-value', }); expect(chat.messages.at(1)).toStrictEqual( expect.objectContaining({ role: 'assistant', parts: [{ text: 'second response', type: 'text', state: 'done' }], }), ); }); }); describe('test sending additional fields during message submission', () => { let chat: Chat; beforeEach(() => { chat = new Chat({ generateId: mockId(), }); }); it('should send metadata with the message', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: ['0:"first response"\n'], }; await chat.sendMessage({ role: 'user', metadata: { test: 'example' }, parts: [{ text: 'hi', type: 'text' }], }); expect(chat.messages.at(0)).toStrictEqual( expect.objectContaining({ role: 'user', }), ); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "metadata": { "test": "example", }, "parts": [ { "text": "hi", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); }); describe('generateId function', () => { it('should use the provided generateId function for both user and assistant messages', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatStreamPart({ type: 'start', messageId: '123' }), formatStreamPart({ type: 'text-start', id: '0' }), formatStreamPart({ type: 'text-delta', id: '0', delta: 'Hello' }), formatStreamPart({ type: 'text-delta', id: '0', delta: ',' }), formatStreamPart({ type: 'text-delta', id: '0', delta: ' world' }), formatStreamPart({ type: 'text-delta', id: '0', delta: '.' }), formatStreamPart({ type: 'text-end', id: '0' }), ], }; const chatWithCustomId = new Chat({ generateId: mockId({ prefix: 'testid' }), }); await chatWithCustomId.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); expect(chatWithCustomId.messages).toMatchInlineSnapshot(` [ { "id": "testid-1", "metadata": undefined, "parts": [ { "text": "hi", "type": "text", }, ], "role": "user", }, { "id": "123", "metadata": undefined, "parts": [ { "providerMetadata": undefined, "state": "done", "text": "Hello, world.", "type": "text", }, ], "role": "assistant", }, ] `); }); }); --- File: /ai/packages/angular/src/lib/chat.ng.ts --- import { signal } from '@angular/core'; import { type ChatState, type ChatStatus, type UIMessage, type ChatInit, AbstractChat, } from 'ai'; export class Chat< UI_MESSAGE extends UIMessage = UIMessage, > extends AbstractChat<UI_MESSAGE> { constructor(init: ChatInit<UI_MESSAGE>) { super({ ...init, state: new AngularChatState(init.messages), }); } } class AngularChatState<UI_MESSAGE extends UIMessage = UIMessage> implements ChatState<UI_MESSAGE> { readonly #messages = signal<UI_MESSAGE[]>([]); readonly #status = signal<ChatStatus>('ready'); readonly #error = signal<Error | undefined>(undefined); get messages(): UI_MESSAGE[] { return this.#messages(); } set messages(messages: UI_MESSAGE[]) { this.#messages.set([...messages]); } get status(): ChatStatus { return this.#status(); } set status(status: ChatStatus) { this.#status.set(status); } get error(): Error | undefined { return this.#error(); } set error(error: Error | undefined) { this.#error.set(error); } constructor(initialMessages: UI_MESSAGE[] = []) { this.#messages.set([...initialMessages]); } setMessages = (messages: UI_MESSAGE[]) => { this.#messages.set([...messages]); }; pushMessage = (message: UI_MESSAGE) => { this.#messages.update(msgs => [...msgs, message]); }; popMessage = () => { this.#messages.update(msgs => msgs.slice(0, -1)); }; replaceMessage = (index: number, message: UI_MESSAGE) => { this.#messages.update(msgs => { const copy = [...msgs]; copy[index] = message; return copy; }); }; snapshot = <T>(thing: T): T => structuredClone(thing); } --- File: /ai/packages/angular/src/lib/completion.ng.test.ts --- import { createTestServer, TestResponseController, } from '@ai-sdk/provider-utils/test'; import { Completion } from './completion.ng'; import { beforeAll } from 'vitest'; function formatStreamPart(part: object) { return `data: ${JSON.stringify(part)}\n\n`; } const server = createTestServer({ '/api/completion': {}, }); describe('Completion', () => { beforeAll(() => { createTestServer({}); }); it('initialises', () => { const completion = new Completion(); expect(completion.api).toBe('/api/completion'); expect(completion.completion).toBe(''); expect(completion.input).toBe(''); expect(completion.error).toBeUndefined(); expect(completion.loading).toBe(false); expect(completion.id).toBeDefined(); }); it('should render a data stream', async () => { server.urls['/api/completion'].response = { type: 'stream-chunks', chunks: [ formatStreamPart({ type: 'text-start', id: '0' }), formatStreamPart({ type: 'text-delta', id: '0', delta: 'Hello' }), formatStreamPart({ type: 'text-delta', id: '0', delta: ',' }), formatStreamPart({ type: 'text-delta', id: '0', delta: ' world' }), formatStreamPart({ type: 'text-delta', id: '0', delta: '.' }), formatStreamPart({ type: 'text-end', id: '0' }), ], }; const completion = new Completion({ api: '/api/completion', }); await completion.complete('hi'); expect(completion.completion).toBe('Hello, world.'); }); it('should render a text stream', async () => { server.urls['/api/completion'].response = { type: 'stream-chunks', chunks: ['Hello', ',', ' world', '.'], }; const completion = new Completion({ streamProtocol: 'text' }); await completion.complete('hi'); expect(completion.completion).toBe('Hello, world.'); }); it('should call `onFinish` callback', async () => { server.urls['/api/completion'].response = { type: 'stream-chunks', chunks: [ formatStreamPart({ type: 'text-start', id: '0' }), formatStreamPart({ type: 'text-delta', id: '0', delta: 'Hello' }), formatStreamPart({ type: 'text-delta', id: '0', delta: ',' }), formatStreamPart({ type: 'text-delta', id: '0', delta: ' world' }), formatStreamPart({ type: 'text-delta', id: '0', delta: '.' }), formatStreamPart({ type: 'text-end', id: '0' }), ], }; const onFinish = vi.fn(); const completion = new Completion({ onFinish }); await completion.complete('hi'); expect(onFinish).toHaveBeenCalledExactlyOnceWith('hi', 'Hello, world.'); }); it('should show loading state', async () => { const controller = new TestResponseController(); server.urls['/api/completion'].response = { type: 'controlled-stream', controller, }; const completion = new Completion(); const completionOperation = completion.complete('hi'); controller.write('0:"Hello"\n'); await vi.waitFor(() => expect(completion.loading).toBe(true)); controller.close(); await completionOperation; expect(completion.loading).toBe(false); }); describe('stop', () => { it('should abort the stream and not consume any more data', async () => { const controller = new TestResponseController(); server.urls['/api/completion'].response = { type: 'controlled-stream', controller, }; const completion = new Completion(); const completionOperation = completion.complete('hi'); controller.write( formatStreamPart({ type: 'text-delta', id: '0', delta: 'Hello' }), ); await vi.waitFor(() => { expect(completion.loading).toBe(true); expect(completion.completion).toBe('Hello'); }); completion.stop(); await vi.waitFor(() => expect(completion.loading).toBe(false)); await expect(controller.write('0:", world"\n')).rejects.toThrow(); await expect(controller.close()).rejects.toThrow(); await completionOperation; expect(completion.loading).toBe(false); expect(completion.completion).toBe('Hello'); }); }); it('should reset loading state on error', async () => { server.urls['/api/completion'].response = { type: 'error', status: 404, body: 'Not found', }; const completion = new Completion(); await completion.complete('hi'); expect(completion.error).toBeInstanceOf(Error); expect(completion.loading).toBe(false); }); it('should reset error state on subsequent completion', async () => { server.urls['/api/completion'].response = [ { type: 'error', status: 404, body: 'Not found', }, { type: 'stream-chunks', chunks: [ formatStreamPart({ type: 'text-start', id: '0' }), formatStreamPart({ type: 'text-delta', id: '0', delta: 'Hello' }), formatStreamPart({ type: 'text-delta', id: '0', delta: ',' }), formatStreamPart({ type: 'text-delta', id: '0', delta: ' world' }), formatStreamPart({ type: 'text-delta', id: '0', delta: '.' }), formatStreamPart({ type: 'text-end', id: '0' }), ], }, ]; const completion = new Completion(); await completion.complete('hi'); expect(completion.error).toBeInstanceOf(Error); expect(completion.loading).toBe(false); await completion.complete('hi'); expect(completion.error).toBe(undefined); expect(completion.completion).toBe('Hello, world.'); }); }); --- File: /ai/packages/angular/src/lib/completion.ng.ts --- import { signal } from '@angular/core'; import { callCompletionApi, generateId, type CompletionRequestOptions, type UseCompletionOptions, } from 'ai'; export type CompletionOptions = Readonly<UseCompletionOptions>; export class Completion { readonly #options: CompletionOptions; // Static config readonly id: string; readonly api: string; readonly streamProtocol: 'data' | 'text'; // Reactive state readonly #input = signal(''); readonly #completion = signal<string>(''); readonly #error = signal<Error | undefined>(undefined); readonly #loading = signal<boolean>(false); #abortController: AbortController | null = null; constructor(options: CompletionOptions = {}) { this.#options = options; this.#completion.set(options.initialCompletion ?? ''); this.#input.set(options.initialInput ?? ''); this.api = options.api ?? '/api/completion'; this.id = options.id ?? generateId(); this.streamProtocol = options.streamProtocol ?? 'data'; } /** Current value of the completion. Writable. */ get completion(): string { return this.#completion(); } set completion(value: string) { this.#completion.set(value); } /** Current value of the input. Writable. */ get input(): string { return this.#input(); } set input(value: string) { this.#input.set(value); } /** The error object of the API request */ get error(): Error | undefined { return this.#error(); } /** Flag that indicates whether an API request is in progress. */ get loading(): boolean { return this.#loading(); } /** Abort the current request immediately, keep the generated tokens if any. */ stop = () => { try { this.#abortController?.abort(); } catch { // ignore } finally { this.#loading.set(false); this.#abortController = null; } }; /** Send a new prompt to the API endpoint and update the completion state. */ complete = async (prompt: string, options?: CompletionRequestOptions) => this.#triggerRequest(prompt, options); /** Form submission handler to automatically reset input and call the completion API */ handleSubmit = async (event?: { preventDefault?: () => void }) => { event?.preventDefault?.(); if (this.#input()) { await this.complete(this.#input()); } }; #triggerRequest = async ( prompt: string, options?: CompletionRequestOptions, ) => { return callCompletionApi({ api: this.api, prompt, credentials: this.#options.credentials, headers: { ...this.#options.headers, ...options?.headers }, body: { ...this.#options.body, ...options?.body, }, streamProtocol: this.streamProtocol, fetch: this.#options.fetch, setCompletion: (completion: string) => { this.#completion.set(completion); }, setLoading: (loading: boolean) => { this.#loading.set(loading); }, setError: (error: any) => { this.#error.set(error); }, setAbortController: abortController => { this.#abortController = abortController ?? null; }, onFinish: this.#options.onFinish, onError: this.#options.onError, }); }; } --- File: /ai/packages/angular/src/lib/structured-object.ng.test.ts --- import { createTestServer, TestResponseController, } from '@ai-sdk/provider-utils/test'; import { z } from 'zod/v4'; import { StructuredObject } from './structured-object.ng'; const server = createTestServer({ '/api/object': {}, }); describe('text stream', () => { const schema = z.object({ content: z.string() }); let structuredObject: StructuredObject<typeof schema>; beforeEach(() => { structuredObject = new StructuredObject({ api: '/api/object', schema, }); }); describe('when the API returns "Hello, world!"', () => { beforeEach(async () => { server.urls['/api/object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content": "Hello, ', 'world', '!"', ' }'], }; await structuredObject.submit('test-input'); }); it('should render the stream', () => { expect(structuredObject.object).toEqual({ content: 'Hello, world!' }); }); it('should send the correct input to the API', async () => { expect(await server.calls[0].requestBodyJson).toBe('test-input'); }); it('should not have an error', () => { expect(structuredObject.error).toBeUndefined(); }); }); describe('loading', () => { it('should be true while loading', async () => { const controller = new TestResponseController(); server.urls['/api/object'].response = { type: 'controlled-stream', controller, }; controller.write('{"content": '); const submitOperation = structuredObject.submit('test-input'); await vi.waitFor(() => { expect(structuredObject.loading).toBe(true); }); controller.write('"Hello, world!"}'); controller.close(); await submitOperation; expect(structuredObject.loading).toBe(false); }); }); describe('stop', () => { it('should abort the stream and not consume any more data', async () => { const controller = new TestResponseController(); server.urls['/api/object'].response = { type: 'controlled-stream', controller, }; controller.write('{"content": "h'); const submitOperation = structuredObject.submit('test-input'); await vi.waitFor(() => { expect(structuredObject.loading).toBe(true); expect(structuredObject.object).toStrictEqual({ content: 'h', }); }); structuredObject.stop(); await vi.waitFor(() => { expect(structuredObject.loading).toBe(false); }); await expect(controller.write('ello, world!"}')).rejects.toThrow(); await expect(controller.close()).rejects.toThrow(); await submitOperation; expect(structuredObject.loading).toBe(false); expect(structuredObject.object).toStrictEqual({ content: 'h', }); }); it('should stop and clear the object state after a call to submit then clear', async () => { const controller = new TestResponseController(); server.urls['/api/object'].response = { type: 'controlled-stream', controller, }; controller.write('{"content": "h'); const submitOperation = structuredObject.submit('test-input'); await vi.waitFor(() => { expect(structuredObject.loading).toBe(true); expect(structuredObject.object).toStrictEqual({ content: 'h', }); }); structuredObject.clear(); await vi.waitFor(() => { expect(structuredObject.loading).toBe(false); }); await expect(controller.write('ello, world!"}')).rejects.toThrow(); await expect(controller.close()).rejects.toThrow(); await submitOperation; expect(structuredObject.loading).toBe(false); expect(structuredObject.error).toBeUndefined(); expect(structuredObject.object).toBeUndefined(); }); }); describe('when the API returns a 404', () => { it('should produce the correct error state', async () => { server.urls['/api/object'].response = { type: 'error', status: 404, body: 'Not found', }; await structuredObject.submit('test-input'); expect(structuredObject.error).toBeInstanceOf(Error); expect(structuredObject.error?.message).toBe('Not found'); expect(structuredObject.loading).toBe(false); }); }); describe('onFinish', () => { it('should be called with an object when the stream finishes and the object matches the schema', async () => { server.urls['/api/object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'], }; const onFinish = vi.fn(); const structuredObjectWithOnFinish = new StructuredObject({ api: '/api/object', schema: z.object({ content: z.string() }), onFinish, }); await structuredObjectWithOnFinish.submit('test-input'); expect(onFinish).toHaveBeenCalledExactlyOnceWith({ object: { content: 'Hello, world!' }, error: undefined, }); }); it('should be called with an error when the stream finishes and the object does not match the schema', async () => { server.urls['/api/object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content-wrong": "Hello, ', 'world', '!"', '}'], }; const onFinish = vi.fn(); const structuredObjectWithOnFinish = new StructuredObject({ api: '/api/object', schema: z.object({ content: z.string() }), onFinish, }); await structuredObjectWithOnFinish.submit('test-input'); expect(onFinish).toHaveBeenCalledExactlyOnceWith({ object: undefined, error: expect.any(Error), }); }); }); it('should send custom headers', async () => { server.urls['/api/object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'], }; const structuredObjectWithCustomHeaders = new StructuredObject({ api: '/api/object', schema: z.object({ content: z.string() }), headers: { Authorization: 'Bearer TEST_TOKEN', 'X-Custom-Header': 'CustomValue', }, }); await structuredObjectWithCustomHeaders.submit('test-input'); expect(server.calls[0].requestHeaders).toStrictEqual({ 'content-type': 'application/json', authorization: 'Bearer TEST_TOKEN', 'x-custom-header': 'CustomValue', }); }); it('should send custom credentials', async () => { server.urls['/api/object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'], }; const structuredObjectWithCustomCredentials = new StructuredObject({ api: '/api/object', schema: z.object({ content: z.string() }), credentials: 'include', }); await structuredObjectWithCustomCredentials.submit('test-input'); expect(server.calls[0].requestCredentials).toBe('include'); }); it('should clear the object state after a call to clear', async () => { server.urls['/api/object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'], }; const structuredObjectWithOnFinish = new StructuredObject({ api: '/api/object', schema: z.object({ content: z.string() }), }); await structuredObjectWithOnFinish.submit('test-input'); expect(structuredObjectWithOnFinish.object).toBeDefined(); structuredObjectWithOnFinish.clear(); expect(structuredObjectWithOnFinish.object).toBeUndefined(); expect(structuredObjectWithOnFinish.error).toBeUndefined(); expect(structuredObjectWithOnFinish.loading).toBe(false); }); }); --- File: /ai/packages/angular/src/lib/structured-object.ng.ts --- import { generateId, isAbortError, safeValidateTypes, type FetchFunction, type InferSchema, } from '@ai-sdk/provider-utils'; import { signal } from '@angular/core'; import { asSchema, isDeepEqualData, parsePartialJson, type DeepPartial, type Schema, } from 'ai'; import type * as z3 from 'zod/v3'; import type * as z4 from 'zod/v4'; export type StructuredObjectOptions< SCHEMA extends z3.Schema | z4.core.$ZodType | Schema, RESULT = InferSchema<SCHEMA>, > = { /** * The API endpoint. It should stream JSON that matches the schema as chunked text. */ api: string; /** * A Zod schema that defines the shape of the complete object. */ schema: SCHEMA; /** * A unique identifier. If not provided, a random one will be * generated. When provided, the `useObject` hook with the same `id` will * have shared states across components. */ id?: string; /** * An optional value for the initial object. */ initialValue?: DeepPartial<RESULT>; /** * Custom fetch implementation. You can use it as a middleware to intercept requests, * or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; /** * Callback that is called when the stream has finished. */ onFinish?: (event: { /** * The generated object (typed according to the schema). * Can be undefined if the final object does not match the schema. */ object: RESULT | undefined; /** * Optional error object. This is e.g. a TypeValidationError when the final object does not match the schema. */ error: Error | undefined; }) => Promise<void> | void; /** * Callback function to be called when an error is encountered. */ onError?: (error: Error) => void; /** * Additional HTTP headers to be included in the request. */ headers?: Record<string, string> | Headers; /** * The credentials mode to be used for the fetch request. * Possible values are: 'omit', 'same-origin', 'include'. * Defaults to 'same-origin'. */ credentials?: RequestCredentials; }; export class StructuredObject< SCHEMA extends z3.Schema | z4.core.$ZodType | Schema, RESULT = InferSchema<SCHEMA>, INPUT = unknown, > { readonly options: StructuredObjectOptions<SCHEMA, RESULT>; readonly id: string; #abortController: AbortController | undefined; // Reactive state readonly #object = signal<DeepPartial<RESULT> | undefined>(undefined); readonly #loading = signal<boolean>(false); readonly #error = signal<Error | undefined>(undefined); /** * The current value for the generated object. Updated as the API streams JSON chunks. */ get object(): DeepPartial<RESULT> | undefined { return this.#object(); } /** The error object of the API request */ get error(): Error | undefined { return this.#error(); } /** * Flag that indicates whether an API request is in progress. */ get loading(): boolean { return this.#loading(); } constructor(options: StructuredObjectOptions<SCHEMA, RESULT>) { this.options = options; this.id = options.id ?? generateId(); this.#object.set(options.initialValue); } /** * Abort the current request immediately, keep the current partial object if any. */ stop = () => { try { this.#abortController?.abort(); } catch { // ignore } finally { this.#loading.set(false); this.#abortController = undefined; } }; /** * Calls the API with the provided input as JSON body. */ submit = async (input: INPUT) => { try { this.#clearObject(); this.#loading.set(true); const abortController = new AbortController(); this.#abortController = abortController; const actualFetch = this.options.fetch ?? fetch; const response = await actualFetch(this.options.api, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.options.headers, }, credentials: this.options.credentials, signal: abortController.signal, body: JSON.stringify(input), }); if (!response.ok) { throw new Error( (await response.text()) ?? 'Failed to fetch the response.', ); } if (response.body == null) { throw new Error('The response body is empty.'); } let accumulatedText = ''; let latestObject: DeepPartial<RESULT> | undefined = undefined; await response.body.pipeThrough(new TextDecoderStream()).pipeTo( new WritableStream<string>({ write: async chunk => { if (abortController?.signal.aborted) { throw new DOMException('Stream aborted', 'AbortError'); } accumulatedText += chunk; const { value } = await parsePartialJson(accumulatedText); const currentObject = value as DeepPartial<RESULT>; if (!isDeepEqualData(latestObject, currentObject)) { latestObject = currentObject; this.#object.set(currentObject); } }, close: async () => { this.#loading.set(false); this.#abortController = undefined; if (this.options.onFinish != null) { const validationResult = await safeValidateTypes({ value: latestObject, schema: asSchema(this.options.schema), }); if (validationResult.success) { this.options.onFinish({ object: validationResult.value, error: undefined, }); } else { this.options.onFinish({ object: undefined, error: (validationResult as { error: Error }).error, }); } } }, }), ); } catch (error) { if (isAbortError(error)) { return; } const coalescedError = error instanceof Error ? error : new Error(String(error)); if (this.options.onError) { this.options.onError(coalescedError); } this.#loading.set(false); this.#error.set(coalescedError); } }; clear = () => { this.stop(); this.#clearObject(); }; #clearObject = () => { this.#object.set(undefined); this.#error.set(undefined); this.#loading.set(false); }; } --- File: /ai/packages/angular/src/index.ts --- export { Chat } from './lib/chat.ng'; export { Completion, type CompletionOptions } from './lib/completion.ng'; export { StructuredObject, type StructuredObjectOptions, } from './lib/structured-object.ng'; --- File: /ai/packages/angular/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig({ entry: ['src/index.ts'], dts: true, format: ['esm', 'cjs'], outDir: 'dist', sourcemap: true, clean: true, target: 'es2022', // external: [/node_modules/] // you can list external deps here if needed }); --- File: /ai/packages/angular/vitest.config.ts --- import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, environment: 'jsdom', include: ['src/lib/**/*.test.ts'], }, }); --- File: /ai/packages/anthropic/src/internal/index.ts --- export { AnthropicMessagesLanguageModel } from '../anthropic-messages-language-model'; export { anthropicTools } from '../anthropic-tools'; export type { AnthropicMessagesModelId } from '../anthropic-messages-options'; export { prepareTools } from '../anthropic-prepare-tools'; --- File: /ai/packages/anthropic/src/tool/bash_20241022.ts --- import { createProviderDefinedToolFactory } from '@ai-sdk/provider-utils'; import z from 'zod/v4'; export const bash_20241022 = createProviderDefinedToolFactory< { /** * The bash command to run. Required unless the tool is being restarted. */ command: string; /** * Specifying true will restart this tool. Otherwise, leave this unspecified. */ restart?: boolean; }, {} >({ id: 'anthropic.bash_20241022', name: 'bash', inputSchema: z.object({ command: z.string(), restart: z.boolean().optional(), }), }); --- File: /ai/packages/anthropic/src/tool/bash_20250124.ts --- import { createProviderDefinedToolFactory } from '@ai-sdk/provider-utils'; import z from 'zod/v4'; export const bash_20250124 = createProviderDefinedToolFactory< { /** * The bash command to run. Required unless the tool is being restarted. */ command: string; /** * Specifying true will restart this tool. Otherwise, leave this unspecified. */ restart?: boolean; }, {} >({ id: 'anthropic.bash_20250124', name: 'bash', inputSchema: z.object({ command: z.string(), restart: z.boolean().optional(), }), }); --- File: /ai/packages/anthropic/src/tool/computer_20241022.ts --- import { createProviderDefinedToolFactory } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; export const computer_20241022 = createProviderDefinedToolFactory< { /** * The action to perform. The available actions are: * - `key`: Press a key or key-combination on the keyboard. * - This supports xdotool's `key` syntax. * - Examples: "a", "Return", "alt+Tab", "ctrl+s", "Up", "KP_0" (for the numpad 0 key). * - `type`: Type a string of text on the keyboard. * - `cursor_position`: Get the current (x, y) pixel coordinate of the cursor on the screen. * - `mouse_move`: Move the cursor to a specified (x, y) pixel coordinate on the screen. * - `left_click`: Click the left mouse button. * - `left_click_drag`: Click and drag the cursor to a specified (x, y) pixel coordinate on the screen. * - `right_click`: Click the right mouse button. * - `middle_click`: Click the middle mouse button. * - `double_click`: Double-click the left mouse button. * - `screenshot`: Take a screenshot of the screen. */ action: | 'key' | 'type' | 'mouse_move' | 'left_click' | 'left_click_drag' | 'right_click' | 'middle_click' | 'double_click' | 'screenshot' | 'cursor_position'; /** * (x, y): The x (pixels from the left edge) and y (pixels from the top edge) coordinates to move the mouse to. Required only by `action=mouse_move` and `action=left_click_drag`. */ coordinate?: number[]; /** * Required only by `action=type` and `action=key`. */ text?: string; }, { /** * The width of the display being controlled by the model in pixels. */ displayWidthPx: number; /** * The height of the display being controlled by the model in pixels. */ displayHeightPx: number; /** * The display number to control (only relevant for X11 environments). If specified, the tool will be provided a display number in the tool definition. */ displayNumber?: number; } >({ id: 'anthropic.computer_20241022', name: 'computer', inputSchema: z.object({ action: z.enum([ 'key', 'type', 'mouse_move', 'left_click', 'left_click_drag', 'right_click', 'middle_click', 'double_click', 'screenshot', 'cursor_position', ]), coordinate: z.array(z.number().int()).optional(), text: z.string().optional(), }), }); --- File: /ai/packages/anthropic/src/tool/computer_20250124.ts --- import { createProviderDefinedToolFactory } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; export const computer_20250124 = createProviderDefinedToolFactory< { /** * - `key`: Press a key or key-combination on the keyboard. * - This supports xdotool's `key` syntax. * - Examples: "a", "Return", "alt+Tab", "ctrl+s", "Up", "KP_0" (for the numpad 0 key). * - `hold_key`: Hold down a key or multiple keys for a specified duration (in seconds). Supports the same syntax as `key`. * - `type`: Type a string of text on the keyboard. * - `cursor_position`: Get the current (x, y) pixel coordinate of the cursor on the screen. * - `mouse_move`: Move the cursor to a specified (x, y) pixel coordinate on the screen. * - `left_mouse_down`: Press the left mouse button. * - `left_mouse_up`: Release the left mouse button. * - `left_click`: Click the left mouse button at the specified (x, y) pixel coordinate on the screen. You can also include a key combination to hold down while clicking using the `text` parameter. * - `left_click_drag`: Click and drag the cursor from `start_coordinate` to a specified (x, y) pixel coordinate on the screen. * - `right_click`: Click the right mouse button at the specified (x, y) pixel coordinate on the screen. * - `middle_click`: Click the middle mouse button at the specified (x, y) pixel coordinate on the screen. * - `double_click`: Double-click the left mouse button at the specified (x, y) pixel coordinate on the screen. * - `triple_click`: Triple-click the left mouse button at the specified (x, y) pixel coordinate on the screen. * - `scroll`: Scroll the screen in a specified direction by a specified amount of clicks of the scroll wheel, at the specified (x, y) pixel coordinate. DO NOT use PageUp/PageDown to scroll. * - `wait`: Wait for a specified duration (in seconds). * - `screenshot`: Take a screenshot of the screen. */ action: | 'key' | 'hold_key' | 'type' | 'cursor_position' | 'mouse_move' | 'left_mouse_down' | 'left_mouse_up' | 'left_click' | 'left_click_drag' | 'right_click' | 'middle_click' | 'double_click' | 'triple_click' | 'scroll' | 'wait' | 'screenshot'; /** * (x, y): The x (pixels from the left edge) and y (pixels from the top edge) coordinates to move the mouse to. Required only by `action=mouse_move` and `action=left_click_drag`. */ coordinate?: [number, number]; /** * The duration to hold the key down for. Required only by `action=hold_key` and `action=wait`. */ duration?: number; /** * The number of 'clicks' to scroll. Required only by `action=scroll`. */ scroll_amount?: number; /** * The direction to scroll the screen. Required only by `action=scroll`. */ scroll_direction?: 'up' | 'down' | 'left' | 'right'; /** * (x, y): The x (pixels from the left edge) and y (pixels from the top edge) coordinates to start the drag from. Required only by `action=left_click_drag`. */ start_coordinate?: [number, number]; /** * Required only by `action=type`, `action=key`, and `action=hold_key`. Can also be used by click or scroll actions to hold down keys while clicking or scrolling. */ text?: string; }, { /** * The width of the display being controlled by the model in pixels. */ displayWidthPx: number; /** * The height of the display being controlled by the model in pixels. */ displayHeightPx: number; /** * The display number to control (only relevant for X11 environments). If specified, the tool will be provided a display number in the tool definition. */ displayNumber?: number; } >({ id: 'anthropic.computer_20250124', name: 'computer', inputSchema: z.object({ action: z.enum([ 'key', 'hold_key', 'type', 'cursor_position', 'mouse_move', 'left_mouse_down', 'left_mouse_up', 'left_click', 'left_click_drag', 'right_click', 'middle_click', 'double_click', 'triple_click', 'scroll', 'wait', 'screenshot', ]), coordinate: z.tuple([z.number().int(), z.number().int()]).optional(), duration: z.number().optional(), scroll_amount: z.number().optional(), scroll_direction: z.enum(['up', 'down', 'left', 'right']).optional(), start_coordinate: z.tuple([z.number().int(), z.number().int()]).optional(), text: z.string().optional(), }), }); --- File: /ai/packages/anthropic/src/tool/text-editor_20241022.ts --- import { createProviderDefinedToolFactory } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; export const textEditor_20241022 = createProviderDefinedToolFactory< { /** * The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`. */ command: 'view' | 'create' | 'str_replace' | 'insert' | 'undo_edit'; /** * Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. */ path: string; /** * Required parameter of `create` command, with the content of the file to be created. */ file_text?: string; /** * Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. */ insert_line?: number; /** * Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert. */ new_str?: string; /** * Required parameter of `str_replace` command containing the string in `path` to replace. */ old_str?: string; /** * Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ view_range?: number[]; }, {} >({ id: 'anthropic.text_editor_20241022', name: 'str_replace_editor', inputSchema: z.object({ command: z.enum(['view', 'create', 'str_replace', 'insert', 'undo_edit']), path: z.string(), file_text: z.string().optional(), insert_line: z.number().int().optional(), new_str: z.string().optional(), old_str: z.string().optional(), view_range: z.array(z.number().int()).optional(), }), }); --- File: /ai/packages/anthropic/src/tool/text-editor_20250124.ts --- import { createProviderDefinedToolFactory } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; export const textEditor_20250124 = createProviderDefinedToolFactory< { /** * The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`. */ command: 'view' | 'create' | 'str_replace' | 'insert' | 'undo_edit'; /** * Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. */ path: string; /** * Required parameter of `create` command, with the content of the file to be created. */ file_text?: string; /** * Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. */ insert_line?: number; /** * Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert. */ new_str?: string; /** * Required parameter of `str_replace` command containing the string in `path` to replace. */ old_str?: string; /** * Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ view_range?: number[]; }, {} >({ id: 'anthropic.text_editor_20250124', name: 'str_replace_editor', inputSchema: z.object({ command: z.enum(['view', 'create', 'str_replace', 'insert', 'undo_edit']), path: z.string(), file_text: z.string().optional(), insert_line: z.number().int().optional(), new_str: z.string().optional(), old_str: z.string().optional(), view_range: z.array(z.number().int()).optional(), }), }); --- File: /ai/packages/anthropic/src/tool/text-editor_20250429.ts --- import { createProviderDefinedToolFactory } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; export const textEditor_20250429 = createProviderDefinedToolFactory< { /** * The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`. * Note: `undo_edit` is not supported in Claude 4 models. */ command: 'view' | 'create' | 'str_replace' | 'insert'; /** * Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. */ path: string; /** * Required parameter of `create` command, with the content of the file to be created. */ file_text?: string; /** * Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. */ insert_line?: number; /** * Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert. */ new_str?: string; /** * Required parameter of `str_replace` command containing the string in `path` to replace. */ old_str?: string; /** * Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ view_range?: number[]; }, {} >({ id: 'anthropic.text_editor_20250429', name: 'str_replace_based_edit_tool', inputSchema: z.object({ command: z.enum(['view', 'create', 'str_replace', 'insert']), path: z.string(), file_text: z.string().optional(), insert_line: z.number().int().optional(), new_str: z.string().optional(), old_str: z.string().optional(), view_range: z.array(z.number().int()).optional(), }), }); --- File: /ai/packages/anthropic/src/tool/web-search_20250305.ts --- import { createProviderDefinedToolFactoryWithOutputSchema } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; // Args validation schema export const webSearch_20250305ArgsSchema = z.object({ /** * Maximum number of web searches Claude can perform during the conversation. */ maxUses: z.number().optional(), /** * Optional list of domains that Claude is allowed to search. */ allowedDomains: z.array(z.string()).optional(), /** * Optional list of domains that Claude should avoid when searching. */ blockedDomains: z.array(z.string()).optional(), /** * Optional user location information to provide geographically relevant search results. */ userLocation: z .object({ type: z.literal('approximate'), city: z.string().optional(), region: z.string().optional(), country: z.string().optional(), timezone: z.string().optional(), }) .optional(), }); export const webSearch_20250305OutputSchema = z.array( z.object({ url: z.string(), title: z.string(), pageAge: z.string().nullable(), encryptedContent: z.string(), type: z.string(), }), ); const factory = createProviderDefinedToolFactoryWithOutputSchema< { /** * The search query to execute. */ query: string; }, Array<{ url: string; title: string; pageAge: string | null; encryptedContent: string; type: string; }>, { /** * Maximum number of web searches Claude can perform during the conversation. */ maxUses?: number; /** * Optional list of domains that Claude is allowed to search. */ allowedDomains?: string[]; /** * Optional list of domains that Claude should avoid when searching. */ blockedDomains?: string[]; /** * Optional user location information to provide geographically relevant search results. */ userLocation?: { type: 'approximate'; city?: string; region?: string; country?: string; timezone?: string; }; } >({ id: 'anthropic.web_search_20250305', name: 'web_search', inputSchema: z.object({ query: z.string(), }), outputSchema: webSearch_20250305OutputSchema, }); export const webSearch_20250305 = ( args: Parameters<typeof factory>[0] = {}, // default ) => { return factory(args); }; --- File: /ai/packages/anthropic/src/anthropic-api-types.ts --- import { JSONSchema7 } from '@ai-sdk/provider'; export type AnthropicMessagesPrompt = { system: Array<AnthropicTextContent> | undefined; messages: AnthropicMessage[]; }; export type AnthropicMessage = AnthropicUserMessage | AnthropicAssistantMessage; export type AnthropicCacheControl = { type: 'ephemeral'; }; export interface AnthropicUserMessage { role: 'user'; content: Array< | AnthropicTextContent | AnthropicImageContent | AnthropicDocumentContent | AnthropicToolResultContent >; } export interface AnthropicAssistantMessage { role: 'assistant'; content: Array< | AnthropicTextContent | AnthropicThinkingContent | AnthropicRedactedThinkingContent | AnthropicToolCallContent | AnthropicServerToolUseContent | AnthropicWebSearchToolResultContent >; } export interface AnthropicTextContent { type: 'text'; text: string; cache_control: AnthropicCacheControl | undefined; } export interface AnthropicThinkingContent { type: 'thinking'; thinking: string; signature: string; cache_control: AnthropicCacheControl | undefined; } export interface AnthropicRedactedThinkingContent { type: 'redacted_thinking'; data: string; cache_control: AnthropicCacheControl | undefined; } type AnthropicContentSource = | { type: 'base64'; media_type: string; data: string; } | { type: 'url'; url: string; } | { type: 'text'; media_type: 'text/plain'; data: string; }; export interface AnthropicImageContent { type: 'image'; source: AnthropicContentSource; cache_control: AnthropicCacheControl | undefined; } export interface AnthropicDocumentContent { type: 'document'; source: AnthropicContentSource; title?: string; context?: string; citations?: { enabled: boolean }; cache_control: AnthropicCacheControl | undefined; } export interface AnthropicToolCallContent { type: 'tool_use'; id: string; name: string; input: unknown; cache_control: AnthropicCacheControl | undefined; } export interface AnthropicServerToolUseContent { type: 'server_tool_use'; id: string; name: 'web_search'; input: unknown; cache_control: AnthropicCacheControl | undefined; } export interface AnthropicToolResultContent { type: 'tool_result'; tool_use_id: string; content: string | Array<AnthropicTextContent | AnthropicImageContent>; is_error: boolean | undefined; cache_control: AnthropicCacheControl | undefined; } export interface AnthropicWebSearchToolResultContent { type: 'web_search_tool_result'; tool_use_id: string; content: Array<{ url: string; title: string; page_age: string | null; encrypted_content: string; type: string; }>; cache_control: AnthropicCacheControl | undefined; } export type AnthropicTool = | { name: string; description: string | undefined; input_schema: JSONSchema7; cache_control: AnthropicCacheControl | undefined; } | { name: string; type: 'computer_20250124' | 'computer_20241022'; display_width_px: number; display_height_px: number; display_number: number; } | { name: string; type: | 'text_editor_20250124' | 'text_editor_20241022' | 'text_editor_20250429'; } | { name: string; type: 'bash_20250124' | 'bash_20241022'; } | { type: 'web_search_20250305'; name: string; max_uses?: number; allowed_domains?: string[]; blocked_domains?: string[]; user_location?: { type: 'approximate'; city?: string; region?: string; country?: string; timezone?: string; }; }; export type AnthropicToolChoice = | { type: 'auto' | 'any'; disable_parallel_tool_use?: boolean } | { type: 'tool'; name: string; disable_parallel_tool_use?: boolean }; --- File: /ai/packages/anthropic/src/anthropic-error.test.ts --- import { anthropicErrorDataSchema } from './anthropic-error'; describe('anthropicError', () => { describe('anthropicErrorDataSchema', () => { it('should parse overloaded error', async () => { const result = anthropicErrorDataSchema.safeParse({ type: 'error', error: { details: null, type: 'overloaded_error', message: 'Overloaded', }, }); expect(result).toMatchInlineSnapshot(` { "data": { "error": { "message": "Overloaded", "type": "overloaded_error", }, "type": "error", }, "success": true, } `); }); }); }); --- File: /ai/packages/anthropic/src/anthropic-error.ts --- import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; export const anthropicErrorDataSchema = z.object({ type: z.literal('error'), error: z.object({ type: z.string(), message: z.string(), }), }); export type AnthropicErrorData = z.infer<typeof anthropicErrorDataSchema>; export const anthropicFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: anthropicErrorDataSchema, errorToMessage: data => data.error.message, }); --- File: /ai/packages/anthropic/src/anthropic-messages-language-model.test.ts --- import { LanguageModelV2Prompt, LanguageModelV2StreamPart, JSONValue, } from '@ai-sdk/provider'; import { convertReadableStreamToArray, createTestServer, mockId, } from '@ai-sdk/provider-utils/test'; import { AnthropicProviderOptions } from './anthropic-messages-options'; import { createAnthropic } from './anthropic-provider'; import { type DocumentCitation } from './anthropic-messages-language-model'; const TEST_PROMPT: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; const provider = createAnthropic({ apiKey: 'test-api-key' }); const model = provider('claude-3-haiku-20240307'); describe('AnthropicMessagesLanguageModel', () => { const server = createTestServer({ 'https://api.anthropic.com/v1/messages': {}, }); describe('doGenerate', () => { function prepareJsonResponse({ content = [{ type: 'text', text: '' }], usage = { input_tokens: 4, output_tokens: 30, }, stopReason = 'end_turn', id = 'msg_017TfcQ4AgGxKyBduUpqYPZn', model = 'claude-3-haiku-20240307', headers = {}, }: { content?: Array< | { type: 'text'; text: string; citations?: Array<DocumentCitation>; } | { type: 'thinking'; thinking: string; signature: string } | { type: 'tool_use'; id: string; name: string; input: unknown } >; usage?: Record<string, JSONValue> & { input_tokens: number; output_tokens: number; cache_creation_input_tokens?: number; cache_read_input_tokens?: number; }; stopReason?: string; id?: string; model?: string; headers?: Record<string, string>; }) { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'json-value', headers, body: { id, type: 'message', role: 'assistant', content, model, stop_reason: stopReason, stop_sequence: null, usage, }, }; } describe('reasoning (thinking enabled)', () => { it('should pass thinking config; add budget tokens; clear out temperature, top_p, top_k; and return warnings', async () => { prepareJsonResponse({ content: [{ type: 'text', text: 'Hello, World!' }], }); const result = await model.doGenerate({ prompt: TEST_PROMPT, temperature: 0.5, topP: 0.7, topK: 0.1, providerOptions: { anthropic: { thinking: { type: 'enabled', budgetTokens: 1000 }, } satisfies AnthropicProviderOptions, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'claude-3-haiku-20240307', messages: [ { role: 'user', content: [{ type: 'text', text: 'Hello' }], }, ], max_tokens: 4096 + 1000, thinking: { type: 'enabled', budget_tokens: 1000 }, }); expect(result.warnings).toStrictEqual([ { type: 'unsupported-setting', setting: 'temperature', details: 'temperature is not supported when thinking is enabled', }, { type: 'unsupported-setting', details: 'topK is not supported when thinking is enabled', setting: 'topK', }, { type: 'unsupported-setting', details: 'topP is not supported when thinking is enabled', setting: 'topP', }, ]); }); it('should extract reasoning response', async () => { prepareJsonResponse({ content: [ { type: 'thinking', thinking: 'I am thinking...', signature: '1234567890', }, { type: 'text', text: 'Hello, World!' }, ], }); const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "providerMetadata": { "anthropic": { "signature": "1234567890", }, }, "text": "I am thinking...", "type": "reasoning", }, { "text": "Hello, World!", "type": "text", }, ] `); }); }); describe('json schema response format', () => { let result: Awaited<ReturnType<typeof model.doGenerate>>; beforeEach(async () => { prepareJsonResponse({ content: [ { type: 'text', text: 'Some text\n\n' }, { type: 'tool_use', id: 'toolu_1', name: 'json', input: { name: 'example value' }, }, ], stopReason: 'tool_use', }); result = await model.doGenerate({ prompt: TEST_PROMPT, responseFormat: { type: 'json', schema: { type: 'object', properties: { name: { type: 'string' }, }, required: ['name'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }); }); it('should pass json schema response format as a tool', async () => { expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "max_tokens": 4096, "messages": [ { "content": [ { "text": "Hello", "type": "text", }, ], "role": "user", }, ], "model": "claude-3-haiku-20240307", "tool_choice": { "name": "json", "type": "tool", }, "tools": [ { "description": "Respond with a JSON object.", "input_schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "name": { "type": "string", }, }, "required": [ "name", ], "type": "object", }, "name": "json", }, ], } `); }); it('should return the json response', async () => { expect(result.content).toMatchInlineSnapshot(` [ { "text": "{"name":"example value"}", "type": "text", }, ] `); }); }); it('should extract text response', async () => { prepareJsonResponse({ content: [{ type: 'text', text: 'Hello, World!' }], }); const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "text": "Hello, World!", "type": "text", }, ] `); }); it('should extract tool calls', async () => { prepareJsonResponse({ content: [ { type: 'text', text: 'Some text\n\n' }, { type: 'tool_use', id: 'toolu_1', name: 'test-tool', input: { value: 'example value' }, }, ], stopReason: 'tool_use', }); const { content, finishReason } = await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "text": "Some text ", "type": "text", }, { "input": "{"value":"example value"}", "toolCallId": "toolu_1", "toolName": "test-tool", "type": "tool-call", }, ] `); expect(finishReason).toStrictEqual('tool-calls'); }); it('should extract usage', async () => { prepareJsonResponse({ usage: { input_tokens: 20, output_tokens: 5 }, }); const { usage } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(usage).toMatchInlineSnapshot(` { "cachedInputTokens": undefined, "inputTokens": 20, "outputTokens": 5, "totalTokens": 25, } `); }); it('should send additional response information', async () => { prepareJsonResponse({ id: 'test-id', model: 'test-model', }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response).toMatchInlineSnapshot(` { "body": { "content": [ { "text": "", "type": "text", }, ], "id": "test-id", "model": "test-model", "role": "assistant", "stop_reason": "end_turn", "stop_sequence": null, "type": "message", "usage": { "input_tokens": 4, "output_tokens": 30, }, }, "headers": { "content-length": "203", "content-type": "application/json", }, "id": "test-id", "modelId": "test-model", } `); }); it('should expose the raw response headers', async () => { prepareJsonResponse({ headers: { 'test-header': 'test-value', }, }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-length': '237', 'content-type': 'application/json', // custom header 'test-header': 'test-value', }); }); it('should send the model id and settings', async () => { prepareJsonResponse({}); await model.doGenerate({ prompt: TEST_PROMPT, temperature: 0.5, maxOutputTokens: 100, topP: 0.9, topK: 0.1, stopSequences: ['abc', 'def'], frequencyPenalty: 0.15, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'claude-3-haiku-20240307', max_tokens: 100, stop_sequences: ['abc', 'def'], temperature: 0.5, top_k: 0.1, top_p: 0.9, messages: [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ], }); }); it('should pass tools and toolChoice', async () => { prepareJsonResponse({}); await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], toolChoice: { type: 'tool', toolName: 'test-tool', }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'claude-3-haiku-20240307', messages: [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ], max_tokens: 4096, tools: [ { name: 'test-tool', input_schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], tool_choice: { type: 'tool', name: 'test-tool', }, }); }); it('should pass disableParallelToolUse', async () => { prepareJsonResponse({}); await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, providerOptions: { anthropic: { disableParallelToolUse: true, }, }, }); expect(await server.calls[0].requestBodyJson).toMatchObject({ tool_choice: { type: 'auto', disable_parallel_tool_use: true, }, }); }); it('should pass headers', async () => { prepareJsonResponse({ content: [] }); const provider = createAnthropic({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider('claude-3-haiku-20240307').doGenerate({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toMatchInlineSnapshot(` { "anthropic-version": "2023-06-01", "content-type": "application/json", "custom-provider-header": "provider-header-value", "custom-request-header": "request-header-value", "x-api-key": "test-api-key", } `); }); it('should support cache control', async () => { prepareJsonResponse({ usage: { input_tokens: 20, output_tokens: 50, cache_creation_input_tokens: 10, cache_read_input_tokens: 5, }, }); const model = provider('claude-3-haiku-20240307'); const result = await model.doGenerate({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'Hello' }], providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, ], }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'claude-3-haiku-20240307', messages: [ { role: 'user', content: [ { type: 'text', text: 'Hello', cache_control: { type: 'ephemeral' }, }, ], }, ], max_tokens: 4096, }); expect(result).toMatchInlineSnapshot(` { "content": [ { "text": "", "type": "text", }, ], "finishReason": "stop", "providerMetadata": { "anthropic": { "cacheCreationInputTokens": 10, "usage": { "cache_creation_input_tokens": 10, "cache_read_input_tokens": 5, "input_tokens": 20, "output_tokens": 50, }, }, }, "request": { "body": { "max_tokens": 4096, "messages": [ { "content": [ { "cache_control": { "type": "ephemeral", }, "text": "Hello", "type": "text", }, ], "role": "user", }, ], "model": "claude-3-haiku-20240307", "stop_sequences": undefined, "system": undefined, "temperature": undefined, "tool_choice": undefined, "tools": undefined, "top_k": undefined, "top_p": undefined, }, }, "response": { "body": { "content": [ { "text": "", "type": "text", }, ], "id": "msg_017TfcQ4AgGxKyBduUpqYPZn", "model": "claude-3-haiku-20240307", "role": "assistant", "stop_reason": "end_turn", "stop_sequence": null, "type": "message", "usage": { "cache_creation_input_tokens": 10, "cache_read_input_tokens": 5, "input_tokens": 20, "output_tokens": 50, }, }, "headers": { "content-length": "299", "content-type": "application/json", }, "id": "msg_017TfcQ4AgGxKyBduUpqYPZn", "modelId": "claude-3-haiku-20240307", }, "usage": { "cachedInputTokens": 5, "inputTokens": 20, "outputTokens": 50, "totalTokens": 70, }, "warnings": [], } `); }); it('should support cache control and return extra fields in provider metadata', async () => { prepareJsonResponse({ usage: { input_tokens: 20, output_tokens: 50, cache_creation_input_tokens: 10, cache_read_input_tokens: 5, cache_creation: { ephemeral_5m_input_tokens: 0, ephemeral_1h_input_tokens: 10, }, }, }); const model = provider('claude-3-haiku-20240307'); const result = await model.doGenerate({ headers: { 'anthropic-beta': 'extended-cache-ttl-2025-04-11', }, prompt: [ { role: 'user', content: [{ type: 'text', text: 'Hello' }], providerOptions: { anthropic: { cacheControl: { type: 'ephemeral', ttl: '1h' }, }, }, }, ], }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'claude-3-haiku-20240307', messages: [ { role: 'user', content: [ { type: 'text', text: 'Hello', cache_control: { type: 'ephemeral', ttl: '1h' }, }, ], }, ], max_tokens: 4096, }); expect(result).toMatchInlineSnapshot(` { "content": [ { "text": "", "type": "text", }, ], "finishReason": "stop", "providerMetadata": { "anthropic": { "cacheCreationInputTokens": 10, "usage": { "cache_creation": { "ephemeral_1h_input_tokens": 10, "ephemeral_5m_input_tokens": 0, }, "cache_creation_input_tokens": 10, "cache_read_input_tokens": 5, "input_tokens": 20, "output_tokens": 50, }, }, }, "request": { "body": { "max_tokens": 4096, "messages": [ { "content": [ { "cache_control": { "ttl": "1h", "type": "ephemeral", }, "text": "Hello", "type": "text", }, ], "role": "user", }, ], "model": "claude-3-haiku-20240307", "stop_sequences": undefined, "system": undefined, "temperature": undefined, "tool_choice": undefined, "tools": undefined, "top_k": undefined, "top_p": undefined, }, }, "response": { "body": { "content": [ { "text": "", "type": "text", }, ], "id": "msg_017TfcQ4AgGxKyBduUpqYPZn", "model": "claude-3-haiku-20240307", "role": "assistant", "stop_reason": "end_turn", "stop_sequence": null, "type": "message", "usage": { "cache_creation": { "ephemeral_1h_input_tokens": 10, "ephemeral_5m_input_tokens": 0, }, "cache_creation_input_tokens": 10, "cache_read_input_tokens": 5, "input_tokens": 20, "output_tokens": 50, }, }, "headers": { "content-length": "379", "content-type": "application/json", }, "id": "msg_017TfcQ4AgGxKyBduUpqYPZn", "modelId": "claude-3-haiku-20240307", }, "usage": { "cachedInputTokens": 5, "inputTokens": 20, "outputTokens": 50, "totalTokens": 70, }, "warnings": [], } `); }); it('should send request body', async () => { prepareJsonResponse({ content: [] }); const { request } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(request).toMatchInlineSnapshot(` { "body": { "max_tokens": 4096, "messages": [ { "content": [ { "cache_control": undefined, "text": "Hello", "type": "text", }, ], "role": "user", }, ], "model": "claude-3-haiku-20240307", "stop_sequences": undefined, "system": undefined, "temperature": undefined, "tool_choice": undefined, "tools": undefined, "top_k": undefined, "top_p": undefined, }, } `); }); it('should process PDF citation responses', async () => { // Create a model with a predictable generateId function const mockProvider = createAnthropic({ apiKey: 'test-api-key', generateId: () => 'test-citation-id', }); const modelWithMockId = mockProvider('claude-3-haiku-20240307'); // Mock response with PDF citations prepareJsonResponse({ content: [ { type: 'text', text: 'Based on the document, the results show positive growth.', citations: [ { type: 'page_location', cited_text: 'Revenue increased by 25% year over year', document_index: 0, document_title: 'Financial Report 2023', start_page_number: 5, end_page_number: 6, }, ], }, ], }); const result = await modelWithMockId.doGenerate({ prompt: [ { role: 'user', content: [ { type: 'file', data: 'base64PDFdata', mediaType: 'application/pdf', filename: 'financial-report.pdf', providerOptions: { anthropic: { citations: { enabled: true }, }, }, }, { type: 'text', text: 'What do the results show?', }, ], }, ], }); expect(result.content).toMatchInlineSnapshot(` [ { "text": "Based on the document, the results show positive growth.", "type": "text", }, { "filename": "financial-report.pdf", "id": "test-citation-id", "mediaType": "application/pdf", "providerMetadata": { "anthropic": { "citedText": "Revenue increased by 25% year over year", "endPageNumber": 6, "startPageNumber": 5, }, }, "sourceType": "document", "title": "Financial Report 2023", "type": "source", }, ] `); }); it('should process text citation responses', async () => { const mockProvider = createAnthropic({ apiKey: 'test-api-key', generateId: () => 'test-text-citation-id', }); const modelWithMockId = mockProvider('claude-3-haiku-20240307'); prepareJsonResponse({ content: [ { type: 'text', text: 'The text shows important information.', citations: [ { type: 'char_location', cited_text: 'important information', document_index: 0, document_title: 'Test Document', start_char_index: 15, end_char_index: 35, }, ], }, ], }); const result = await modelWithMockId.doGenerate({ prompt: [ { role: 'user', content: [ { type: 'file', data: 'VGVzdCBkb2N1bWVudCBjb250ZW50', mediaType: 'text/plain', filename: 'test.txt', providerOptions: { anthropic: { citations: { enabled: true }, }, }, }, { type: 'text', text: 'What does this say?', }, ], }, ], }); expect(result.content).toMatchInlineSnapshot(` [ { "text": "The text shows important information.", "type": "text", }, { "filename": "test.txt", "id": "test-text-citation-id", "mediaType": "text/plain", "providerMetadata": { "anthropic": { "citedText": "important information", "endCharIndex": 35, "startCharIndex": 15, }, }, "sourceType": "document", "title": "Test Document", "type": "source", }, ] `); }); describe('web search', () => { const TEST_PROMPT = [ { role: 'user' as const, content: [ { type: 'text' as const, text: 'What is the latest news?' }, ], }, ]; function prepareJsonResponse(body: any) { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'json-value', body, }; } it('should enable server-side web search when using anthropic.tools.webSearch_20250305', async () => { prepareJsonResponse({ type: 'message', id: 'msg_test', content: [ { type: 'text', text: 'Here are the latest quantum computing breakthroughs.', }, ], stop_reason: 'end_turn', usage: { input_tokens: 10, output_tokens: 20 }, }); await model.doGenerate({ prompt: TEST_PROMPT, tools: [ { type: 'provider-defined', id: 'anthropic.web_search_20250305', name: 'web_search', args: { maxUses: 3, allowedDomains: ['arxiv.org', 'nature.com', 'mit.edu'], }, }, ], }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.tools).toHaveLength(1); expect(requestBody.tools[0]).toEqual({ type: 'web_search_20250305', name: 'web_search', max_uses: 3, allowed_domains: ['arxiv.org', 'nature.com', 'mit.edu'], }); }); it('should pass web search configuration with blocked domains', async () => { prepareJsonResponse({ type: 'message', id: 'msg_test', content: [ { type: 'text', text: 'Here are the latest stock market trends.' }, ], stop_reason: 'end_turn', usage: { input_tokens: 10, output_tokens: 20 }, }); await model.doGenerate({ prompt: TEST_PROMPT, tools: [ { type: 'provider-defined', id: 'anthropic.web_search_20250305', name: 'web_search', args: { maxUses: 2, blockedDomains: ['reddit.com'], }, }, ], }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.tools).toHaveLength(1); expect(requestBody.tools[0]).toEqual({ type: 'web_search_20250305', name: 'web_search', max_uses: 2, blocked_domains: ['reddit.com'], }); }); it('should handle web search with user location', async () => { prepareJsonResponse({ type: 'message', id: 'msg_test', content: [{ type: 'text', text: 'Here are local tech events.' }], stop_reason: 'end_turn', usage: { input_tokens: 10, output_tokens: 20 }, }); await model.doGenerate({ prompt: TEST_PROMPT, tools: [ { type: 'provider-defined', id: 'anthropic.web_search_20250305', name: 'web_search', args: { maxUses: 1, userLocation: { type: 'approximate', city: 'New York', region: 'New York', country: 'US', timezone: 'America/New_York', }, }, }, ], }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.tools).toHaveLength(1); expect(requestBody.tools[0]).toEqual({ type: 'web_search_20250305', name: 'web_search', max_uses: 1, user_location: { type: 'approximate', city: 'New York', region: 'New York', country: 'US', timezone: 'America/New_York', }, }); }); it('should handle web search with partial user location (city + country)', async () => { prepareJsonResponse({ type: 'message', id: 'msg_test', content: [{ type: 'text', text: 'Here are local events.' }], stop_reason: 'end_turn', usage: { input_tokens: 10, output_tokens: 20 }, }); await model.doGenerate({ prompt: TEST_PROMPT, tools: [ { type: 'provider-defined', id: 'anthropic.web_search_20250305', name: 'web_search', args: { maxUses: 1, userLocation: { type: 'approximate', city: 'London', country: 'GB', }, }, }, ], }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.tools).toHaveLength(1); expect(requestBody.tools[0]).toEqual({ type: 'web_search_20250305', name: 'web_search', max_uses: 1, user_location: { type: 'approximate', city: 'London', country: 'GB', }, }); }); it('should handle web search with minimal user location (country only)', async () => { prepareJsonResponse({ type: 'message', id: 'msg_test', content: [{ type: 'text', text: 'Here are global events.' }], stop_reason: 'end_turn', usage: { input_tokens: 10, output_tokens: 20 }, }); await model.doGenerate({ prompt: TEST_PROMPT, tools: [ { type: 'provider-defined', id: 'anthropic.web_search_20250305', name: 'web_search', args: { maxUses: 1, userLocation: { type: 'approximate', country: 'US', }, }, }, ], }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.tools).toHaveLength(1); expect(requestBody.tools[0]).toEqual({ type: 'web_search_20250305', name: 'web_search', max_uses: 1, user_location: { type: 'approximate', country: 'US', }, }); }); it('should handle server-side web search results with citations', async () => { prepareJsonResponse({ type: 'message', id: 'msg_test', content: [ { type: 'server_tool_use', id: 'tool_1', name: 'web_search', input: { query: 'latest AI news' }, }, { type: 'web_search_tool_result', tool_use_id: 'tool_1', content: [ { type: 'web_search_result', url: 'https://example.com/ai-news', title: 'Latest AI Developments', encrypted_content: 'encrypted_content_123', page_age: 'January 15, 2025', }, ], }, { type: 'text', text: 'Based on recent articles, AI continues to advance rapidly.', }, ], stop_reason: 'end_turn', usage: { input_tokens: 10, output_tokens: 20, server_tool_use: { web_search_requests: 1 }, }, }); const provider = createAnthropic({ apiKey: 'test-api-key', generateId: mockId(), }); const model = provider('claude-3-5-sonnet-latest'); const result = await model.doGenerate({ prompt: TEST_PROMPT, tools: [ { type: 'provider-defined', id: 'anthropic.web_search_20250305', name: 'web_search', args: { maxUses: 5, }, }, ], }); expect(result.content).toMatchInlineSnapshot(` [ { "input": "{"query":"latest AI news"}", "providerExecuted": true, "toolCallId": "tool_1", "toolName": "web_search", "type": "tool-call", }, { "providerExecuted": true, "result": [ { "encryptedContent": "encrypted_content_123", "pageAge": "January 15, 2025", "title": "Latest AI Developments", "type": "web_search_result", "url": "https://example.com/ai-news", }, ], "toolCallId": "tool_1", "toolName": "web_search", "type": "tool-result", }, { "id": "id-0", "providerMetadata": { "anthropic": { "pageAge": "January 15, 2025", }, }, "sourceType": "url", "title": "Latest AI Developments", "type": "source", "url": "https://example.com/ai-news", }, { "text": "Based on recent articles, AI continues to advance rapidly.", "type": "text", }, ] `); }); it('should handle server-side web search errors', async () => { prepareJsonResponse({ type: 'message', id: 'msg_test', content: [ { type: 'web_search_tool_result', tool_use_id: 'tool_1', content: { type: 'web_search_tool_result_error', error_code: 'max_uses_exceeded', }, }, { type: 'text', text: 'I cannot search further due to limits.', }, ], stop_reason: 'end_turn', usage: { input_tokens: 10, output_tokens: 20 }, }); const result = await model.doGenerate({ prompt: TEST_PROMPT, tools: [ { type: 'provider-defined', id: 'anthropic.web_search_20250305', name: 'web_search', args: { maxUses: 1, }, }, ], }); expect(result.content).toMatchInlineSnapshot(` [ { "isError": true, "providerExecuted": true, "result": { "errorCode": "max_uses_exceeded", "type": "web_search_tool_result_error", }, "toolCallId": "tool_1", "toolName": "web_search", "type": "tool-result", }, { "text": "I cannot search further due to limits.", "type": "text", }, ] `); }); it('should work alongside regular client-side tools', async () => { prepareJsonResponse({ type: 'message', id: 'msg_test', content: [{ type: 'text', text: 'I can search and calculate.' }], stop_reason: 'end_turn', usage: { input_tokens: 10, output_tokens: 20 }, }); await model.doGenerate({ prompt: TEST_PROMPT, tools: [ { type: 'function', name: 'calculator', description: 'Calculate math', inputSchema: { type: 'object', properties: {} }, }, { type: 'provider-defined', id: 'anthropic.web_search_20250305', name: 'web_search', args: { maxUses: 1, }, }, ], }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.tools).toHaveLength(2); expect(requestBody.tools[0]).toEqual({ name: 'calculator', description: 'Calculate math', input_schema: { type: 'object', properties: {} }, }); expect(requestBody.tools[1]).toEqual({ type: 'web_search_20250305', name: 'web_search', max_uses: 1, }); }); }); it('should throw an api error when the server is overloaded', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'error', status: 529, body: '{"type":"error","error":{"details":null,"type":"overloaded_error","message":"Overloaded"}}', }; await expect( model.doGenerate({ prompt: TEST_PROMPT, }), ).rejects.toThrow('Overloaded'); }); }); describe('doStream', () => { describe('json schema response format', () => { let result: Array<LanguageModelV2StreamPart>; beforeEach(async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"message_start","message":{"id":"msg_01GouTqNCGXzrj5LQ5jEkw67","type":"message","role":"assistant","model":"claude-3-haiku-20240307","stop_sequence":null,"usage":{"input_tokens":441,"output_tokens":2},"content":[],"stop_reason":null} }\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }\n\n`, `data: {"type": "ping"}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Okay"} }\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"!"} }\n\n`, `data: {"type":"content_block_stop","index":0 }\n\n`, `data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01DBsB4vvYLnBDzZ5rBSxSLs","name":"json","input":{}} }\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} }\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\\"value"} }\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\\":"} }\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\\"Spark"} }\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"le"} }\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" Day\\"}"} }\n\n`, `data: {"type":"content_block_stop","index":1 }\n\n`, `data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":65} }\n\n`, `data: {"type":"message_stop" }\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, responseFormat: { type: 'json', schema: { type: 'object', properties: { name: { type: 'string' }, }, required: ['name'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }); result = await convertReadableStreamToArray(stream); }); it('should pass json schema response format as a tool', async () => { expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "max_tokens": 4096, "messages": [ { "content": [ { "text": "Hello", "type": "text", }, ], "role": "user", }, ], "model": "claude-3-haiku-20240307", "stream": true, "tool_choice": { "name": "json", "type": "tool", }, "tools": [ { "description": "Respond with a JSON object.", "input_schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "name": { "type": "string", }, }, "required": [ "name", ], "type": "object", }, "name": "json", }, ], } `); }); it('should return the json response', async () => { expect(result).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "msg_01GouTqNCGXzrj5LQ5jEkw67", "modelId": "claude-3-haiku-20240307", "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "id": "0", "type": "text-end", }, { "id": "1", "type": "text-start", }, { "delta": "", "id": "1", "type": "text-delta", }, { "delta": "{"value", "id": "1", "type": "text-delta", }, { "delta": "":", "id": "1", "type": "text-delta", }, { "delta": ""Spark", "id": "1", "type": "text-delta", }, { "delta": "le", "id": "1", "type": "text-delta", }, { "delta": " Day"}", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "anthropic": { "cacheCreationInputTokens": null, "usage": { "input_tokens": 441, "output_tokens": 2, }, }, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 441, "outputTokens": 65, "totalTokens": 506, }, }, ] `); }); }); it('should stream text deltas', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"message_start","message":{"id":"msg_01KfpJoAEabmH2iHRRFjQMAG","type":"message","role":"assistant","content":[],"model":"claude-3-haiku-20240307","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":17,"output_tokens":1}}}\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":", "}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"World!"}}\n\n`, `data: {"type":"content_block_stop","index":0}\n\n`, `data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":227}}\n\n`, `data: {"type":"message_stop"}\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "msg_01KfpJoAEabmH2iHRRFjQMAG", "modelId": "claude-3-haiku-20240307", "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "delta": ", ", "id": "0", "type": "text-delta", }, { "delta": "World!", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "anthropic": { "cacheCreationInputTokens": null, "usage": { "input_tokens": 17, "output_tokens": 1, }, }, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 17, "outputTokens": 227, "totalTokens": 244, }, }, ] `); }); it('should stream reasoning deltas', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"message_start","message":{"id":"msg_01KfpJoAEabmH2iHRRFjQMAG","type":"message","role":"assistant","content":[],"model":"claude-3-haiku-20240307","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":17,"output_tokens":1}}}\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"I am"}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"thinking..."}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"1234567890"}}\n\n`, `data: {"type":"content_block_stop","index":0}\n\n`, `data: {"type":"content_block_start","index":1,"content_block":{"type":"text","text":""}}\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"Hello, World!"}}\n\n`, `data: {"type":"content_block_stop","index":1}\n\n`, `data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":227}}\n\n`, `data: {"type":"message_stop"}\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "msg_01KfpJoAEabmH2iHRRFjQMAG", "modelId": "claude-3-haiku-20240307", "type": "response-metadata", }, { "id": "0", "type": "reasoning-start", }, { "delta": "I am", "id": "0", "type": "reasoning-delta", }, { "delta": "thinking...", "id": "0", "type": "reasoning-delta", }, { "delta": "", "id": "0", "providerMetadata": { "anthropic": { "signature": "1234567890", }, }, "type": "reasoning-delta", }, { "id": "0", "type": "reasoning-end", }, { "id": "1", "type": "text-start", }, { "delta": "Hello, World!", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "anthropic": { "cacheCreationInputTokens": null, "usage": { "input_tokens": 17, "output_tokens": 1, }, }, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 17, "outputTokens": 227, "totalTokens": 244, }, }, ] `); }); it('should stream redacted reasoning', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"message_start","message":{"id":"msg_01KfpJoAEabmH2iHRRFjQMAG","type":"message","role":"assistant","content":[],"model":"claude-3-haiku-20240307","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":17,"output_tokens":1}}}\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"redacted_thinking","data":"redacted-thinking-data"}}\n\n`, `data: {"type":"content_block_stop","index":0}\n\n`, `data: {"type":"content_block_start","index":1,"content_block":{"type":"text","text":""}}\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"Hello, World!"}}\n\n`, `data: {"type":"content_block_stop","index":1}\n\n`, `data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":227}}\n\n`, `data: {"type":"message_stop"}\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "msg_01KfpJoAEabmH2iHRRFjQMAG", "modelId": "claude-3-haiku-20240307", "type": "response-metadata", }, { "id": "0", "providerMetadata": { "anthropic": { "redactedData": "redacted-thinking-data", }, }, "type": "reasoning-start", }, { "id": "0", "type": "reasoning-end", }, { "id": "1", "type": "text-start", }, { "delta": "Hello, World!", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "anthropic": { "cacheCreationInputTokens": null, "usage": { "input_tokens": 17, "output_tokens": 1, }, }, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 17, "outputTokens": 227, "totalTokens": 244, }, }, ] `); }); it('should ignore signatures on text deltas', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"message_start","message":{"id":"msg_01KfpJoAEabmH2iHRRFjQMAG","type":"message","role":"assistant","content":[],"model":"claude-3-haiku-20240307","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":17,"output_tokens":1}}}\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello, World!"}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"1234567890"}}\n\n`, `data: {"type":"content_block_stop","index":0}\n\n`, `data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":227}}\n\n`, `data: {"type":"message_stop"}\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "msg_01KfpJoAEabmH2iHRRFjQMAG", "modelId": "claude-3-haiku-20240307", "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "Hello, World!", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "anthropic": { "cacheCreationInputTokens": null, "usage": { "input_tokens": 17, "output_tokens": 1, }, }, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 17, "outputTokens": 227, "totalTokens": 244, }, }, ] `); }); it('should stream tool deltas', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"message_start","message":{"id":"msg_01GouTqNCGXzrj5LQ5jEkw67","type":"message","role":"assistant","model":"claude-3-haiku-20240307","stop_sequence":null,"usage":{"input_tokens":441,"output_tokens":2},"content":[],"stop_reason":null} }\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }\n\n`, `data: {"type": "ping"}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Okay"} }\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"!"} }\n\n`, `data: {"type":"content_block_stop","index":0 }\n\n`, `data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01DBsB4vvYLnBDzZ5rBSxSLs","name":"test-tool","input":{}} }\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} }\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\\"value"} }\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\\":"} }\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\\"Spark"} }\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"le"} }\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" Day\\"}"} }\n\n`, `data: {"type":"content_block_stop","index":1 }\n\n`, `data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":65} }\n\n`, `data: {"type":"message_stop" }\n\n`, ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "msg_01GouTqNCGXzrj5LQ5jEkw67", "modelId": "claude-3-haiku-20240307", "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "Okay", "id": "0", "type": "text-delta", }, { "delta": "!", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "id": "toolu_01DBsB4vvYLnBDzZ5rBSxSLs", "toolName": "test-tool", "type": "tool-input-start", }, { "delta": "", "id": "toolu_01DBsB4vvYLnBDzZ5rBSxSLs", "type": "tool-input-delta", }, { "delta": "{"value", "id": "toolu_01DBsB4vvYLnBDzZ5rBSxSLs", "type": "tool-input-delta", }, { "delta": "":", "id": "toolu_01DBsB4vvYLnBDzZ5rBSxSLs", "type": "tool-input-delta", }, { "delta": ""Spark", "id": "toolu_01DBsB4vvYLnBDzZ5rBSxSLs", "type": "tool-input-delta", }, { "delta": "le", "id": "toolu_01DBsB4vvYLnBDzZ5rBSxSLs", "type": "tool-input-delta", }, { "delta": " Day"}", "id": "toolu_01DBsB4vvYLnBDzZ5rBSxSLs", "type": "tool-input-delta", }, { "id": "toolu_01DBsB4vvYLnBDzZ5rBSxSLs", "type": "tool-input-end", }, { "input": "{"value":"Sparkle Day"}", "toolCallId": "toolu_01DBsB4vvYLnBDzZ5rBSxSLs", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "tool-calls", "providerMetadata": { "anthropic": { "cacheCreationInputTokens": null, "usage": { "input_tokens": 441, "output_tokens": 2, }, }, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 441, "outputTokens": 65, "totalTokens": 506, }, }, ] `); }); it('should forward error chunks', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"message_start","message":{"id":"msg_01KfpJoAEabmH2iHRRFjQMAG","type":"message","role":"assistant","content":[],"model":"claude-3-haiku-20240307","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":17,"output_tokens":1}} }\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }\n\n`, `data: {"type": "ping"}\n\n`, `data: {"type":"error","error":{"type":"error","message":"test error"}}\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "msg_01KfpJoAEabmH2iHRRFjQMAG", "modelId": "claude-3-haiku-20240307", "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "error": { "message": "test error", "type": "error", }, "type": "error", }, ] `); }); it('should expose the raw response headers', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', headers: { 'test-header': 'test-value' }, chunks: [ `data: {"type":"message_start","message":{"id":"msg_01KfpJoAEabmH2iHRRFjQMAG","type":"message","role":"assistant","content":[],"model":"claude-3-haiku-20240307","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":17,"output_tokens":1}}}\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello, World!"}}\n\n`, `data: {"type":"content_block_stop","index":0}\n\n`, `data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":227}}\n\n`, `data: {"type":"message_stop"}\n\n`, ], }; const { response } = await model.doStream({ prompt: TEST_PROMPT, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', // custom header 'test-header': 'test-value', }); }); it('should pass the messages and the model', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', headers: { 'test-header': 'test-value' }, chunks: [ `data: {"type":"message_start","message":{"id":"msg_01KfpJoAEabmH2iHRRFjQMAG","type":"message","role":"assistant","content":[],"model":"claude-3-haiku-20240307","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":17,"output_tokens":1}}}\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello, World!"}}\n\n`, `data: {"type":"content_block_stop","index":0}\n\n`, `data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":227}}\n\n`, `data: {"type":"message_stop"}\n\n`, ], }; await model.doStream({ prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ stream: true, model: 'claude-3-haiku-20240307', max_tokens: 4096, // default value messages: [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ], }); }); it('should pass headers', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', headers: { 'test-header': 'test-value' }, chunks: [ `data: {"type":"message_start","message":{"id":"msg_01KfpJoAEabmH2iHRRFjQMAG","type":"message","role":"assistant","content":[],"model":"claude-3-haiku-20240307","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":17,"output_tokens":1}}}\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello, World!"}}\n\n`, `data: {"type":"content_block_stop","index":0}\n\n`, `data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":227}}\n\n`, `data: {"type":"message_stop"}\n\n`, ], }; const provider = createAnthropic({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider('claude-3-haiku-20240307').doStream({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toMatchInlineSnapshot(` { "anthropic-version": "2023-06-01", "content-type": "application/json", "custom-provider-header": "provider-header-value", "custom-request-header": "request-header-value", "x-api-key": "test-api-key", } `); }); it('should support cache control', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"message_start","message":{"id":"msg_01KfpJoAEabmH2iHRRFjQMAG","type":"message","role":"assistant","content":[],` + `"model":"claude-3-haiku-20240307","stop_reason":null,"stop_sequence":null,"usage":` + // send cache output tokens: `{"input_tokens":17,"output_tokens":1,"cache_creation_input_tokens":10,"cache_read_input_tokens":5}} }\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }\n\n`, `data: {"type": "ping"}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"${'Hello'}"} }\n\n`, `data: {"type":"content_block_stop","index":0 }\n\n`, `data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":227} }\n\n`, `data: {"type":"message_stop" }\n\n`, ], }; const model = provider('claude-3-haiku-20240307'); const { stream } = await model.doStream({ prompt: TEST_PROMPT, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "msg_01KfpJoAEabmH2iHRRFjQMAG", "modelId": "claude-3-haiku-20240307", "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "anthropic": { "cacheCreationInputTokens": 10, "usage": { "cache_creation_input_tokens": 10, "cache_read_input_tokens": 5, "input_tokens": 17, "output_tokens": 1, }, }, }, "type": "finish", "usage": { "cachedInputTokens": 5, "inputTokens": 17, "outputTokens": 227, "totalTokens": 244, }, }, ] `); }); it('should support cache control and return extra fields in provider metadata', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"message_start","message":{"id":"msg_01KfpJoAEabmH2iHRRFjQMAG","type":"message","role":"assistant","content":[],` + `"model":"claude-3-haiku-20240307","stop_reason":null,"stop_sequence":null,"usage":` + // send cache output tokens: `{"input_tokens":17,"output_tokens":1,"cache_creation_input_tokens":10,"cache_read_input_tokens":5,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":10}}}}\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }\n\n`, `data: {"type": "ping"}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"${'Hello'}"} }\n\n`, `data: {"type":"content_block_stop","index":0 }\n\n`, `data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":227} }\n\n`, `data: {"type":"message_stop" }\n\n`, ], }; const model = provider('claude-3-haiku-20240307'); const { stream } = await model.doStream({ prompt: TEST_PROMPT, headers: { 'anthropic-beta': 'extended-cache-ttl-2025-04-11', }, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "msg_01KfpJoAEabmH2iHRRFjQMAG", "modelId": "claude-3-haiku-20240307", "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "anthropic": { "cacheCreationInputTokens": 10, "usage": { "cache_creation": { "ephemeral_1h_input_tokens": 10, "ephemeral_5m_input_tokens": 0, }, "cache_creation_input_tokens": 10, "cache_read_input_tokens": 5, "input_tokens": 17, "output_tokens": 1, }, }, }, "type": "finish", "usage": { "cachedInputTokens": 5, "inputTokens": 17, "outputTokens": 227, "totalTokens": 244, }, }, ] `); }); it('should send request body', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', headers: { 'test-header': 'test-value' }, chunks: [ `data: {"type":"message_start","message":{"id":"msg_01KfpJoAEabmH2iHRRFjQMAG","type":"message","role":"assistant","content":[],"model":"claude-3-haiku-20240307","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":17,"output_tokens":1}}}\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello, World!"}}\n\n`, `data: {"type":"content_block_stop","index":0}\n\n`, `data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":227}}\n\n`, `data: {"type":"message_stop"}\n\n`, ], }; const { request } = await model.doStream({ prompt: TEST_PROMPT, }); expect(request).toMatchInlineSnapshot(` { "body": { "max_tokens": 4096, "messages": [ { "content": [ { "cache_control": undefined, "text": "Hello", "type": "text", }, ], "role": "user", }, ], "model": "claude-3-haiku-20240307", "stop_sequences": undefined, "stream": true, "system": undefined, "temperature": undefined, "tool_choice": undefined, "tools": undefined, "top_k": undefined, "top_p": undefined, }, } `); }); describe('raw chunks', () => { it('should include raw chunks when includeRawChunks is enabled', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"message_start","message":{"id":"msg_01KfpJoAEabmH2iHRRFjQMAG","type":"message","role":"assistant","content":[],"model":"claude-3-haiku-20240307","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":17,"output_tokens":1}}}\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}\n\n`, `data: {"type":"content_block_stop","index":0}\n\n`, `data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":227}}\n\n`, `data: {"type":"message_stop"}\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: true, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks.filter(chunk => chunk.type === 'raw')) .toMatchInlineSnapshot(` [ { "rawValue": { "message": { "content": [], "id": "msg_01KfpJoAEabmH2iHRRFjQMAG", "model": "claude-3-haiku-20240307", "role": "assistant", "stop_reason": null, "stop_sequence": null, "type": "message", "usage": { "input_tokens": 17, "output_tokens": 1, }, }, "type": "message_start", }, "type": "raw", }, { "rawValue": { "content_block": { "text": "", "type": "text", }, "index": 0, "type": "content_block_start", }, "type": "raw", }, { "rawValue": { "delta": { "text": "Hello", "type": "text_delta", }, "index": 0, "type": "content_block_delta", }, "type": "raw", }, { "rawValue": { "index": 0, "type": "content_block_stop", }, "type": "raw", }, { "rawValue": { "delta": { "stop_reason": "end_turn", "stop_sequence": null, }, "type": "message_delta", "usage": { "output_tokens": 227, }, }, "type": "raw", }, { "rawValue": { "type": "message_stop", }, "type": "raw", }, ] `); }); it('should not include raw chunks when includeRawChunks is false', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"message_start","message":{"id":"msg_01KfpJoAEabmH2iHRRFjQMAG","type":"message","role":"assistant","content":[],"model":"claude-3-haiku-20240307","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":17,"output_tokens":1}}}\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}\n\n`, `data: {"type":"content_block_stop","index":0}\n\n`, `data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":227}}\n\n`, `data: {"type":"message_stop"}\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks.filter(chunk => chunk.type === 'raw')).toHaveLength(0); }); it('should process PDF citation responses in streaming', async () => { // Create a model with predictable ID generation for testing const mockProvider = createAnthropic({ apiKey: 'test-api-key', generateId: mockId(), }); const modelWithMockId = mockProvider('claude-3-haiku-20240307'); // Mock streaming response with PDF citations server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"message_start","message":{"id":"msg_01KfpJoAEabmH2iHRRFjQMAG","type":"message","role":"assistant","content":[],"model":"claude-3-haiku-20240307","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":17,"output_tokens":1}}}\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Based on the document"}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":", results show growth."}}\n\n`, `data: {"type":"content_block_stop","index":0}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"citations_delta","citation":{"type":"page_location","cited_text":"Revenue increased by 25% year over year","document_index":0,"document_title":"Financial Report 2023","start_page_number":5,"end_page_number":6}}}\n\n`, `data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":227}}\n\n`, `data: {"type":"message_stop"}\n\n`, ], }; const { stream } = await modelWithMockId.doStream({ prompt: [ { role: 'user', content: [ { type: 'file', data: 'base64PDFdata', mediaType: 'application/pdf', filename: 'financial-report.pdf', providerOptions: { anthropic: { citations: { enabled: true }, }, }, }, { type: 'text', text: 'What do the results show?', }, ], }, ], }); const result = await convertReadableStreamToArray(stream); expect(result).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "msg_01KfpJoAEabmH2iHRRFjQMAG", "modelId": "claude-3-haiku-20240307", "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "Based on the document", "id": "0", "type": "text-delta", }, { "delta": ", results show growth.", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "filename": "financial-report.pdf", "id": "id-0", "mediaType": "application/pdf", "providerMetadata": { "anthropic": { "citedText": "Revenue increased by 25% year over year", "endPageNumber": 6, "startPageNumber": 5, }, }, "sourceType": "document", "title": "Financial Report 2023", "type": "source", }, { "finishReason": "stop", "providerMetadata": { "anthropic": { "cacheCreationInputTokens": null, "usage": { "input_tokens": 17, "output_tokens": 1, }, }, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 17, "outputTokens": 227, "totalTokens": 244, }, }, ] `); }); describe('web search', () => { it('should stream sources and tool calls', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', headers: { 'test-header': 'test-value' }, chunks: [ `data: {"type":"message_start","message":{"id":"msg_01SZs8CgARn2ixN9VnpjE6WH","type":"message","role":"assistant","model":"claude-3-5-sonnet-20241022","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":2688,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":2,"service_tier":"standard"}}}\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"I'll search for the latest stock market trends and financial news"}}\n\n`, `data: {"type":"content_block_stop","index":0}\n\n`, `data: {"type":"content_block_start","index":1,"content_block":{"type":"server_tool_use","id":"srvtoolu_01WLwJ9AzAmNar5vFyc4ye6X","name":"web_search","input":{}}}\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""}}\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\\""}}\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"query\\": \\"latest stock "}}\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"mar"}}\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"ket trends financial"}}\n\n`, `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" news June 20 2025\\"}"}}\n\n`, `data: {"type":"content_block_stop","index":1}\n\n`, `data: {"type":"content_block_start","index":2,"content_block":{"type":"web_search_tool_result","tool_use_id":"srvtoolu_01WLwJ9AzAmNar5vFyc4ye6X","content":[{"type":"web_search_result","title":"Stock market news for June 18, 2025","url":"https://www.cnbc.com/2025/06/17/stock-market-today-live-updates.html","encrypted_content":"Ep4KCioIBBgCIiRiY2JjZWJjMy1lYTFhLTRmNjktYTUwMy01YzgwNWU1Y2U0NzESDADUUNu4L313Df41ORoMeuawBfXo3gRCOLlHIjCrwqH7eyUcTYYzK3uGMir+55P+7F3DVFvMlpWza8wxKlGxFe8SEzZvK8sliu7L6toqoQmPOqy8Ovlf58WF7WiqWgo8K9zk0OZPm3k7xiIMJSS7jfk+VTAD+oS7zGeyiyxFXz6tJ/ehQ4h+ioIl07T+gMtnzvItR0QHlM8V+DcmpTDlYT0+/HqF7DD01Y1Vb5rq+XPTSpeT3zZKlUT5kcdnJib5uiBjbg85Duj2tNTg1PlxOmr8WMgpfM0rqhzclh4O7Hws6bAcTIYHwrFy3XrFFZjV4BrIKzAV9pjGfRnSYcUluLZ9b1rdOP/Rraeyql7/qVVsR9iOeiHVJXAIwy+fGDbamEZMNdCSfrqKXR59+kNxyF5WQ8QgVH0qLqzKBEmzwHkhtB4PuWp/kotLbtA8/nIUdNqi2EWYIcHZ/KPQq6b9MXnDFPvyzjEKAnCRHQUbKWfM9TLayLvZja2E0CYbck3YbezBpYYfyhf+euatxFmVv00X1i1GEd4fb/PrnP+4EtvDHYpLgcTzzakSpi/E0rEZf7wh1Bj8IFlmxoZeP75aJxvt3w43qEXC6aB4PuLZlYY4dFygS63ijxHvPHleXTSVVYjJJZyN1MvVudgvTw9sTSzC47PIPwJNXTN+JkSYcajaq+UfEviPE5qDvqRqorRwIeaY3791D2vhOb/YMCmQEIRV2D0zRAh0t9GJNBVG9o9EBujURJobrW0cEVRsc/dfi4nQsM7jZ6cgUR9uoqPtaP8dpk67CUiUTfTNyIBMhr8aXlk5hdY/iK7Pu+JyStkplMDl3IPdXdhP3jz3xH9KcduHlPtXnhW37IizPlx8wlaQcUxG8bDLyHyH9SF/3SkYP9SWwT3hU10/qrsJq2ka3+YUkqu5EIdOv4ytFRYJbJGF1BhS/dj+Rh3UYRhuZCBxx3o4xghboFCamMhrg75AqZwimgv4ouo7SXzZSoW06l6DAZJFB1QgIia0LvuUDCc8MiS79cpWGIjg029yvUx1mefHIpbqbCVOwECmLj2doEL/gqxnajD7KK3RGYuDobT2GTwO1+YwWWZABWm4KIAGuB9LcPz9k30K+KaVquN51GWR0h97p9HtG5sFbDhLkl91DNQ1eWI4GGEFoUM2Xzg/2k9rpg4eh5MQvvZVSuFMRDih4/MLYFVl4yDsBfehoZ8niEXCV/ObPgUDy6enIsdQhp8uFI67TVTCYH+EddwsQ6z8yRPQChxNnrTsUcTY9msqRXQj5lMU4p/heWh1LU+Dc/FwsaJkj+luq4ziounR8fFI1O+S8iiN+2Fh1s+zoMknaKtXCqzf7FybviFcPXahHVHfmeyg7DhhDAjpJouva7XoYkCpvCgh8TML4h9HynNfKDQEpPFqpGzt360ndbEKKEZx1R7D/yW3EA+mlR78gAv4AHzf4dwqqyqH6jOOVwwTl6sB+zID7ItNPyk30t9YKIjCoLfzaYwOicZA/RWyzzxWmHpUBC7NaBnY+LFMge08e/AKqzluxcN/mdtYMLDOqBweXvPUMWWkwR8aA7pFH6NYsllOm34MOgLm+Z+pclrkNOsGpV/DrOmP5uCSkJgJX1e3EMWCsOhHVUtPqXMkeLpxNbEKNzDG8aIsmA9DblZoXuCtWfmOOE+L7Yse4KfmY2cYAw==","page_age":"1 day ago"},{"type":"web_search_result","title":"Markets News, June 18, 2025: Stocks Close Little Changed After Fed Holds Rates Steady as Investors Monitor News on Israel-Iran Conflict","url":"https://www.investopedia.com/dow-jones-today-06182025-11756849","encrypted_content":"ErMhCioIBBgCIiRiY2JjZWJjMy1lYTFhLTRmNjktYTUwMy01YzgwNWU1Y2U0NzESDLOuzu1pFWqj4VIzzxoMuGtZ5jbcPejo4TWWIjD8k8WALeknPnGbCsQySzPN4cYNjn2ZhnNeFncbS7Ufr4ibpUUWtzfku1lSrU6Ry54qtiCimAlxEN+wPxFi/2lJKSEFb78ho3eFnmMacGL6V/tLMhojtn3r1R/rdfP5n+WW4cgQ9zoeNldEVlG3rIrlFLV4O/yidcMvN7ekMLjvsML69Cf0VhhcV0MOTSt+dXCdBLzvSx6QtZ5b2K4zGZSQBSyGcP5w9kT6fdS/dGD8/0I7SBlfZ9reLHxQ7tuRSDjaERZptyKgBT8lGHKmqeHKCPACE0wOA4R1O5tT+wNaj0amPeH3YZp3IZhrzS1CjsMdWii2pT0zHN1G/0RdjBXEf3uJvENOJGe4rfw6RwU+6gEmyFAq5SJrMXjk6Nfe3wsVd6n6lowuBq4BJ5rRxLlGZ+bvWMgvLbTdYtIJVzTFBUTJjMHPvP5c7Pt+t30uwqvFYYVZxQbuD3vARMY1RCT6rBW895+s/VMLoKWZHvaczJB+sRIeq4u/KjCkebtTQzC4M93wPUcm7Qj+07y8gHjNE7I0H5Tiir1H9F96o/8nIB04feBVP+syWaHWeTLZZP0GiQebiRpSefw4DkPxHJ3q0eC0jYphc3Vo+o2SmE33SDPYX1l3tumTbNmLiyprCgH5vLOwiiDCmIqMiZT6QnAEvGX3qjT1aZfpQ6jhjy6qm1LdyZBOXt8Milv8WkoWqtxxJ7WcCax9DPQC/4+sRn1sqJx26YhF4zQP1CAiwYpuppWNpir9q4N5w78JapMc7BAPFdkHgtmMuphdZTSLDiywG2nCxSnWw3jq9GXiFPHL77BlYhcv4p/bBQhEEsX3vV1UfAM62aj1gBMlWBj9hIL5+zQEedDunOOSqH8kRxYGbVSHbQYvtqL0tIYDJa/Lj3r9QocHiPueoyrjEjEsa08ZD+8iM6zM9m12YzaIkQ6bvb9lDAss4pHxTQzew+S1CN0XYubbil4YEaSrmZQ6QIf1TkkYMky8phzWpoSM38aOT1u6Yejmpzu6ITI/8SAWiukWRPX/ze94t1tbHJZjquoJ6PWyUPNGsf+E9eJ/Ymbz0+WJHMuDHCSDgkh2tdlwPsLBivIrFY7z0RTPBVvaRS3NoJix6ch0wYRZSM0w6O7FTxcGyqHC6ZQ0ZRWYb1gRmXT3gvYl9cft7I8sx7oJKREH09xMT3Gt5GDEC/Osm+F2ArCpsKRlsCkDRsbfqGKmRsl83X0GFexjWRPmbCtQ3/pwwEzsAEK764UPE7eWB/JIfF49+uo9LMjQm9b4kyxPWcuyG84m/bmfSuzCvXESaQk9rQXPwU2LEIXaWA5DlrZcmQtFBX+K6CA54eWauJPxZeSiJLHwysAOtSQ92Gi3Rdsuy3BILDAiYjd7QvrQCyEJPs5vRKAvYLX5796jiK3GUI7orp6oMNYvqMt+ziyENpQfiupXAkRSD41CSL9NYMH3hiKk3npIIZG2MTqgiiZ9fV//OoAfLqfteyLqy82dzQe7ySyN5ULytnEFr0Ob7aLiKo6K0Il2MCDWqtMCkshIi+lLpJLTXwk2xbYBP6dgSSsd1lRss1eVPY1Aku0junoUb4nFy+MNVpuzKMybgaFWo0HfL92KzD3ZHgwmrCWOdMz5RfVsF/JeOXGnsNKkC9XeHBSEO9IvYqVKnCtYFQx3bBA+gD7S0nwNUIizZil9UBGbaPm6aU0bs6WFlD/i65JxRx3ZCb5PaTvWeObphub/uvYkPPoGKcMVNV7vME99TpOxHc3uTe2xUTahz4t9q047E/HCDDa39zYCF+HNfD30p8QWi9e5VtJfAUZBSet4CkmYWWXSCj4UkMA3XcqLbOaZTipS57Vq7GR0uvHlZH3qkyZkLXQaW5eQy8epcBpwtWIezkzx1rdcGu0xLad4XsICIjoi3A3VWd0y/kh1vwEfV5vcNMg1whxBaxpggHQ1HXcTk2WRDF/nFLlCRGQClHmmDedT9WIeU3GGgKCqQRcuRxIdAeyCf5KFln653g3MecEAf+wg0v1/zkk9qa6OZ0JmW2DfhfpK7WobkDTdszykYxJXvj+MTQKCuQqOKeu1lHzfNiuGJhqFn3VmT9jpllO++2NRxdQSW9T5RnYFSS5vwJwriQt6aPbKTvQNzrvR/CYBkCl5a6gxoSaz9cCWfBduiDa40bv2O1DrGN+6Npm9YLTH4FNrjDyY8EQtj50kjt7K/Aa4HG3SbfoQF6sSjuSoP+8hS7RLRggg9cBboqeyP1AMasOvm2YmaM9JURBa7vCFO7BLkaju6rkMLeZAH4K2rfSMtRRByRT9NnP/QvTvq18So/L5rfEijYmsD7iKq8OQDCoRg5wONSPMTqE7KIWYHBAioxDaJVgfVjKC3cRZ5ewzavJJzhli2QJgfa5WBBlZAE6szCdhQ3TGUpiOA9nkHDzVGiWPHU8+wLR/tz+me4CXGtuYwktQJ4sNQADru1waPaM+f0GaePBoX9YwvF4hhDHGXy/kmCZNfoVvdX1AtZUjvVTQfyK1iGUJ6mkVxdzH5ZyjOFKnc2e/+i2ADZMms7+1lVz00mhWGJ2zIBZxLXRxW1/QGakqGTNamtWYDII7h932mGXAJzxa5edNDotWNHIdUkxCf1bo8yJUixw9vepNG+igRakXijj0UmxAEg3W8t8rQW+aQmVYUn8swoRRHLmU4GdSygVo3clV2DzAzFSeeeTai1ZZNbVJRc49dV/Cv8SqnhMIb83EhHeiF/Be4kmeaAq4xvib7VVnsx8PkNEZGsNB8AQY93HyMC/IBMXQtzSZ3SkK4qBiGQRfFQKjI9Z1dH3SZDii1xrUCNdOqLy3+YiHzhaIeION1nO5jDWk5XObH7orTdpGcb+3o2Equ5XVzj+fJUM+1ABlT55/Aq2R/i6VwGlAHSnOSLPgJrE/FfrZCEgfv9/pMTsCZiLQOUQrftr4I2d+h95kp4qnkXm5Wu8JZzMMaCdvlU3kgGdFdsWSgkXrztE1Lc1EoQQkIIkReyE9ijFsdCZFq2c5XpkMpRbcQNhhbCCLZ5zayK6HZoUC5yh15W3qSbTkyzKjIjSmdAsTk5cNVpk3xIpj9PFhX8pjiwdR9jk6u4yDl8J04dMGOrhK+Fyf8DNvKlHgVsv0bjCr1XNypaIsRaqr41riCP29VMj59zdmwD6a8+lrMZafoRcy1Wdr2UzhP4xWiHPXs5kUZNETCo+lyFUIqoaeyXe3BaVXSCqeAxzW+hO2llS7Egtx+d4k31Mr6GWcqZQYJl+QxnhYISii8KoGDp7b/V8Iv0NYHpqHThKwX7GZpRRntKwsVpBrhDHsjaxhxZvwljCDd6xkoZu962Pbt+a8Od5XKWU3H7cGngLXxsWgawzRIz76u+B0Q9B7n0m6JbSLQFxVY9V2nCc3K+RLuiHOc4viBtw+9RMUtZk92nnEfAJgUzsBmklLOADmYrif6fwrnfEa3X/OFFNb84h+m8Xud+67MAHPCjMAIw5B9yIZZR+D3Ffos1kCx13DFrGTY/OGojTFvLS0ZMcuzGCcvUDYfMi4mv94NowTVgkf9jTO3Csr0DalXQkVEBje+H/hcoXjjP4j6uXAcXHyOGd0fMAC6BOwy7NRyA2FJ+IKBhcFu+OvJerMOoc+fkngeP7t7a5XZIOcaSpEAYnRK630AdZNJ/voDzgtKkm9Ynua1hQFdCvghmjVp9XBWvnuQl/omQB6xG0TY6C3CmZ0Y6Pnc4FNsVn+Ve0j5QMFr2rliNvVeUFMfNlInxCCIg8PaLF8zd6beLqaSXuBAjyzUbCHZ4oGa+uRh/WuIdiqd2huHlrGHRL3Ed6nuqgmAd8KjhCPNg8U3u0b72pM4YUZCswfTU21mgvCv3K8tL54iroFdqaMfCafEeiz+c6Enzo1oSyeGnMyVw+JGFardAY/JYZgOn26nnXKfQCg2QVshCIzzEJsH6+770vnGbtUQLM9EyErc/co9PULDtwhg0xPAb0Fv5ft3VhgjQsS/2LMlxgS2Q0BnppC7StWY6QDU4Y9jLCUO8dZqa84HTtSML6R6FV+MdXegwQ0+OtJrn1oUZBmmSIDH+oHce0eW3ANEaL+DOTelx8eDTve+PIJ9M0TJ/+4JFv21O0kanEnkmBI9dfG/W/vV0KnjJG6uuvu7pWPk2BhedEJWcGJWDD1THpGL5geaF/1sh+ZsGO54VsKds62QuFEN3cJ9XfA7f2GsDH047tiEqdketNH7yatrssoOATEyMnytHUp+a7slOdlk7pQRGLojyKvjAGGTHI6Hnei17eR50SNd+VOheyzo3WKCHcNFfJ3wt0kHzJTI8fazgdWzUzJ2KIMMpIv35jABMxvyhZY/znb9kXpg2105EmykzHbqlCz1eUMffU4xbA7LE9zQqetbCTmQwcwdqaMmNwSTmFTwzcsSbsMI7RhTN1Fe69jAYD0cZZFIzjntXZRlwxevBY4jBTSE0Ol7MXGVJ8IIw6JG2xTu7kcqf4TZbPTvStEwbJcnDYDv2ziI0BcNJ8E6VB0ZRf2Vr6hBxsETC7EYOPkqI7vGZvLsDsR/NtkYstWr5SyNjFdkm4sTUyh/WhF4PCLMBl+7QnHu6m52LKmoYp+1/i47jDGkjjBVudYW9M+TH9jL785EdJNTApbrAnpMONzFinBDrOucLskEEjAKXyTpthMp6DFdWLGj2K8QrQv2qKS1Zf/sEfUfPsflCtWmXiu3nDn61rXLv4NloDS1dBP6jIqxDKXn+jldSLNhy78pWsdCl49tMqL1cxWufimsT+gcI9EpaeoY/xHFG7rX+NjrFC8rC23R6AAA0Iqkge7baKX18XS+WIpt40zWbt9R5NW2eJllvmKAf711JkXZM9XTRZSiap5vprF9b4NnLgS9eokbb4O/IW6WIb0TJ51y1h8PFhazA/w7BSquD6nm+zndGuimt/RxhWZqReDJDryCyHNTYfgN82gucg6B5GkWJ59gTDA1yp+fUonBUQ5mxkgEW3dmE3KlXgJY9LZv7ZfHUQJzHnkt8hodreWjbuIdKV+vOLLJq7WCkipSv17H+y75TyrNqBR/jG1jWpGGV8XqpNGrfsDa5xQWV7vz+RGLD9vqEauzusI8kHD3TNl4rTJjD1od98b/gLRMmk1nsHijMxuQl9K5z7rcakWRNRobvEySkp2VYOWwjo7F/3Nxje9v+xOwYGKmdWVz2XlL62fJ63Syiasn+yxTbnPwNWCihgdeJ6QqrEHmzSrdutd4kKHvRz4MuXMaN3m+OHaDFBfrjP30zuZ9YmvLMNh0EWJKCpHV/ZiodYBgjaOQAPPbmYGFLtHm64yJDNerQgNnt8Yr/i9ifiOi1NUSEcnZv8OqH4Y0S+OPP6yZlgBMip82S/wz77eII1hf0e/VkDjB1LV670d3et2a4QOtaaM1jj7acZ6/0CG2vsk7kYO0w3Bzmb0vYJp1f0szKUYQWygqmzH1ZgLm/ppHnSKxZpHpvuEAO/5zQUggenBH7sYUBjt8rLsMtrGwwCBPZNO3I5TQehrFocYC26geTradQa1EYba5BKQcqXDNSfEOPOF6b9Adky5AMM1wTIsEroXMl8f4gjxQbQqDUwM34rel6OgdeYc3Jdw7s8lLpjKC1UgGAM=","page_age":null},{"type":"web_search_result","title":"Weekly Stock Market Update | Edward Jones","url":"https://www.edwardjones.com/us-en/market-news-insights/stock-market-news/stock-market-weekly-update","encrypted_content":"Eo4aCioIBBgCIiRiY2JjZWJjMy1lYTFhLTRmNjktYTUwMy01YzgwNWU1Y2U0NzESDIu5D0YHPt/Cj4tOPhoMNdYvdjdIx/JfnkmMIjDWylQjzTr7lavagcDBjMyTM+Zv1NEVdcVo1l5YuF8R606MCc2rp8KYiq8R5ofuA50qkRlU/CnaJ+nsGpRsJD4x9DsuCZxoGuT69y9tEyN+6+lz1y+4DKPa+lFlJVC9gkDp2NNDK1SldsR4snGBx52iBWM4vE/J4qvuoGhU21KcPOzXwMhmHkHsvIX3uDjysVOLFM8zpqJITPkVDY09TEm2jinZMETaQ23q7qQJS5TQMo4Q1cajpZEpAktfYO29RXIMtfonRmzl4K+20W7KxAa70PIvT0+b8yPPxX9YkoS20cq5Bl44e0eU+ICbjA0662hS6vPggMCz2RYavtMnSfDmUSfQk38KNAy1dUlXACNsCnspmX7nHMCevykz6KsXYYQrvQ9U3huHYUDEXs4v5b1P3ZkW6W2/ewdo42EaqKQjnJTmUfjy9xXpER+TQCMm+Us+frJIRntmlf/6k5Y8wToEN2vdC6Pmwz/rZ7bLNdvQB5nV9j5uDJ/6Xo9iXupNPRhVz8JnzSYLoos+Y2dkH3LL89Y7WYNV3LW6o+wBJ0HgtUFFsY/qoFtJdvJWko7X28R0cEeWHwl1674cSomCUtlEuALZ6yF6eZ7R0Q3DY40bfvmL7ODodUSScvPd+p5pYoj/r49a6QNzvwCr9LJ8GAiw58M+2krclSQAjJTtYUTaRyY6AFO12UxYuOJuu1TNu4W2TpZstTRzK6543RofA/aSpFTOrgpt0vuCwwqjWIeKowEx3DbUsiH6OTB8md0WWu6kW4g1IS7IgM+14fJ9bXfkLCggCtR6x6OHKNQlj22GPNEL19oSSGkFT83HIj5W2k9ShdcLXjMpOkClDoUhcFuyYTC9jv11uBe0SX+LnyoRGZk7tbNR8QiouqUZz/tFyfEOl+aFNLyHmobtagnJpWr2MWarZvNvDqvq++DbGDh6OkY0oqf7JjY8ZI+T7YhXmfc+/1Fd+yueVCOnyajhzmp3B5hKCtFddQyrSoJnnI3+VdcmJHWhjcOAyzx/9BWLD0xLufNkvwey3phhaAQrCt7YPsoOKO3i4JuWL7aEayP4Dx89IQJKfgqLXKZ8jfq5B8kCF2L15XVDMOru/DA438g8uPPynhgdoRHSRB4TNPNSOI2773Zb7Hbo/aMmqAUlZjFbEwo7Xy3cHya+zd7uLXYPoiazyy7N+OEH+/d3aps+9HqKhV3+q4qHJXIk67JlDhDMhbNYGjwzxPwsxG4V2OW+mJO95l15ARm8bVv6X40+VKGXFcq4w/Wh8Nqg9Xm89HhvEGrGreR2xcdya9S23FQnSmSdHarjDd1xhXlpi+Nz4xGCrdeWCCv6f/klWoP9EhKkmV5yqWg51hzluWVYrQaNo/E3ugrCKOS/jgA1gQiVJiMqDQaekzxGvVLnlRDBiCyrrm5oSRzcSY0kuYZ+Qj2PmQHi7wI78YgeF5DhXsYpDyzNZZ/FBkLiXPnj7aykVDFvcr+RnDtdHSJHflivqJDmN/nLG5XwbxtGPRZUYFsvXJFukxA0RjQqE1dor79cFrQP5wuLFgM7lPDFeVVYkA7ijntgxEVFAhRwMDp/2OT/KkzP6vniIYHCYT5sgXR+bHA6X9mWCquJTyi++vtzQATVR/ElvJFwApmf7FF0s64ekoOCvr16TqlFhLL1KEynYESNSvqkNYuqy0zqC0D4AJnQIc1OU2whUpWTS9bsanoJvqH7uKyY9pnNj5xmKyz7mpQbOzusWWsYYgg7pQ8OdaOaxGqOziEGmQu7gG648Z6zo0kTC3wmDrkhzUw7HbG62U3jb0GkC3A1CiU+Zz67ZhhRdqazP92OUQLo20Diud0hAy2RjzvGVZ3QjjMldIYL9vQDIeQueGvwzzO01BDcl0g5fvs4KgDguezTmuXJHudx02nUwXaBJG/UqDBGYYQSMZUNw8KxVDFmjMq85yVwY7Tr1W1/GXzr9sXveHLVc7oAwQurEGrY3jW/tn7OCtofwADiFS0rgevJTJbmw+2zTbk8FnZIL8YcDc6/WedqbTQxymXa9clOjZXb+defQxUTxlaLgGGq0NNj44kVMT3yRCh0TheVi+bQzZ3VqJecForWU9FDH1u11LchwzEyOSrsBiX19afCxJVPOV8fE3mt6AoFiphX5IFcrmR6NrLzW7VzlvLjRFnaYBG6N8Cwpu6ft1sOaT21OEtr6krq26YcaJBZMvA9YiRvEHq+1/bdl4U3fAVin2mYfaEk84vpZ/JCuzaA5f02KvwRFCiEnmjvk/QemtddEBFlGA85q/K7MnOE+9/yBgCsCtvpNm9myn4x6eqwVXgs3XmRr+1n+ysMoZULmb+53ul+FKZ8xD2UcgZhkgZQ5w4+CjCyMKT7yDC1qXahfsNJMPF9nuashwh/b6VjCb1mcSDQVXOzSF1olr498c8wjj6dua9oMonz99h/Fy6Z34mOO2rHDij3ZvvmWedSUu+bhbqy6CTbVjzmQWbSXD2MxtJ1AZjTcd/1aoS8W8PzgWVOSzQDZCEWYhxHnXVYj9nNkKgyAlbdh7wasXyJ5orkVxke5jgQmmPnKMawmmiBp/y8eCJ3ImLc8D2xCpYOzXSSWnDp7QrUwlfciSVgPm1s3J3yw2FGm7su0+t57TU15QAlrU+U1JPPx1du+1pBp7kldm0GcZJ9tqcRRNBYvNHJv7aEA7x2hg9C+IEY7KXcmsQJeyoyvLU+j+Db6/MtpYsXfR1vYWoT9zzE2kitjhwnFfuXFHg0fgdDJq9fneKQiym0tbILEox63QP6p80uXHcYUqgmA2AsNr6mE0fT2SFWkccbBiTinjUZyEEF1oDqf8E8VCoJ3HE4URNFvoYXnZmBuQEsLwiJ/sGEZCqRydQhu5kktE3SWUGtNQ7r/s1TbSmdfTbwlRNj6CFBkrXLsuCcmbk/VWz6SRmi9x4l8IzedR9/1u7wWSYIWokbP40e/m7XYLFXY1qDgyqaG/AYohqO4uqNCopshC+D31qZhmliRSB4guOPV3nX/aZNrPLOdS7IEtqOopS04NJNY0jddKsOQjA77rrKM8qH3MhusICn6Ll7fhNOob+LAmXshlIhLvAcMhWI0/I2oXHSWJKKpTOLO2MVT0BBLH3DbZwYKjWKiT2OK8+j2Kj+/Y+ETSTYGUWw4h8h1IWMMkDfvxqefUaDXe84DFLi+pS5qb5nmJ5GDotxte7MHWyMsxHaK6qVeWLcVQsZ/bmLhukd7cbj3Xq1603qNo4sys4oJ3YII86DkHf2mm3yqXSDaLQ33n+h6bCrNaoko3AEF05u0dhMgVLjHwjXqXmMGyJXgv83ZYvcJPC3CUJDcyV+0Wntt+0Xsa0Gec8z4XWSkBjr8LWtDLEdicJ13kqYn25Mrc6al3JvRVaV5jCDDA29zgN7RGitYKNtmBz6tgz7sNItxAf4gjqThw0ORnN9H3CGKT8SKyrpZON1cZd96/Pm2Ac6jK6fkSfngt1neML3xgfL/sK2BBvMNpCuBFVixhmd7C34Q8InxUuEeDQ1UC1qx9BzH3SL6Q/nwwJmVE4HVroDfDcEnl+77FSXH7p8THGCq0MJwHk5Bdlv6G1QtKJUVtIGJ6M3B9x3dujrrHY96pwJOApor0RAILDGTCcemVl+uA5AElAsNofgI99nbICqDaCLtWBtRHh/jc2aHrCGngEOiACytynCT3yVutQKvMynNsa/sKSyeYFj+j2dPwZoYT348tA+F7EwO9Me2HPC0nQXes9g9GQQXX2ZEi6kMMTlPyGUhomhX9AWyouAZaD4f0CaCH7TYhNxzVV3PH0nIz9miSOH2gj6QpgavHMdNaxDKuyc7vzw7HVPbLwDUCh5vlx8ETyJCr1N/YJXpBS4fO+O29KP91KgO6wGmve/vmTOKkTYBAViiEwqHk15KUd/XtKCyShBTyiltuwl+XL9Nzj6shVPQH1IiYaZEtviuy0PzAcIHWzgxRDMzs2OIFtUmC6l74neAxwjQhjvhLqiI1FHCBCQJRYj5bc4hNJpoGI/Lb0ORQrpLveh6Hvllfbk5e0JBdAoPdtu4QQH6rIK3Ket6QDeuc3EjzwMmLc9n7IyokhEql1x2JnvG6k9igDW7oOkPvIZTQrg7sLnM2qPlj9qhP2mn249Gy9K7b3SI9FWxEy3xrveGY0nDFkldrVdXnpaMo/WYHOm5PPx8P3l9NX1BKX0wkfIqs7iCpSQshNFgH+RbilFpd3vfXRfnAX5BxuIfCS8sX13v0hhgzzBuqiLuQBrIpyR3c5QJJtNmKWeZIIR3ubvEYWfobK2We45ZW4Wt590iAmTUPb43Pwag8uWgQKdX2sXixZAV27wJKF3+qZbHV6yXwMwqdBepmxr9Ku90ncy1AD7GAM=","page_age":"October 26, 2020"},{"type":"web_search_result","title":"June 2025 US Stock Market Outlook: Has the Storm Passed? | Morningstar","url":"https://www.morningstar.com/markets/june-2025-us-stock-market-outlook-has-storm-passed","encrypted_content":"EvcdCioIBBgCIiRiY2JjZWJjMy1lYTFhLTRmNjktYTUwMy01YzgwNWU1Y2U0NzESDBkiZGLSjeDRJRKcphoME/ds2kL0ekLCxw6kIjCTGkSXPmnbg4GjsIlKYCj3MdY1Pf/Qq8VSZ8FIytPVE8a/+H12V0nNnRCK/YOogKcq+hzggUkA5e+9vyKMggmb9R7B1RIxotsX/NC1ay6P3nmGtcx6mm2DPOcnT0aVGpbSFIsw6DZtlbgSDUtyq44Nn5RfvpV09dLEAGlxXnzOWwZ6duffHfWKe8g3ctmmtyT3vw0+/OSqrPH84T40A7R9EDw3wcDHnY96pADVQIh6c19KSmErCCBlgZAmyT2JNJzFVEEAAaP1yBWVe/dfIZUGZBtyV66Mpx10FJVZCICb8cy+4Ik+LBu3KSxnVf0eNAFQpmexd0nHRuV8GNjHv+6SEPqd6nOiJnwv5OWWU1QuWzDOZUybS/YsOM3kH3BWnENK8HPZIlKQDqrRkZGfifrY/Yd5+7SoHH3Au6eWmApaYE0gb1u5D2+s8ZHgAz7mdqarMGEmSftW9CfqA72EREtsJ9uNM4yyLtOXvtmncxj/WxLGFQeYj8D1CH73QMvoJkH+dGq3a7VFBtYn0VyVdfT8ZK2p2esOgv8orfvMw4uHiB2yOTTzzla9BwJ/5vYIRF6jUny26yI2RGrUjbqk7BQnJJ+kK3xVDyiMMndFSWG/DvbU3NQ9Rjgfyf5Spz78nRIDUR13PcVKtP9XpMjfWy4o4a5uw3QoqS/d5KuoLt/6jUp2zqc/8lHXp5zyc4bV+SNbiC3xe3fdp5PkZa1BeIvd2s7xHeIWZtl09cKxU0JugyKAR2BL6PzmxuWTZ4FEnHcrHPYGPAf4jai+l4MhBxrLa4AtKL+m9eIptp0i0gb1EbN5EB8gU/acyNkxHz61GAzPGxbQX9MvKqAU0EvZTHlGKdhLaguwZxryAk/wgpmxsFn/YwCh3uoiqzXk1YRj0eqPoX5mzpvJ70eAXjNjkq9i+oTq9WkixDbt6AprBDTSNmvJ2v56hPNFLOFK6YNpszukM5Em6FHU3Dl+KL61PaUNxxtQtobncctF8YEdT4ikBZHyJNoN0btq1Pp961Fb3hnVn7SymaDPdL/7IcNHpUrJ2teC1tEo55qd/QV5+HkXN3PRR5gDJIT078Ubtn1I1beDGQ/ucTNNjQvO8VbZg/hnREG5LRnHqlQahPVoJblZ7jncIN6dYGTs6/uXXH7wMW8HEEp+1laWfp5KTVszysjmQEClzJpPqchmn2kDUcLWrvC7alTDtLHBkdYGtuvWxJusrChBkChQ3XuxO1qh+q0gaHLuyFwvxgMGq4z+9GI/J0W4cSL81Znt8hnDVZAo2zxRPNuvr7bU8l/+eKwUPwg5A4ycOAR+sPFxxWSPM/xMt5R85q/+0JeZgohDBGyZTAus3H8Jfx+H4AjccIucfmyh/oDqLFL//rXJUJx060f8oFUV6t3dIVjVDvpiEkDbZNpw5tyuecU/eaFWpsGL90FPwUpHdoUydYCWPGFu5ty7ATDE0jSN+z5aKSBgozaLdcgr9rHifcC4EbVZJPBs/IWDKFwZVsGh29akvu2sHX30uzaiMg7TXk8tNRevHp0+jTrQf0iY5164XTJfXq8B5eEY7j++5rr/Rw3UvlNvdzZd0iqYAAhipm6dfAoTipAreFyb6vwU/k0Rj6QMlZdOG9ygfnWTbpfmzyioo9srw7mBm9eG6ZDyRkzrxNnjqGafwF5S/f9uQsjWAJBopn2mz1NAT9tg0Q9CwxzRNwdy158ShoMTei+1nY7BER8t9GBmPY4TLx44250j2Q5c6LrvNRLxIzdCNVg0wWv6t6IZYaMV0mfebvJk9EExOA7jyVfhMswNiKObp1icc47iDM79PeQcshKcyLwZnekUVi1u2e0DKwqbEny/GH0gx5abT+VIvmekUaBR/ELipzoy8kJpq3ZfF/bJQW4lydrBsqe1NvllKNpct9/BkA+dC4vSkhgar92DwbFp6Z7y8n+xnje0H/VpMHbZetPNP3ecgb4uacs9j3sI3Yu4xwGq/NWchmHGSKy5yGdFeNy3b7S0zDeoIx99xhNS+uXqJDoDsF7h9R3mEz/uqBLiQG7Zb+vfLd02goh9ehygKH/rl9aQYlTMykqY0heQAbAYKWXKt4mcBwTkUJVqPPJXHepER1MGv0Fy2IspHxxekO9LYpNMkwfj0eBiAGnPqcfY7T+CYFqISKVT7AKmFmuM1FwA9hBgdKeFEKKUCdB9X3JMgSh5oaYop3dbAXYHhxmt4bbnSL5sePoL1aNnYIX2PcebWwnb4caYmkEcrCWtLQKZnPMl0gM4uzBnrwc9az0cOiiyxp8DwHLEKtv5/OKrGfXxoZCN3fOuaouDKuWnp38joXY6e66nO/N+a5xpfVedlFYRpTjSGkbTZFZL342j2kLOles+zvTmFas3RwBRjPVZXxbgnm3O1vcfoHa+t6jqecdmHb3ZCrWYvIsE6053Zr9cHzp4fS+fbjWb+gt97D529DpNLLwno3BZm7WipIdT0MviQrMm5luWGYMJ9mUbxp1ydt6DyTMHkh3Ijyh8hkiIpOU/TjHxHnlhYDX4YyPJty9mvAK41Pwosrq0v2hSAFq7U48fllJ7XS5IVk/m9vm1hT8+LdM0Fiwui4QZZAjH6ONHY2iX0a4A21RXGbydDmx7nYJaL/VmAIxI3bv3ezhi7RKd5PwfpCPu2O7ihU9nNYH34Y9aUFqwqVc9W1D/CU1L3Yvrp0Iv5rwqk+WeG1d1+mG3U9vKxvxKLzpb9wjbZ7G0QgZrR4t4owZdPkHP33pPko/YcfKJUIVfASF0szfHCpyoRg5YC7guqXVsHfdcB7hiW8WIEoAvkTMC7nmaq72TnRHG7kKqEFrvwoFGw+hYNM7GOiMF0qBwSFtiIqn3NoSQ2Ttou8CXnrdQiu+Wx4smbAxycWP2OZA4z6rIso4ftE3jVlU+krSdcgomHpCgAI/fQFHxbvZ8TnsrCdDUKHzAfvbRED11Da4/Q7Rl2RUugaKYfb6j6rE6NNUyUIQLCXeXMXoXIxw1evpK6fJMlkC+/DaTLwKjofV0oC3mxKZ2IvVxG81m8DF3mZjFHACJBxcjD9EluwSN+EG98tGmtGxfZGFyUOvPwbuWoDWg0S8C5XWFYmPEyyRipFyDuCxcT6lX+MtXOXiK1BfEGxksWeTjjCJ57yICblnLf3BdynbTtqe/kbFmqJ0pn5lRb4Vgc0T0i47EWbH1FZPY8lG07cQjbvIZPwUYMBfQ5bUg7yjuDNuhy4fPdaiOAQTwMcyHyMlGzJED1CrZdYRSwK2/mN4ACXqHJr/hN0yZPQkYfXpq5Kg6VkuNeHCLUcT0Zda3TzUXdqhBlIYdL8M2b6SEUrjbfoMsT1yWEluFsd8yLWdWxGmIURaKX0Q50V0NLuYZH/PtXtHrZApvAynZA9oPudKSmY5nhRsZUmG1qsmofPLTPlGnzs/LolMgF0mKLicr1HTk1hLrCixR/QkTbgUzVKKo0WV9Im1f8tvLoJcC+E5J5zxWRG8QihWs3YDXdJugRb9gXx/pObsITqSUglAF4relvI+7M72YdWmIU/6US+CnVic7eR94qGF3LBOosnzyb8odPJzW+G9UYARxXc4jzZ7+9KspW+10A48w+XAIfju6AvzEqF2wdTCqpsGyF0kW0AhvMGjr9YCEQIq8y7LjrKW6DgkaMEuKWPYlv28ZdnkWne3hw5kwwO7aTaZQobwVgkU/7I8EA0OWMugxZbMkyVPAFRqArOJfRCk97cbbKaSF7whHxaXufbg0TGnRjzsRoRp7aDsiyKTNHqwxChJIL9cXsRw/57AS4u1zWqswwdK5ZCdxlzA9iZ/W7iRGUEIK+O+OzCVZauacqcGp4mOpEMrJfZ/jIwil49E17Sf/r6ul2OLQ6friW8Z4h8Qnej1FtjsUfjBGdMW908scsXHiRZYqlLYyrOHHOwFNDW86BraGaUyh6Hb27dnsK3XSdB6yMRvUrE4782VcHm1ncwUkKCSuqcAC1WIHTeKOjDvQECwsW14HQLC0rxgIvLV3DmqgJnsqKLIB7dgnzHSJLEajw5ZBcNVvfMP9TWnVT3tK3GaBeg+qpXHj10Tzz1HJk7mulsld8L6NWiP1zbaNjAsET1XP8d7jrqWdbZXT8/YtP95Ac6S+IfKntf7L66TgqkhpeKcIiqRcj0MD1t1On1PXhGk9D48RNt1VBHkAcqsGGZRY5LAxEK47m4YVUlCEIoBDrYKsesYp7CJy7ZnBEnYNSS/3NvAFAk+GTQ8L1R9ro5ia6FQsDiCl3texK8a6Y0pBvtceIDAgBOyo9AU1cRp1zdt/e6xtu2r+gWtTl7ldQQpDMNhS99JdhlOMDc2CuUdlTlsTlb0VccO3WNgPDxTRbvvc1C3MO8emKzv3L4ugTuHp6Gpq4U7yq3DRwqZ+pckJltZML8VQe+45dA4JxNJUAOqn0LZ9Iplby1PCqXAk17TCkPm/C9Ep/oYq4BHBtmvyl3JR2GRPpO7kEEqw871iIW9SL2fp8Gt9bQxga5Db5+bc2FvWRp4rgWMe74Z9XTUsAF8pop0EGsltbCoanTGsj8SbMG2jSxFDm92EI5dyLu54HwAUUSj+xAni7rOoJ1T6B6MNaFSvS6GXbsrnr0OH3D8WzpR5vv1Ewas/0NilUci/ImmGnc91x2XUbG4jp6kviWLfc04uVqsIFOB50igeg5qrqp2lKGoafrAp1jnlap6X2wZNXucpXE8MdMoMjbSzDCelPaWe10mrt3vj6XM8UtVrr6A/c0ICvkqtA9P1aUuRD88DU63S0Jt+frOg9RAnD3yTZ5TQ7zEn8O2PBLZs21K09L8CX+K0X0BuYMCUQvLh0OvC2HcLudHEWYZRxZnWk2aTfcY9bsHSGRuK5WoiZ5IR4oucU9gujavQf3Dow7zIzjrw9x5vxVr2ZMrHCx/GPz3cpAngAVEIFyg69TpByfjuAKxSh7I+o/5RFz5ER1pLoT4tCBRWlcFn5HTSM3yVHacsL85dG2a14USen8aS3X3WOMsTtBylxjGtRJA1cQI9GP2CeNPAGAM=","page_age":"2 weeks ago"},{"type":"web_search_result","title":"Stock market today: Dow, S&P 500, Nasdaq stall as Fed holds rates steady, forecasts 2 cuts in 2025","url":"https://finance.yahoo.com/news/live/stock-market-today-dow-sp-500-nasdaq-stall-as-fed-holds-rates-steady-forecasts-2-cuts-in-2025-180946600.html","encrypted_content":"EscfCioIBBgCIiRiY2JjZWJjMy1lYTFhLTRmNjktYTUwMy01YzgwNWU1Y2U0NzESDEaoSWj9qboISdVnahoMLQClAfmtJ+cUcEwLIjDdvA3JKgmewq1qUFyHTg+P4F7YfXb+Yi1dyCzRfwqVSpIHRBPFgvc4qUy00ZYgxVMqyh7lnojL8MchsvpfKR2fseX/dl8YS55sBhT0yVavZyaabn+MDffuaRPxoxKBs+Vssl5YvMY7jRetgr89HAl08CZ0tJsivjXkbcnL2usDWTOD5obP+WW3wtTa6fY2rzbrcwTH5+M8jbBrUtM5KN0KEgntsb5RZSUsAQdZ35TRlADdBOMcfex7DxtPzPTK46piJ/9MrwPnmd76jbVypE/6FmCeRE/jpG5490HsPWZoqvBzCRkRHvfOZE6EDvrIFxlnRe4zO58g2mlrKIuPYHSIXQXCBUiv4eKxaFXX2BRi5QkFY/icfX0ZPzuQaS1OSEq30BsTvfRfwlAKMEwMJXoIc2IxutBBFZWkHrN+EmwMijb/mCrxObMj9OJirxaByfjiCVA60xhAB7/m56NGPvGwivGGLyu18HlmmXQ4Y0/8BF4X1kG2QLj5jfLgbWApAujryshdyZYY7XU3Yhi7ifH5Cfr553ecLabtdCXtgKtbfHRUttp9/TMPm8BNIS3ORDrvGLFJZBoIg8F5X2l7L+Cq2ioih0wcA1mSsaMdRstVX0csLN5gQzHgJ/Ahn+evtyXgdTuWIgETqIRSIRqaRDH59LodZzD0+PTebthsjXaewr93CJZoIutZ/BeJhTmgi5HyOje6BFOTQA+XDBEzW40ovkib7BxJmvB6buqQoORkVG5Y/C6Qz7+96hlHsC8ICX19m0cbfMl4McpE6zpjw4DbQ7id7mno7M6vJvQU7Ol0z0UCzp3HObFHfJKqN+AShwpR2yQNdv9MDURPYvcGQwqP8t6m+9IqKYo8fEyxAt0+V5Bodkh60QSDLAwBONvRjDGyGB6h4wvLyiHpO7SAkYiPiMx3oDow2lgCwoKNMYqTnEfXyuzeqdQgZ/Ej7u16mJfBIfppC18d9PEqt2i1o+VM9ZLHlYqXgylqGpI1w3O0kEZPT3Om9DlpTG2bRVsDxorumDsjIqrRwr500o8BlElPwC8U6t2IfFjOEnqY0LplZD6oWbDJPQQOg3eGy+6yux6b+/+rrbc1GmTq+W1VchTMSsz0n5hcisows2VQKqZTFNl/ImW6VUPueFOr4aLWyzhW8ikAVNOx7bYtPKrHhhfVE8Msi0nMSfzGlwyQ78tQmLd9j/nIEziK+Eak3XbNYX12jTjVkx7GL8vNj1FG6BnUq8TP/BZ3g2wB7qd2gpaEkthSNU6dXaSd6S1an7LN0oCCFhaOvycmWdVPfKWPQRXj6hJvvSVM021pGglLh79jr+kM2FI1n0MeVMr+Z+PM0Rk5GTw0DUSgWAO37unKZkktbWLchQYCZrj4/ljq6f/EIRiQ6BlY2puhJG6e6U0S1mJB+RClnGkpMZ2OgxnhF5CU9lZCtVEWyeDYROQwuxhxTyo5hAZmRWlNMHTujSmDO9WO62HKYCU6jRRXHnBIj+jOVFyz5CVI8TEf8sOILb/HnHBE6/LBXR8HuWpw5p3gTGwJYXkDGj3O3a3/3R32kbmurHU24V1rIxLLxrbwv4RKHytr4CI7P6hOVg9HfPaUqpgMJHIXC7VZWEMAVhEvtcjLiOI0ZGqjAJweZ8E5qEQHSgWiUsFrFSidb5q/xFEqeHvI6pBXbWUzCYoN3u7OCE7cRckqH3sDT22KapxYrRU5bS/OJ6dr2S5TgyIhUSuFpWDxeqT1QUS/1MG4/yBSOoOqA9Bt0Odd1GKDLzrml6saAssohi94wcSWuOs+z9SIxZIADn5c1ZQOMm231hWizAZ0CTdXa0cndbOgjk2ab2AqUT1zEx4lBEWtEnnhv67ofsLS4iOSCeGarvzaOdn7OE6Sryz6CfR3a6LYXB2CYIIAImpGjeeG0OXyI9HBbeLCmFp2CLUQID2k6tkXs/Y6ZKuUrs3yG1+mv92Ph+QtHmIAHwPFhRI3lUj71YMuaL7xpoF6rKx4GsDzhCS1kzOhASiyFq8kp+KebcbZoccoHXA5du+9hjLLkSU0lCtipaX5PTfjTibLmzdHqdR25qfoncPmwIXJOYe8p/YKuLAL3codZTIyhN8SU4g4ppc2iEUBoBooerivEsBVuDFaAeTS6xEomYjfupRra1Kd9pidq52tk/MFMkkzCJkCMs1dtywqN+bBSMMHj+Q31x8WacSbS1BfqvMk8jHmGKEN1fNLAHYroiSq+SWZynLeAG/YKgZghKzGQmzLav4gOb4OMreK+6r75oTDZoIHTSM1543SAWpHhtQGKfRN1AJypeLIUo+ohdUW454Co3KDbRuk2BtrZmv3aYpZq1b93ZpuJhp+XceweoY0TcBiCN+Be/D80Z/HcCb8UQ0S2sw/8HbSR2n5vHHBtRvePqbBfpVOmOg/iUTSFdzUc8MepXbi7wnZLWpEG8bZZ2fhR085QMB5fu6HkL7xWntikqHMUfH8eM+/nhEv59mU7asXE6caUGOBPWPG6x+YmWROXvw/Cm3qD12b5VpxhodUJW3jagiGuegMM5oXsR1dp/1aOwRuCUQ67bfKl1L8f5hQaTa4OqfA9h2A4Q9s9lYGTq/XPPJI4FRIsce9arFSn4NTAS2NL+wE5rBNm/r9e3Y8W6L6KP+ttlDZE59xcp7HITwWYo1ZokF9c6ZZZijwpqL2jtrBuPdM+q8UcRuCZlLaETSzVHkT1UfKgUQ/XR8+hfymi+Ca1MzblyUw3kXF/hNYEyMe/HRjlKJH8G4gECyZcuRIm+Hi85e1oy81to9178vCqgljLkOXNPaj/QfSfqTOJdCRQ1Sg4VWkGauhf/hBSKY8wGXlRhlyxrUszwjzstLGjeLqgZYj7RR9Yb+VCAIHYvk1U7tw2Ei2EOJUv89VX6wzRIpeDumZdT9cnYIBdDvmshsZzHSmq/4hbFDJdQIrXs02XpmiUDquAa98JHGHopUT803sjpeRz7ppDiDikLj67dNmurp2Kp+Y94icm2dyCsOP57Nh30KVJxt4LAeJsOnV7d+dABbPu3lhfts4tXfc5k3KV4n25LAI3Fx+4nPf9dzyqZLl83TfbpL+uNrpLNh3/TQb9wPIptSCo/N/CtL808ZUB9Wq1IGmbGrXWZfLPAEDsS6hFiI0DpPE8AahQFbyKLnhWzkZqJdAj5vinDIcWlhzTmp9KC1QSu3rXBtpU+sAo7y3o4ilzEboPXs4lCr1BdP/2YlWAYYRFy7JfPhCtH+9nCUPQ/iGKtuXBXzuX+YralkZCIjjLwVDkxL5wMrUCALbrvMst0IB0Ghh/qcFVh1lB3sUw2Hfox+hMG81+wFbmr0XDimH9FYy9Gkx4uKDiAN/Kgu1QJtF8odlmSAjLiNzu4mkB1XHwc04dJyGH2HWMEvPJCyG+CE4aKTL8/i6B6pn0/KcjFtQJEdCdIubi96pdIyHTIXYO1yXdCrfs5mVNTJVbnMuwmAclm2GR0Uk+Mbp6YrdajOd/ZnwkulFSmmMIR43yYI96m5ahdhjhyCBorlWyXawCO8fnyIWNSNaIMcvQlCbx4L+1TXZ1Ej+ZmmPrsYUrdF00+qz4xKI4IV8zx7vmQFDAtky9elE7zlFxyEFmHIwXCE5/yfbdPNeBR/IBCcWasdMHMwQ5l1HIl8vj8MaLzf4nz/OcUVQDR9B88EFR+WZEtiOzQ5OSK6azfu+eDMuiHjviBTonsvm06DIHbZ5ydou5D0moSGZt+Q5X6Dvbz5V7a3WaKPFGcV1JDB4XlF7+LdQ1qYun4jeDQWgI8KMRC51Nl97QLMx1aLGbEiw9L4jDnnpy1Ar6A7MWllv98rrCcj4E/XlC/jV6MxxGyobzcWdd4ACQKeX1qEIreISNsWwaOBHVuULAORqfV2wJdCYivL3gpr18xoBzF5lc8cfUZSE5UAsRrHCZhuWTSj3RdCf2IXOz7kB+5lGohHpJNaNU1PzZxbXTEuq/9q6H6YAMwzDiJ0serFppffu1Yl+vgUnfPKkoJUAmDStFTbUXWwpmbLyMN1LQWXgjUCQLGz7trUWCSU00YTey4vYtS6T1Qfel1ZtTTwytLTjjMu/HZT/nLdJdSWOUtQALcLlLQ4p0oyrCNxkpYuayO0kcdKFPReL5BXfyl+/YSuCgERtvBvNx2fd2j8TaMzSw4hmmvr9c6IXggU2nlZQgCxhZW3Svj+rq6Iviwg/b3Jk0eKAMpNE8uxk9pv4gCbXEdBu1fUQNNjFZA8A7McNsd3WY28dHINxP8y137+dl30Nv1PvcVyDhvgsRcGJuwsnLu9X5/t+biUPEWSB3Ic+rGRXiTdYUG5KUVnGopvdSoo/QixhzSDZP4ccS4pHx2fjPdwM6MgheGULvuES67sdER+fvDqSg0aoyoyTMheSWIYdZvoIwRvoPyazNz03iOC9LEMd80PsuKYq6Amn2jFO33qBqo47bv7ZrT9U4Cvtigf8cSMWIoa4yDmMrnMgJFKBcJcSPxZgjnHIphb9iAC2Z0VrySphsVldE9VLSOGZNk9kYKgijr1fcefZC7y6JgrrPxQb51Lb8RHA5A3gV09YYFheJO9/iggGnHU5erzheYWfKl5AGFv2Q59nDnRa0BJI6ii4UhOY25n22+tJxsOF7Fr6YOAu1JwoAVQXNA34fKOub5F1AvRDS1/3tyKYi262fiohmZVQqrDz8y6LrnrRUnynZ0z5Qjz80B5vV3vY64MrkgUT8UfziLqqoaT+qZa8ybgCwhWRkIkG20UrIU8y2Woj0K25wvKx1Lh4YH2v7FGFgPPqLGM0/t1dVwKHu/lcCgzFu9UtnHvLn8hy8V4lz1QjWYpBHoHa3Tr/+9M/IoXNLJn+ufJMYk5VvJDqtSkqIFBKgKS1MQFX2QLMQzPlLrn7/hX9gJD4fZz2eZtCNjbY+nY5CUWOLO3xauw/sglCNJ4SI8AIl72+G/AfXT8aUFRRKIvwrjExVjedpZxn+jlap46bXJ76koeGzvsq4OA3wvXjSehOcY4yDs4bTi/ph0mrbitbGzGeh+jgP1Q5eE3fQHuVKrOcAYjFmm33coIsFdHIxgj1CnApb8K4zgso7NFU3smkdHy1Ggnd4Vg5PfnauZpmscVmRv55MEORm9EEffTdaysG62w0efT90K4ZmttKno3BhSrhTDIou7pIiJr0G7a2lZj6MU+agMY3Q0QgR+F88u+wUcQivBgFfbBYACH01gK8rlM2FhLmwpiT3B9fAgDb1EXzofWMu4sGTJ5ytLlVc2kPK5Ifgwb+BFldSgRRPpQTvSD/obbg9zk/LniwxuS2g27MPhgD","page_age":"2 days ago"},{"type":"web_search_result","title":"June 2025 stock market outlook | Fidelity","url":"https://www.fidelity.com/learning-center/trading-investing/stock-market-outlook","encrypted_content":"EtgeCioIBBgCIiRiY2JjZWJjMy1lYTFhLTRmNjktYTUwMy01YzgwNWU1Y2U0NzESDK8VKB/m8mhF6Fm1PhoMRaDQ/VAXJa/msf6KIjBvNg1V5km16TbXoeUuZLhKyWY2yKfAWvgZ57kotAOKbhV2HENP2mKFwz7TD4pzifMq2x08oG4gx3+g6WPLr5lbS6NlHMQmJNlkZRiD2grSFup2cbfZFrl0XBj4Q1ob8MfZGCb+XqTXOa5s3VlD26BHhy+x7goX5xJg/XHUz3NA/82sJYbStorclU9ZYrGJVoCIvrd0/ZPSm51oq+FUX4omyA1X3rpfoAErAPesN/ncjxwalf6TmVCGJt9m8ImHpUe4EXLdymeGJQ7iAxGxEb3iu+hIRGg+yUjxqJ+h09kHeEz0yfx53LEPobwq2WPu0uck8H6MTyAepaDN74grhhMt/Z8cZQwgCT0gil4t/lH/yvW2/88Jys2vfyttGkvrK9mlAIBfO5W2EReGHGtKFQytONVF38yBnQHIfq48IWfiBFGEQNifmElH0g8v94RMsdy5yM0sD8gQIiWrNR4MJDfBZpKs5DdsTF8Cc3k+bMWLQixe3JBcFfGW2D8y55JGJy9bQdbw3XtsSqCa5Mdvi0fkOMxVUwavdinErh9vUdb7aXKEy0CmIs8UB/Cb633pShcBkZh69p+fmn7MArGEXpKII700UxcXRPUhJrCT5LzC9GjibfZ9gi40qXxrRvgYN4+f9umPoKrYEZVCjAwgcj+m1QKzWqp0K805DX5l4522k09we3F6n1inKS2+/ti1bITRx1YbdFPrDF1Z7QJxFt3kI/HoSuuQqSWU5Nhep3lF7dEBByKOUhR082G16KSTDe0Ow5rdT4mnqlfX6IXIeV8ds2oCQZsgBEK9vj9oT8v03yKGLqq9UDUX1qqqA3cWsDZ5fkrrNaleroJ5jssfHpo08ajQOSJHSpuOci7pUdKaQfEf8dBag/Sp4gZdf+4NmBcaiR+fPnT6T9sSuM0Tt2+JzwlAxCGrADqnu3EmNyNBjw3lsC0U7SN8FPIc0oaVEAXAmYH/zcYCF3lXNASP3RSFgl/cqSXy2w2fvxTaCwpK35sIK5I2176NUF9kyqgcK0Axq1FnA9+rdTjmhR2aUnlTY68YII9EULZqx6PySqWj5wkehbxK486XYwdW9Sw/HpMqJQyms//q0k+AfemGZrOV9Ub75XsPWheotGg817xXKNZhlBADxd7jrAfxC1IIKcl70rdXLx+jOgV5zS/xdbxSC7L092cxV4+JXYXWhgXt4t5XOUJAgg9b0lcWf/OoOqLjOYCtQBDQNAWOnNR40LjjrGJ+JKCLr1ffp5oD3Wy9WhL6WhPA0Cf8dy0BeLOd2cCzHZszwJ1mRnKOmSF4SOpRtqiSWgb098Gcgi/EWh5XGAiQ8/4vpb0ZHcMe6K/UDPQOvFa8rh7mqjTrt2SoQcRSkE/KLr43rSKf6zx4yJutrnNVfsscyYVnJC7x+73FZA8yauV+eYS7PA9sADsRHG7vztinKk5EFey+Gu4F5wnTFv8RqhKXeouDIARb6k350VOTqNYY9GXGmdCD5XtCO/8f5FANUrGUuv/+3pXUer2olZWg36oJbpuUiKY5DwaADQMAUWNpXPKxySDKjJAWVL2/QHpM9wXH+pf7e3WgvUhpj0yP/hOAvq75scMFoW0++MG+0c11ZE+i6cTN7uMhtfdEIBtN5zalkr93eCfex1KKo1aSVa85xbTX8rTO2qmrtXNNrXAmTojTrH5mJhQt3x6BCcjvPi/RZuFN52Mk2w14NUTCfOVXLrFqT1MzZgoGp6srYP4AAYaH24mthFC7IOa8ziv7i+CQqNg3xMs6QhriBaFWEKqWG1p0TbhQXZIfCOLBJvdDvYXh1BFyNAS1r45fUzWadaI8sAq+bRmO+Z5PEGzfr3iqUn55k6XBBYqU24ZmgHpRh5yTxUkij6fAMt2bqbP1i9yti3ZwqQLr6hvPtOKgxaEOf50MyD6llJLByvIno49lu61xrvKc7EEbPAKSV7C8ZAm3LN67xLhTZjmZMn12wnqV2kgJtHNeSUKkvih7Mjwm2MVaDmtwRXzGYgk9sX2kwq4EsZuzupXJjOmk1xbUzLYOBIm9+x6On/6F239/irysHcewe388Y1ILBfTSqj1j825GtdtQ3MvI6K2/wyAi9viuU8YcZtxhO6OR2LdIMQfcd2u20ZYfhP9QJe71RiVckUlbPBiR89BCwxhK6DaJlYS+kHlkY21SVDAk3oy99ISM3bFwSxun0AqyFEtO3g/cjxnl/AOLbYI3R4nLSsyqYFNbGxIMmJdOin9MtCDZ8QlPvod+shDMK49k4B7Tw3SDB/dh5grj0FF74wV9AKxG4XBx7J1wFvpZPAFfAXQwt4a+KimY8s8FF85zPVXOoH4lnhpkgkT219Ps1oLvrzz1FIuCxTYj/+8aO5d/KU+NIkycXdb411XDcX+BZjqkJsPbqStieLA9PHlB/6Y2fs56RW3FcPywwH8S1YIa9p25k3d7h/n5Bz+f8EZnsJjY5S8HjwXpTRoZdGyAXJYKkIE8aciXvRgLgE9YSC8U+HwZLQeeqlMQHedUl4bkjoke+TEduQCPDdO0EX0dW0oy2U81i0bY0GgrzXwiWAvCvUri67B4bzbOq6fgMHi3zlBMGM6xntmvopaCpfe3BYp1KQG/gUBBpkDXVgMsjkfJRU5lC4bZI5cwJYejQzSZB3kP1KfglJF8uBIooKPMWRg/Vs/JD/Xo+E34okOaPxq4OLit5LCrlyrwkkXRDddWMDIS1iLaiUUxguu6bxlVIOiqaWUFLEb4g0pzY92OGcVcUTuaxNzRPGNni2+NDSOGsGJDKGlPQTRIcECCidXgjsXTyrZOu4WrZBDgq3+zVU24gYd1J56e+/Ywgws7LXjlI/iEEzgI3HSPIYvnsFxySflVeHx5gHonuyBiywR7zHxCKd+/gnYGYm1NteAC8kLqxq4LFZw0spR2y4I6S8OekLWndNq8YEHTlwr+IOWIo6eXugCvf8guiNdEQIXUPj1z/Ydqrib+2FtNnY/Ejic3yuuhgNpUMDqYTcn4wCHIa5ukoS5PONJlH1KMnsLCouT/WU1Xj9nsVBWv1wBj7Tkitjsk/o3UrcS4JzuDABBM23uQdz0y2CrLBkPDBeKClQ7NJSAWZ2UlNRQGjaJ8qsMCBSGuXaMx/L5L1GKqM74t2S23C3yS6PmSPlsemwpVO4YQg5p/JmHAOqBj9icmiSkyStOVptL+DH51/qqOYxnYnuhKhjt/dxNTfiWo3pcf1dKioUFUsH9X50zX0KmfpTJhkScAhJf8zRiZXt/4dCt8B3RTBb07CBRNS97sf8s/1PJKSlJvX8CUuxdp/JQKjWD6Zfbz2vAcYz7onCQ3x34w+sZLiNOrQShFKSvz4+mx6pVzguASCLXsFPcThnl0k0RHCiFUt0RGcPNJrPvLr+thdMhEW26mCiE6ZcOx+vSO+5v8/YdI2kRH6fTpTSfuf6ryeGrWGwyjJS/77Km8aCCQvl7m9IhBPV/+PF16BkxgFoqKTNCatId/pXif4J6ayhKxr6s/hn0nLvXpglzvTP9uoQXMyVfebzgbzC4Qoy6oIBcSemjURxEBtrwMUn21qeMLlkZ0KP75zWPqMQaMBVjz6oBujm/eoX0TCKp4bo1YuYleqUrjic+J914N3zy7tK8XNoh7UyT+qSbY3lvChV5gNYQeqdRvFMGLL8RKsyzONf/Kn+pw2lbOr23Yy6KY0p74CKw456jBLbav1RX6p20EdWSrkI/3LTZ6hvRVTD/cPnwUQ8V3p3Rywb3NaMb6z/R7Xx3yXX0HKg/nOQD6YgQVuLzXmDK92c923C3/vM2TaxxqTCisacR/el/V8tbSreAgdW10dAG9I+HKUAwOwMeW6pWi+eE7nuycmBjay3C8mhQAzyBs5VjoVINdnAvZ+sCasPaMpR+HpMuJTj/hUt7CADuCbg1/D4wxJOP3i1Z5+es0aPg0ZzJkLmMWLm3gtKh4W+Cuu85G/Wr6YbEdDVn2Iw866JkoQ3mUPqv2kcHOwC42+aFcIhAA/GA7hYTYsjRsB5Dst3LP3r42oGjYtPGkexn1c4GZx7C1rcvcJxijj5uQ2SlB8lrP35HBxN+fvfnMtLgWQFOK18qS6cj0cTdMdRTx+jlMbxZMniRJX3GS9R2C4PWjFgIuVKZ3RR3JeXddhWNR+XDB/cR459olzMnyYQQUZbz20qSqTBAoCM+wNXn26J1MzGYLsVo/9rScrv8y27qfCWhXcrc81YgLF+L3uP0qshA0PhkheJ5HLrRKfuXcAvnx8nHvza1/iAe69MlwgNBeJJktOL17XRt73W42I3KGc64TgoPa0ztgSAPoBHEvafOLLoDFfh2bFAubR1O6Uc8rCPXOfUzvLjcYOrgqbzkmmMKvaKDYlUyJAeTm1DuCvYKdP2heZH7mkfeS58yH4GVx0JZSriF1VMWPp6fy5Zw1d5aKeFCbBsixLVN0RqDKM4CqBO0qzV/zaoCPLxS0H5jLRyI20ko1kOK3sRffhUbiTcJcXwi4QZBNZZvo4+iRT+N//mMQifX2c5gPK9gyMqfQUsGkt0WhK28z2B6+58cn1KI+jsdYAAWq22Rg5i40L/U96HHpcrFWkS3Ro/bBNFcGjKgMK1ROwRAxm8O3X558jGcU9R7xkYIqzNKcIF2qlvna/rYOgD1nIM2A79JKqFPyOO2NjfTKEDc7BdCl9Pp4qd4A9tgpSjvQgGt0aRP+3caKR3+7Yer4A0CBZjnPUlsAI8IIGFiahXWrRGRFl5Cbjc20mMgqAchzDZNxmqwKNyhUEG4u7mFn26xGLrIapoyjEPXvbu61kh1KPbqXCfL7gvO51oDL9MAazIKXB6XK/KoQBZv0O+FbNl0eNaqigDfujrWOITk6vtDP3bwmA0jH4e8W3TkudWBI0L9bLVvlN56MkNdvtWu0yz4Ac84xHw9oE1AuAmUFQzruGVMAd8t6Db39uuqDrrOF2bxsUAlgwJeK73TuDybMpxQtAW2ovxZtMR34TXC4kt9jy89hYawutZ2iKZFdby0R7PSj8ViG6wzsCpPzbfWnBSHNfYcnUfuE7POmCedwmMuerPRd16BwCBT/0Dk85pr7Gy+jS3BksMwbf4wz6cYqHPue4GfTZE8O4jtEUIsKaC8XgshrMUIu+ilx3unebt14tPwH74mLBRgD","page_age":"2 weeks ago"},{"type":"web_search_result","title":"Markets News, June 10, 2025: Stocks Rise for 3rd Straight Day as Investors Await News on US-China Trade Talks; Tesla, Intel Surge to Pace S&P 500","url":"https://www.investopedia.com/dow-jones-today-06102025-11751328","encrypted_content":"EsAcCioIBBgCIiRiY2JjZWJjMy1lYTFhLTRmNjktYTUwMy01YzgwNWU1Y2U0NzESDCR9i6GxjLvOcJ2pnRoMftXbpCHQy/koi+aOIjDq/S9IKxx7FpouPyWUYoozUgWdazpYGq4dRLulPgbpx2uBPUO6Nu6eGpCFsadt0dQqwxvdseE2/58UBfGr/uJ5uuWgsF4Obu/En0YDQW1ogqStppu2Y6rzjwf1E//aM8NGwccegy9ABHN9xOUA1c88ijtkSvLGQvUZAgLbYVmuaKCRlVh7YoK9FuYckyuSz7PqtQ5MWvxvhtAf3rVZgsZKqK7y4ToVngwGNmqB6bRzb7Vsac/Uab9Ir7YuyMlt37tTd6Uj4QnQu9AISbAt/IT7kancwfC8JCVhg7yr5zftR4IJawJtAVup9K8vUfuJlR9kvfLOGNxRXgc0fM/UJlzDUouTFBYIEpDDr//yGHJ953xlltUQn7bfJ36v/yOgAI9BX8+BVOhcjttViwQU8z46U04Uor0uIXpaOzqE9c3pPhcZb2lfj4jxJ+9rOEfN6YtLQlw21fe87Y6YKxTr3lIb6c1cPjsKLkdGXOrMFv4+KDAuIeu8cRfIN4K4gBEJspOECUs4lRImXEW2R3nNFO8t57ndWIEbf3eZm4H8q29FDIcOl05j69Kn+H53PZDsTVK3+O7BZXB3sujs1xinzejDWj18ZFEg54yoY4ocsMjETV3G/NC4dE923yjuwV6SICFHkhnfpBkcG+GAN5j4YmCe6IgUjmVR14T5DJ5Ckr5ZPdfOOPlf1290c2fQYqv4Rr6sSOrvhbxQq07EatjNDrbFD+jamCMQzaBTIDgTDZOP97m/RSXjIuvrcpqd6Pqchb/Eaw197WoziwLcyqln8H8zPgXY9j3a9srSWXQMc0Jgd2s06TVfqlbRcfG5ldHI30oiGwK+oafAT9t5Jg3vWMtVc1qq59Ka3BeUne5QXXeklXJUnYUVUEdJlybxjIsM76NuficfmnMy6xHTKYHbBZAbC5Knm5rvKNj7IbSAKiZKX3Rq+/W8lHS7F5O07fE+CGfOiuZkrkCgbQcvlN1YXCXNpKli1REpo/inpeWK1Eanv3KUZJ5nTQ0Bb+93T/QSDXdl2qo6KXWVX+qfTWZUs/uvHZeII1JnIzLO+fUL+UCG3RbXRb62C2gHeZGgGYe/EhTZ5kgu93m1hvwrbkis3poR50gawg5sd5t9tkpvj+dnLTQVh7w08K16ml06UiHQhsGtVDVDcqiFC7ZvH4BoAZyIUx5BOm/lndCljsp8k/zCkctqMv7ccWO5Kq5jGMB0fgnEyt7ZZxY0OIPv1Sgz/qI8lLv3RMinPz8MHHp2/L87WJyCg54Ey/PmoRNuu3qzK0hgOJwHUWzzbr/6jJs/9bCIoKez3htdCEy7Uh8JRKLLg/dGOsLvdH27DCgJGp2Zi5LCB+fQHUX6FmDQTH1DE4jim7fOQC8TQ0cb4+9JOU210u5qWjdUD1SLeyP4SRmnhM0HyHX9ApInFFCZqtsz1zZEfthGZkdtkX/QqS2pXinAuccJuo8V9bQ/BL+jTHBa1yOvRjb7/XX6Z9NdIYEwqLG5z4mMWeIXUFQNtaT5mqWT7Q5uwYq0ghY9hWglDXTSdvRlQjHGXev86XPIwSZ0Jy4SmCvsiYHUAdyDiWoesEyKaGLVwv/lLkszwCQHPaPThSieVRDJCVOKjmbJ8rPc6FSjqZ1EePiI/0kiPl+MLgIERmYk0rLwjZ5PNGk+oijyvnw1wv+S1die4EfBy49a7AaTtcm454fqJYtmDyq3GA9KdKt/MfwI4HKXzV/p1CFyVpiX4BXDhPU7nbyh3q7x+qwH3nQMS+NvSCcN1hnRwrdS1qIXS0DJpz1qymG8jKQXn6WWLQwKqSkDSUUJeXjgmw537lPJVFQAq9YT6CRBrzgM9pwKIHVV2KCtYLrBNNlDAdF/poj7DYHeXW/0/PrzmuPoLNWUDP68wL+tLm5aqFQbWv/os9CA4u1ZpQREqeRw3qDu0MvomO9+l33pTfA550YUoIyXaiwM7AZtiAz3hYe7rG4Axh2O7wrjr5DSfsD9UUtV4y9nw+HaLnBSGhvAtFGakEBlAKfetS4AIdv0SbpOmFdiR3UiU17TJvi+fkx3dPC7WvAYN6pGY3ut19QMB1warE2/SIVPjxk5OGqSCVFNXunRib6rnYBiD0KYpDqYrFzF3BCn2LgBcxUOtxvvZDQ6NAKHoKvr5guLkEkRSowmthmPZwlbdXxpmHw21PYtt3TCriJPG4qCS5vU1vN+fLEqVuFYDwEibvBd9W+BGei9lDkvGknm97BD+HwtZV6FBrn2gmx8yT8Psvx9LWbpVb4oQjmwRey3kKhSqR2tgY6XHJGzD8YZUW98lqKOXRTYOZlQZ+wMG2yZKPLFArNgS+VL6C03Y8C1v7paJQoVygoo3/KKUSutEo9iyXosRgnUW5l0yduU5RrMMsuRoGIIFJC4dfgYvnESYNDPnjBWx2+Pj7Jr7kcQPCAQz3RIjOGVtwDGDL2W8NagVOhbAz/x/ASa8eSx3P3qW5BjgB2u4u8fJG5osd/Wvax35eRKNQzUkOb++LeObJHJxnP+Ww0P4tvhuEgSt4b1Kj9B1KFL8tClnVp1z2gtS03gU4VxvT6XYmmoNb0M0G1OM3nrMH9esQJOQyVqc72/4c66rL0AJCQv1q8zDXTAmGkNyzgZhLqdL6Kw4kmWpKi0aYV93hoNlyMMemES+OxthRotqJu5JWtt6N6zo9FkB0XEvOjUwkRTVhHKniTrtgX2V2WJI2LtB0k26AmsB0YfefY+zwkYoNp4Wkd2bOURMIkMzrEaEBVZ7aRtlnCB+CJuScsHko1KBHQoXvz8TYFpFbYksSdxqPpDpIiKl5STMILuU3FtsPkYmKmeugvlrAVtDRcJ5O6qkqntARVb5wUATx84xCdMi7uXEqDtIn049RGj9ae23OASMi38TMihpibCLwG/R8T3F1PlDh6CNsi01LQC8yEKKw0w3W1u/d03ukRUeogZjfy27AZO3K1hZhH73d/FMnBNwU1c9pzwSIW964BYGxwXGuNBSQBPQ8TzPBiFZDDh/7c6TF/MkEN5aVtyl2h/pdZBMjzNHKpucSTCOYET4OLLCkHrwWHfhMBN52Xr/MRUUTqeqw7klK7dHDpYxrGgsWCpOOMQgZLELs6fbq0WzDovRQfbXZhRhC2tGiE3TmWb8xF8COWKI8Qrcir+HJ6KJm9sbzO09cLwv8bAMqTMvHjLfsjt3Xsj6PCOqDO8uoTG/Ltkb5Ab3TsBLIaD92ZuHcjWKEQZcWOHaQTp2EWz3gzkzSS+qMtadjPLpiEl2WirohHhr+qZHtSnOlh/raYQwHysrv5VG3vIjBLrbGQ4MwVPhPCIqjJYxJ19cjOVCbs9OwKUtGPbSuE3BloN7pLEcvF7L2NGWCWTbk0VXA05j4KvbYVcnqTtI7i5ZMmga8boQbBiu2apeWiZxvuDi7VrA3mebEPkk067sSTWHfrc+Q6ndOpFhpaIg0sLWgFSTZKDELjVbLpP86fXCLpYgGTEXyxCZMHEczOQ3NpgwDsI4enqa4mPyjk5nib5HEnmv5EFqLjPzke24R2WYHkj7gGaPZZqBHpFZ+DSzpVWUz55/HvtDnVP2X8Wvsd0Sa1dU32Fh7AuTgj9m5xhbF5ElS39CyhW+jEWWzgNSWS+3FXn6/InRPxkPJN0+PgbWgWDuUnvW9Z6bXGWmesq+O7cGvx1rFQd98C0CKaakGlauICkeYmF+CJmpvUnRMZ57mjfFMIYu32lDeS4y/wF0KkCx8hys/qFsMn/wowJ9XstiiB7tOAMIRB81KDFN12HjMd9r6PziaI7jdCCPFynp1S69ykGRfuf8uc6ricwyO1ibcRFJt6bCLexXjZwWAlw1r3hwsP+CitLsH7DmARoL316k18w8KgA4tGImXfiF7yAo84u6OHRwxnWIrNEPuORAjrBALE0qxR3+YYRsyLli17Qu3u7D94opa4GJ33dihxGNrGGL0MuoEkiwF7mCQ6yZwdR0L4wlrGmfr2avGDgyyPONh6OBwfamLsmTjlryp6t0O/L5jL02uXAkVhTsqFiBpUyJ0pM4REy09mFUIKMbiwnqCLQ77xdjS9B/0lvrJ4JTW3pe3ujO/GGewHawPehpp767PWrP9AYLORLMiZ2tfrvXufu7/81gPvb2PYwdGMAADB0Dmzv/ifxXaBHGLL4273dNjHHd7fgqZ4HPqBfWbP8tgZg+9Ni09nAUCwaK1yCFxbRYUdw7VmqW1bcGGVL58zzfVNLQzK4J36ptQ+vTF9DdBtmoCdX01B6HjWB9m/d2QParBCcD/fbSa/6cKhtkKK9lAd/kFNP1QxobNszC8snElCEKCkq+sBH4VY7C377sF2Qh02YG2Zq9U7qlYRYnntJah4wO+pOjVzJsbMJJwtd1UiRQORPTXMun7H5F+db1vEfA9AE5+RINC+Rheg9IyAfvbW9dEW9quSthvAVYsmCEqqH/F/8MBAG65PXqzEmRNdwlAzaq2TA7zH2Uptdivy6ISBWH3c7MYOoKH9+mibovokK5H6sQEBsVCUHhWBq0lgTetYp1k3FywpHo3XlVs2ws1dQ1zdnhj/fB4c+Fm95LB4wduDeGPwkiBzInZXnn4Dtzx8mQIpizM4wod0LAnuvj2Im+Vq2jLya/sW1DGcwQGtkR1INjHVDNmygonegS0zcuCqi5amnNwxeaacUCso7IZoFCVILHDyp5H5+JuQr2AR8A4SKStxSzoWkWaG8tGIWuwGuQg1VC4juGISahbP5nNptYYN9csZsuIhSiJdwD4MFlDUuxF4b0PQ/C9IiGAM=","page_age":null},{"type":"web_search_result","title":"Markets News, June 3, 2025: Major Indexes Rise as Nvidia Leads Big Gains for Chip Stocks; Nasdaq Back in Positive Territory for 2025","url":"https://www.investopedia.com/dow-jones-today-06032025-11747070","encrypted_content":"EqkgCioIBBgCIiRiY2JjZWJjMy1lYTFhLTRmNjktYTUwMy01YzgwNWU1Y2U0NzESDJMkMzIK1CRSc0nSxBoMciceUmTnXiSKKhJoIjBxS8+5E3/OkAwovN7lGIkyzluy4UKmentDibRZ0XvgaMheEKLoUCU6ZsazGFxpxysqrB/zzQwibRMrRdXSm0nILBS9Qk4VjYtMdhzqedhXBpeHnamoFQ45ps+Gzquz/dtAyJuV+LmL4IozrZf9OIkfd+rmDZzWAwy1Ywk9Cn+zpnl0UFpaF+F4b1srpcKVUn8uiYx7mG0jeq9vD4aVTfhvCU8IJxipngkAWSlF6bB5S8mt9Ma/P8V5klbfrmzUYT9J+KsUeQjhR5DS1KfrSDs4zCxnrV8gqJ1mKtQttzk4fx3YFVWSoSb3Y98m7ZDuRsAYzEBhDL+WGkMOGWLN1HplZWKwvzRlLLQiQ7/DV9Vt/pW/bQwA9D1y9jex1ShrxEoTBk96yyGu7YAOxPEvNapVmynRXNcAGm1MYTIoVs9BxforCHGlG1liQNZb8Z/+hMWKkGxkiNEuoKjXGo1P3zCJec6O6BksFDKhoK7V827IwUQTOoDCDGqjr/bPXvRAcgvmlo9BK0kkCgUtNXUoVfDjx9GJ7DIIugVpsg9SrmBElGx0Wy57Zv0uVroe+azgvhU4K9CzIkeC2Vlt2KgI5aE/BbuY3zbfS/6ZOX7Dg5tiuImjhumVRTaTsPnwdYC2jhoLpTUfKWU6bzXf6aUtTZ5DgyaBYSV5qhN9qiHhmBnFD8jePMLFpDuMZtOejk9sBlVgelhOkLsvT+ZQ1EOpTkrkddg0VX1XDCd2TaguZaPr+6UlXa46PCd9Gdvi7Xz28aXZE1Gz0hKTPIXImX3yd3oOaJtdS0/eBykIKjZU6PdtJyzSNMH2D7O/m1KxSW+rYI/49avUkcYXGQXjG6QL50908LMs0O6LDvUE4/A6xLsnFHSFU82IjF2xqVWCasV/c7pMPQ8Psrln38GhDgW+voI47IMc7hH8qO0myx/QRHmLbOqCtvVfJ7PNtpPxqKEOukO554uiOQYAAQO7JvLF1O74oRbOFE7dbe57TN1PSfEgSjGLCP37n/fBZTXhZe32mabGSfvJ/+0miTfkW66mhkQr1bwgSKgAQWvDH+nb8zW2Kp3Wx0Cm8jHfd3p/K9m9C9Arc9haFYqkwgymHlgXwLurt/vZ62JAViPAX6MH/2NNeWoSMUGZ0F/lsbUnakS9yZjGcA6zXcf9m67xFgCPdqHngtXFdjEhejsGRPCgYcxayCsOqUVl7oG+ky6TF7rH26G1L+F0C4+rVfAyzRIjDGBSI4iPysiWNWRUM62wDYGYOsvYAM5k4xxd9u05nlQCVKt00J2Tm0ryW4NP4g/4IrxfxD+RRnkeC2V5NY46iWRBssAdLrDi+rj5pCOFhRkqq+8480r9aTN8OG6gb7GGNR+TULCkQJ3UEDAqQPE12MrQYb2cU5X3AWWDaMa5x1Vkzz8KQ+1CE3PbaMo7WUF5U6IX+XGTKZFS21mvX33XS3rHU9bg/OtW6tYJKvL/U3gb7Q2KN+kkmy/Ye7BCxrLa+UmyhZvAXku3Esr4vky09dgBucZeWZAmiyWhVPK5XvWrMul/WF+ewK2pSzHnqDFwFA7/0SPTMpFxfUEcomfVl41G5xE3LBYh6uJNfn2qGmHPkEFxZIpfkqCWDxR42TeMtt7/r2Al1QukyVJ+PVnaE+Qs7zlx6mia/XlnBntWs7UKflqtPLUeei4OFFXr9WCVTNh1EkJIDLn9pjqX6F3QIUnxMJJe3NnwnZhQvp0zuMqbw6rwMBJBiK5xy0A0RvRJ5PKEyXrM3RW/6SkysDmFwz9f7d4CIXfOh3gCNFrFom821a+7tameZJ5H7wOcZGpQlKh0zRWieQJAVz7swqE6rtEXHNnqgnMgOcwbrK1oLHKukocRkepI1kSnhg2EB8TnsU7jCWtxcM0GYobB/9MliW1HNuQDwcYnLi5Ie7FIps8z5lUzSRkoUJuk4R81rbqY0v9A++ZYbsOfV5PjTFm/MZUzOrQ13Mtc9arbRJHKxkisxHMIgU2nA99bkgv0y+953LqpqKcIrXFmkzLm3mKLQxLkVBPWRe/fsai/JP8QDu7CElAHkVhHcORBpByegRGFaA6ulFmCCKGsvXGu0oMTiW0cCQjOS1vH8Rohqv2NCq4jmoJa6Fv4rPA/CU/YovJ9dGtoULYAiFRw3b8lEbBmb/XA1sw9RmYwCPtJhuwakjl2NMOWw5Czt1QRYvnfv5JEffbVSoYmil845WJikGwM52JP8dTKtE2/eC7g17r6Lk7ffRR8xLA/38CPoAm/zMKnZD5vtvef4d6wNkBClwWh614xvmeukap3qrf3JgtxqfkMvgEZ/0J3UghFC9frOxgvzj9BMqaZh7RARRpWKCY2X/ypJV+gXyrYm+Lgne1cexO5hvqN31mX5dKVNf2wFabRCNAZa15C21lRfm+ch6ltBCslJE3gVix/rI/dWURXV8nwlCu+YQ3lMc4oimW5bulGKUWgpLFI7LHFQyccIBUlB9gg+S7FL6cAxkngM4nWf515/r994H+p33+jVhC7NmS5fn+XwA4KgIsZ2ZQFTtBUceQNoqk5LUHLXd2tUJGj9h2wo/GcC7j8bMPB8vutt2z6ynnxD1Cy59awPZCMk0zroll5EgbovwpRvR6C4row4LgXlE9MaySWgkp+y1hUSnROxW/oEXQnmCoXTJSZLl5EjuzPdJcpdCFV5sw0P/rRlHEPk49jwqMTH/6GTwO/EMTU37fvceI0Svf0f/+GWZN0DTswDIAahk7Q/GcIFJ8WLO+tXLvyAgyPfuQFc/INVBo23Y23FvfLHBYefOMDbrEcIRuIYFYbS5WmqZT8CFa8w6WObrFkKQY0i7HBkfFKvNo9XqQSkA7JuUSQuxBWOxZBU8nNb1F9aurScgBb3lJf/411xsOF8wIeWPIGoIkO3NDs495P6yxcPKoLlBpxIo43NTrlXDerohQlkdR16wi1SnE3UWz+vAHESklrOK6QzX/UfzFK7jb3hqRRRvE59BABIg1OTGiR1F0D+eB2nxrJNtnr3VX+DWRAa1tSQB36vHamtTDio1ELH6SbjP7T4ewy/EZDG1kZeUY+wdj+Gl5ZjneUJeYwlY6imNnann1cvhPIqxipAMchQ7hbeeTG74mU1F6HJX0OseGohUgqctVg/zu/WzP7/hkPbESXrtEYifQgnKAcOw5wYTSP/YVZDmk7sKLisANPLjnczwzOeLocyjO4/jLd+/kBuKXkZDvX0nIEuXJ0XmMPNITJxVg+1LNysCxbJLEPze6nGrrlEZHzJS6orlj9ieRaviMnuO93+0WnKsjpyTh50VDciTVXNqabbI+tUM+D0JK1xDpcUcKmvJrqi1koj+FBkrUkBZCxUpK7l+RwUudMUt/6o8aLXm9ESJ9gtZsWmIaERct2vpgK1BiZPUB1J36Dh3iufHmrDUIZ8y6RitrMgjwEftETK1skTnsHUfRxIdOLu0BTLt3NKFNWIpiALiUBjcTggkKiZgdq+fC7UJ3FYOW0Ie3TW2y39kYZ6LaxtUem2/MhMmaA0Lqj4hjS61zkpKGmo3SfA9Y/+XCp4ZzOt04Ld3KtHEir/EkhBXAvgklXlfFRqRf4DC2CAbp9dLBJklIy8DrG24UM82o6omQeTtnyN72t/qfbCFpB2hIkFCoPpLkH+9oydvVAnAhckL32EX/OrCHf09kyib7jDeol/DwFrzd+20Qz8Y3q6CpWbGdO2wCgBRt2YPoNoFgPJKU9qi58UrWG/+z0ZV3KXE8Ey+jFy/T3uF6QrjmwOa6bVq4W3c0YZmiJM3SfjwvDgzb9qTpVfeN9AKkrfQKH7nIBGawC9lflfSx313kYPiXJkEk/kFTh+23TTH/PujErLhEKQJmP6sA02O7ll2cG7stz3oGBs9SusqpkIl6uIYOc8nEDxCm0bBw3y1odR1TJFZEWvv2qQ+YUF0avVYoESxrSAi171o8WE2K5bEEIof3wDUDHXWhxJPBTKoCOF54WZHD+rLUFHrzsXOX6O5+6EIwkmZ8mdZoC2YJGp48bYEL12ODYsXnWZvbWsDSiEueylHdsvGmmpHsu/DkfTVQwlEWG0NESz9p4yfHfEU5rSneoxVIgMb9ka0SJxsAbZtBIDXC+noc1p3VkVRRg3W4ylNE1xg/rklBdzkuo/ugpRYesfhyxFPf/KnZw9ZxVx1fXq35EQlT0ql+c15Cfahov5uPmnQkp5XT2S1T/0Z1lDo8FCUyxrKrvA03/e0EtznPNUAoXPf3WMQ06RTPj/vP0So2/z7saiKrr658rswbcvBISoFRfOG9MNY1LIgk0loiivaI5kEbqrhtUhdCorI0A5Zx3rPxdVMcQlWEOamDgGAPvi3p+VvYzsHlZb6ddojZIFtoyHP/eeJ/3h4eVCwsCSn9rZuyjq7y7tXRjLRsobzo8iY/4gkBrVz9L6US2NJZJ/5sZJwq+Ze4VTHJeXmmmYmzYZsaJwqg8QnV5dD4poCsvUrHFQ7gWW2t4w0cR8RdLpm/WkaqSiDWTaR5SJfYY8jRTzHk1FWIChMCOCzgGuXYzSk8scp3mF8w7aSSOE5NqjfTfhURPH3rxMJ5h+Gwxbjg0nJkez6JWCradl6LaV9EPFtENcz0e1XTGb7ouzB8f7mijmS97Qp50Wpx0KRZWWYTcRuajd2vjZBv7Ad370g0AROYj8pXKukpEp0PSfjmC8zJFT/hh8kOAGFmZ3ef1C26OQQkQsvCqwb8qbpkK45VNy8zZpZO4KKVdtS27dkuhYved2USiFNN7gXgL1yDn6W7dGRixplUNOPEmL3lDi7AfBDG2EhaASUrTclf8r1MCFiudj4WaSpVazXXyiU2Gv3F1XakHkaYZXUS/RCMCfg/qdwTRCz1ZB6NuJCIHInGnvUj8lZGsvTGnXAhE5LXzn4+T/sykSeslBU8YJJjSyEsbV8/lfRp6h1dMyrB5lP08qXDhv9uOF3WXH1F4n348LbJikahG2dDa0OpDZ30ku6jyTRyIX/XKdeEFZzS2MMATM7sZIm+k7PEdNfDTVgx5iOmBQDzVBNaLt6pEyG0Pw7LFA+5nieR6knQhu/V/ciU6RcdoQVKqiIwcR8YJEMPLIwwCqbPAu5K6dlso8aw/ykIrK3f5/DdADeVy1Sk5dU1X0ArFSDxx3NZyxEdh5Dnx2MHFlpTwNWeP1kjxEXxqgD4UyA32mBGluAkTu5jTGzfexqUx6IQiCF8VrrQQI2VziGd3NG+m82+xtXXjPcU8N+lq0OokpgnxxC7tyNOOHdr4eUO+Pq9CJmfNNEztO1tUguiE8BqDtipBTtD5IiYDdJ11sVG23hBITdRVN9m5JVsDDFYeUMcUnoYFZWYGThWjCh+ZuSpBPVnJTpuuv0DeBkRdh1shJZIxLavZwysHA+wpWazfGTzu115eA02W8txlzSpRJqd/cuuPy+ce4iCGGAM=","page_age":null},{"type":"web_search_result","title":"US Markets, Company Earnings, Stock Market Trends, Market News | Morningstar","url":"https://www.morningstar.com/markets","encrypted_content":"ErcQCioIBBgCIiRiY2JjZWJjMy1lYTFhLTRmNjktYTUwMy01YzgwNWU1Y2U0NzESDPqKNocm3POcbHDdpRoMSFlIfaIXNXDrCB4OIjAKTSjM5MoWYldp3t90qO9yHavs/1krczcg+tfR6thD+O2uQigUl2G3fRRiMpWYkFcqug/7TB9xkC8nFQ4Iipb/sqUNCf7DLrU9KO9MYhTkfaPddI3Hdrp8Vk7ROfQUla20RLIdptunYkW08C1EC52hQbB+k/Gv3Z7dGMgqi6fF7v1iiWBc1BaQKktPn5DwM1jVUkYx1JOi+7OYd7UPylyMFQeO/F9wMxgpMgXYORmu5eXhLVzEcb4+G4lB31qAhzyYcg5fJLf99kc7fAHJknauBN7O3RDjaUMkx7pfE/Njg5SmocbsLLa1t7xGbHr/ms+CyDQ7eQso8On7ZLhA7aecRPCcFem1OhZHJHUN8kMsq2wHRzhCfGhQo1PJPnFExPC7XVEhD3cKYoo7D319XlvpWJc78swKTa09rCdgQTVhODw1dZs28twf0rbzo+ROkD0F/RP6RDHWvw4X/noYFKwA/CYcp0rPTlt7dgF/6QsAn+pJvdgzPwy+lr3gllbKdi7xE5c5s7cVB2G/cCMdW0ekHYndAE8xubjFTStUnYOo9xxU35wdKohl10ctpEbHmSdYB1qQFL9cdC+FmSdCwbHQGUKkF70VzHsKnW6/q6eBsTuqtCQDFnWNJGuTZ6Pnl+Rdc8yag5cgSJLWaUwikGyavHkletIcJJAvJcGglV5j76ZGaVQoIge7TCUkdfH88vCbo2gyZMJDrfQ2e1I4ZkqTk5ZKHghRp9cHiaIaL/XQYSCeV8LCpdylQBoBJL23FCA0ySDETO6Whm5oWKCiy7hRjFGlixwas1TVnGBxaG3vEy+dwDHooPEyD9fbsAC51XKFBiFmndqbzkfBtkU8X9o1MFOy4+AZ4QI3BOqwG9BYMp8uOc8fYCoodhM/DwwCk+vvwfN1m4mz/Ti4CZqIgh1/IDbccx016VhtTH8dw6UqYpOaRWx38MUKdhBi4U0RFXNzp0BAZC0iQr9udSy/ZezvBNPofU1FYaqFQVcFOQvwDYIlqdlF+KuEN6J+FCkCrS1JQCZ+hJeClDyfTY2IvR/Jq7feazqwuJsCURjB800AeCmfKHdY0/x/J+RAVN//8bIjRAdtnCZSpLS8aHqFtvhXweAAGAY13G5o62Tfdxl4QJAHYmhNlvgCg3o5b67foVLuluwic0BgQPcgTIn0RG+IivlSaTT52xAi1ok47eMiMnVHbmHWolXOSYWS8bqiyWJAzmTt4+WXXW7H4e60cpoHacAFHaxBsBvYiuqYVHE+0HLcObguOd7445bnMLvbWqkLql02uQUmqu6L7NNvTWolDZEIPNnk3ftLuPmGcah55uUf2j/AFO5vLZ+su2I7KWyqh69SsTXw3GoT0d8/nRRyMl/KeKOrt+iZ0IjLOgICn/NBb8fgouxXQZ+m2gU8iZazF7vixDwLhDIkQ93oGNLLWz3wmxGQWJle9cAFVVC+WRplKTw2eGU5NE3Wvh/pxpjRAqmzTxY1qG1fo1Fcc40KocSbV6oPQQBp5RWpJSRNRC2b6Qp3KgkABmtwR55t3s7G9pRtai8/97+sGLl3xLH7hi/5EsFyA3Tf0RjnY7aRy2vOm3Whn4JtptKiv+z6I3kyfXGzgrmxOufJ24IJkwBBgabHyxsHIPQqFe9Z49/6Y4uA4dkZYpG6sBdVlUNIim5lkS0oh4DrAjDyPCIp8P1X3wWFEJMee9G6jo4/HriXbUWTA+eLZeoBdzHpkew7pCXj2mdXOZ3IJ9v9X72vtHEv0nuc1tKuPye32595QGiG5nH0j2Rj5cJqrgxuijmyM4TmczpqIDQOpELa9th1DayMe8ncC2TVM3clia7/9Lc8WB2+ARgkz+0ZJDOkyTwLFGfS7373VeCL1q9OOJq7LaL6KP4pTsC9QBAlSlSP7OKreFEieK+7T95OSKorKI7TOHPiErHYEOttAntBAi6rHqkWlmQvTLAj4UYJ6zjzekyUlRUx68YuUeotSN0/IGTbkdF7VfjurM07d7N3wCCZhTJbYV8pP6+zQ5BwFT/Sjt4+LQbMqv29JLU6cn0CqjIgKmJXnqQH6/1g4FoJZAqazSzfFFr26Sw67Dht6KLld2dQWjl0s7IjZI+TJqHWUMJc/XNVe3hEUp7+nDNgUx97EwSs0nc39EjMqYTTwso6ad6oAtqUF587SF3XHMNqmAl4+vbsUbGAvmV9RVSH6jM27o3TlfJJeJRLGN3NoaJlYJObjbwCWGQIl7XHo1R9egNLqHyP5xiubaW0WZg4CbbO+GdxWk0kuGBggpjv+gsAlq3Jf2jB3mRyVEg2CegEIevx0VgH+Z5cgglUCbqysqfE61RinDVIfG/nrEsaK6E3nXkWSRiiv1NCNXo3uAtbamHX58W5Vc5pDfRElzogFrXxrbcb2UKJyLFhFfRW19qkHHc+uZN13hUa5aGquXJPG0SUZzxuXmh8jTYvI7kNqk6O+nIWawahJ/4/zlaOuy70bdm+YBLMKebqPa+n6LMtkCeJz8AAOHhHEVm9pccD0o66Yqa5OH3cGWCYr2ptNgp+fF+y6q1g45jyCwT6hNVBYMLoRoxFP50QXASfS3xixPRn7Q0O5u/o2NGBHI/4LQWMBRHGJtkESVSdjExQEBFs3csAMEUb6yrcZDEXqmJtZ2X8zBnT0GPwmWcpeLzN6p93JN/pAg4+vgny+lQ+nDHJXA1sQVCCqTlHb35edfIhtGonGAM=","page_age":null},{"type":"web_search_result","title":"Markets News, June 9, 2025: Stocks Rise as Investors Await News on US-China Trade Talks; S&P 500, Nasdaq Trading at Highest Levels Since February","url":"https://www.investopedia.com/dow-jones-today-06092025-11750420","encrypted_content":"EpIhCioIBBgCIiRiY2JjZWJjMy1lYTFhLTRmNjktYTUwMy01YzgwNWU1Y2U0NzESDIQ0jz/ulBIATIUxshoMKOUiQek4i7Ja8C1DIjB/fentxdWOZIXcGXJs6iHTmfCW9bDK6IOEiboHWhBXU0WlMeW9Z4AXpRQfN2xYORAqlSDuGiqN203b24YfVzvbX+0aN+sU9SV0AyY4lG5k07OmorwzIBezgjd9KO9g6kWeZC4hzTRF5eAkJPCgasmKiqcf8HvVxTq8pMPpZknHTcLU5sO1+s3EIPe1DWXnPZ4huweyKDRxlB8PhY3gMHDQnAoxj1WaAHSOhfdxmhJGwQy3r7fxJtbJ5JRRTbjZIkOQKc8o7NSe2sBE1k4hLCo3swU5c3lFJdDX+HzGraHR3AWbWEgtf0Y4KJGoTSutbH7yI3LyHiiaHWq0S4XWCSfn52E4H5tpABbidQVXKfHKml7+1VxnyxgxnPJSuSm1J4Xw8fzGCRj2NBRC8Y1c+LjyjZIqQiCVc5yTliwCXTh4Rf8VlBwF9+2+fk2COvSqNvVJ/u4KT1X3oqcDwGf/M4bEdVMlZvqArJMC/bTbloGur/ohLFKwRBJsNb777SS8cuzqPRaGI0RYT6f99hH/dSHNNX1okrk56FxiYnZxlwLEbKCmZdF9Q2Mvvb8h9LBvWY6kBXgdiUkZcSBFKCwaRdTle89cOzdk3JRg4owtqO0aT12MAqfLkIJDNwRh7ZOthi+NJeLDaSdnD80aEfC8eicsDpxkXPG5+2UNU2g/F/LrVPgwYCqZ2/vqv4UrPlN4btSOWMpbe5f0G2LU27+awaHqufiI5Oo5Ovg9eTrIMt7dwm30MaWYdTDcPtxNyr6ZmiI6tJVGffKIUhasXz0MmkYxpzbPzrzP13wtpBsysG+SYodklMa3AP8V1bmitfN/ljFjTsZnW7mIvb+Uq3kSgIO4168vgo78va6DMMK4SxMY2T2Sth1zASjh7+m0Y4Kj04/pnZcHTlzmm7lUt2kVtLHElj2DOHMlB5k0w92aoRI2LeKKOVmwjL1zMhBbAf1boynfKUmxyMhR6vF/E7/USZV8tt4vvO/u2lyytwJZCU47s9yE8WlzjsOdbU3OdecXw7fiOlvqij0LPcjSyxjSLr+bW/wkZ+4QUtcbQGG80C1KOs1B4gA2ohPrANWOeB4NiQ+IzJR+d26hrXSMaKUWEcYV3ZuNR1N+jDEWlBi8Z5rfjhB/TG4BCzJNjCYIhFLIOAd6USI/OVIgA+J56fFg8PcE4izRsiA840mc5N9P/5rd7NoeT26n+NRKYjny1HOlePALrzpwJ4Yrq6iW3y0kFujL4Vl1RW2GFf0d4SWaI8nmAHhNvBElSgSqfqc05L2S49i+ZLJ0ht8qQB+Le0Kku3SEVFxTOCMgO26WXkVP2d9Yle9IPR6uaOxfC/8qXm0joECotNeYEcaEsd3au+/lLlXfcCQIrJR8AzJlg2pJNUaWTSCshtoiVf/qVVmDi7xKjUTUGnUvTXwjv15P+nWxN6vnWKYxazj+y6N6HRrJxolZwGDT8dr23KOFPJroLg4Bfzx9HgusS7aZ0UjXJNvxHt/1FmRsmu4Ztj/hn7UrsFuetYhq6fEK3J30aS79qn605tAb4hWeVf1t8Mhdujm1Yg8Q4r4N5SHrqs+qeCYNXdKyvsUSQRDnsjWirduSxyy+q2Kpz5uXT14YHu+UIkshhowxJPYNfH6SbfbEVxupRTsODe9e5K0UbKcbXvq3z8sZS7SUJFl4+umgJUrj2tR56rcpYJKO+5Mr+LLuq4aF9VBkQTUhBvaLuvSezAMjeUUxK5kyJTEY7Pt2A3maU2UxJHIlMRm/F6KvreDDLFJoJrsW77iWK10PG0k3uBAg1VNVCF9y63kI4+yUDpsmhtuvEDw8Wg1N4eS1Ogvxn+YGtmh6rlLp5RAPvnVl47xFZSbRS+NVMh3cNsmU1wDOeekWDMut4HDFyzW1USHyNtmKSCNF9RSsq2ALpEdsTAtb9SlL+dA25ChAbxugbIaSdvRQOVuq8pfDMgtjT7iR5HRbcX2fevcxTl9giTDylNS1EsNTSi2dXPIUVCvLPoqMabhSjFu8WeGMbr1WopCs6XVQD3vtsIhAPKvbKd9sTk78mU3hwcKhqqSRsOfQXSTA2D6N0kRn5BgwgiETxVXw3t0K3rzCOd91//Y+g/25x3+vCZY36BVuSG+06i5aBAA4HvdE9MRBpXe3zU4tgtSAF8ScDA0OPphS4mDPhtR8GSbIGPxMW+tjs9/JgxeCOoK1heweTACKXCjtSKFEP8AMSdTsor5v64tNi0FpH0Vqh3XZp+tGJOdWQYiPL3ZzOhGhPxJnuiZFb0uKALF1w+QUusnhzV07QGt32uRdTt7V/5/L2U6i7FK2KvzwUgzbWkm4v7e2X8vaP8f+w0xWZCCyXxZvUdVnC8HBs+i0P8guv2ob1x7HO83BsPfgYaaYtngzGO0wlDaALCIgW7QrryC92XE+e1mRFSJnoqsBF/z+51q+Hpm4CGdyj/8GBBTSZF+SgBgzuZav0R0APTfisktHTL9Q4KwrhQr5OLuZ8yeBXyUwJkg/ZiBU7SGMzZZsUnhwz51AgxWiB63ITazFzSfcuwuumeQM3BUIcx8Z/KjPnhzWqyyLIw7pLeLar5qYenXySTotxa4oNUsukkrh5DYlF/TDWvN6PX/prWV66vfxkUIdtYaxx9OrVdL5xdTmFMBpxZNaHloch/KGnN/MZacCNMu3vX3GrxPXrEB7fnV61c1Z22MN/UFJTG3ulhh8+xbAuSQuzVpaaCUy5PKhugSxqTALGIzWF1yxq9mG2NgXivnmN9Ju+jRJBMVzGmZMpmPC076yhRvOFPZYHVTSoPl/5ApqbNHLIhCTwjo6L/vyfUekeK7rB4y6iQJ1ymiQcRUZbe1VW78gYJ2LVhhrlQEqkhgzr1XEnv0qXLSiXrbo7WXQ1ENOnOfonDulRTyMieJ4DVB1cnLQlFkzwAzIOwUj2PgiOzL94G/fTwOeOfcFmghK2pFDFnltm43o/4LOtl2Nv52Rmh4PhNZn+eb51R/DjTZuEVt8x1Y4WMttp3QlrCKcYFLL/P5ytS1WYAA2QkBaH/CAWyhfY1CpcMb1u2L67GQ3erXn05+VlBElasIIvSchQMwlbDS5lKE8TP3/x7buwgZJNpTnZBAORulNNceYIx2iQilK8aiCKW2DXwJ/Gj9P9NUzTfSOpc/H5HbjgqGrqCMW8YNaK7XFjbl7MZmrOoxNXkDO63HcaqlbGsPcmG/Wpsi1v7KmvGJGRvYGQ5lIvZ2WGKip/UpgJhCRiamjgN0DGxNJ6j+rD+wXRBrq1aoHCMcxmAcxxTs9rSdgB33XenYWLFrYJz+Xd4QStoYJqj17pbnoPsc5o4DJkOtiDo5FgDs41KoEYs8dbxpltdocXYnSIBvqhBf2iNZNdWX60a58DZK7rv3Oh5w2dzgUhzlP3J/ZskPGeyjH+XO+5+bEXEauLVOzI4MtzSaXGR0dTdoPk4DJsjGmiVuVESqb/Q3Avy1+DQWA7mJ/nTjPtuqAlfgiCgiGvLCIgrcSEtqCahs4w6AXeoLz91PMbAygyJenoRdAT7f6S0btFgX4ivltP5Yi3ITegVCLif+tby+jRXxq4J/5a+8PrBGO6qLnQvEe+dPelP815A6NKA5RvgaFMW0YSgKGwpTnXw8Q3zwU32BjtjOtNS3esEWcS5rAmx0Mfu1ym67cd1g05Ns4dA867z5AMqvwnDuUQcpt6mtXxmv+oaFsWNDTsNC8M+0V7I82eSxdvG2aE/5nqm1m/QbaiyT7mvUKlQGAoZ07brKZe4vaeAy1iSmKzLTUCfEUvwLzFW4cVHydeAOmM+VoKHX+tS68Vakl7/oyu1MURLXrQhO8lWddKMiTYMelhcMBqRz4WNqKeFEFuDTOtiQl+2zEyIcEENf/ixb7MIzoIxoC7WSnzBm8p5YsqNCG/+3DpPMToFDFnyne0+5qWlCqqPjO2zW/XbBM6phUc0iGoZiNtG0u5mgqJFCKA6+6gAAvrSszVvMT6NJphOShjhd0NbH7vZ5nRLtaVKPq9etAVYFi7MrN0d1RuaKBz4VcR84zDs/0HISNUHxXBh0vIRfUTZ264j/HMoKmx4lmYSc8XZ6FkEKFpObgDOZW3A+URFkEUxYV1a2agLUZwWhICDfYgk4VgcUIdp2o49LlP+dRoC/P1x8dM+JvU036Rk9jjdAu/ZwDPPZXeiiQwZiu2ytKpGfYq2bBU7Q1PlEqP+9b+UfXp5Jf/JSfnPTTPz2QKOsY4vMDc6TyJTNV23GR6RzgtqoUlWmzE4GFoEtvmjyEhluj0S/qcIACzWKVr/IvKI9TFeo+1Ds+FXVadNdQh8DlPqkAQMKrYPwkxc8HwK8DOndYeouPBZReKehwpd0aAst2oy5ytDYGBDJ4W1RpS4q/4PxTwffwwsA0B1hE9WcwrqH06pcY1zu4XBIWZwbUg3nASexRax66YV1b6b5TfQqrCJzLTE0r2YfcHqwO19vH1pi7JVEe7/HvH/nV2QEFAFvjKgyFojEUHq4eya0YB/GhqwlLBGsY/91oFMHLwgEtquLuv7UK/ZU4Wrh2ZXtxHer+CFiCEuuuOxVIX+Fu02Wwl23SdM6BtWEnv0EhtYu7EgnJa1ybRJYW+1QsJND/tInImyA7/BD6be9q4DVMFGflcgW/mdtulWVUxS9CKtPX9lbpcpjkobI7lDc2YrVdVhJD9EiFWm21tkhixod9tiebSnP82TwMnylTX0C+tbYKvbL/Ztlz4OS1mXiUemB/ircwIJYBqTa2QSQUJeYZzVt7pvbfBnvV/pGcY7ZMtGizO5ZeRodVm4SpDc2/rH+qYkjJmZhkkjpAFmW/nGguhfyHO4uOat/ITIyqEW6D2z7riIwDgxLUBp8JqZDJzcddpQGy4Tba2rTJrd4C5UkU/G/Tv7ZgVT7FMIVOdjrsBgeLKAGVVcYRDi4EgP8ym9FPmgG5SMWXI2qL4TMwqqyYQwWvC0iI41dh5f/wiogF+QgaivyfPP21qpVcWyx3FIvt8jOu2kJgKWo8+XXZg3HoFqRXaywJXZuzvCTOKSYN6bXVPqNc4+CBaD2DkwdK8wSVYv6gyurLbu4vCGg68iGPnxI1DT5TxGjQT2yxAth5bhYCxIto1CNpwJrErwhtVn8SVXsCR3v/QmpsxTalaIwT8bUQj5Yvcz/dutrpSF+oyEI6q+Er8JimZCCXwpAUzQXijlrt9uPbcpCBFSLLq57b4Z0bynTn/qD2HMLLY3/gNCaCfLUrKEmZ+/Xo+D6FU36CQwOKoXvXoFt4cGL9WAEXMmLijiE9XwbvrYvsp8K0WcrPVK6n0ohx4tUYBhJ7sDAi9umwvQw+HoeiIa+O6fGtQqYs9Iz8RztMQAY1CR6oIY5psC8ui9BMDfh+IH9SYdhyjdx4LIQtk1hA+UMMl8YCnkFutrRDeXpkAvCv6GbskmQLAnOjVdS2vXZhdCX1RxJLW/yWIdaqE73FWUmggg9sHHW53UK2XiRD/pAWMDJs2a75wwNDFN6WufqW4gN5Lz0Ti1LwRHuLpUcUFBhMpOA6GFJ6dXhpVpkUMSPH0HkavS4dJ4uy0ELnFQWJqJhQolfZ585BGAM=","page_age":null}]}}\n\n`, `data: {"type":"content_block_stop","index":2}\n\n`, `data: {"type":"content_block_start","index":3,"content_block":{"type":"text","text":""}}\n\n`, `data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":"Based on the search results,"}}\n\n`, `data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" here's a comprehensive overview of current"}}\n\n`, `data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" stock market trends: \\n\\nMarket Performance:"}}\n\n`, `data: {"type":"content_block_stop","index":3}\n\n`, `data: {"type":"content_block_start","index":4,"content_block":{"citations":[],"type":"text","text":""}}\n\n`, `data: {"type":"content_block_delta","index":4,"delta":{"type":"citations_delta","citation":{"type":"web_search_result_location","cited_text":"Through Friday's close, the S&P 500 was up up 2% since the start of the year, while the Nasdaq Composite had gained 1.1% and the Dow Jones Industrial ...","url":"https://www.investopedia.com/dow-jones-today-06092025-11750420","title":"Markets News, June 9, 2025: Stocks Rise as Investors Await News on US-China Trade Talks; S&P 500, Nasdaq Trading at Highest Levels Since February","encrypted_index":"EpEBCioIBBgCIiRiY2JjZWJjMy1lYTFhLTRmNjktYTUwMy01YzgwNWU1Y2U0NzESDGJyCJ7HnEGjHLeB8hoMsaQDATkym4QTF6g9IjCnRoiZ2XE66u8kx6lBEwocXiyHus1/YL2l9cPwS8Zs/PL9uplW+u0XlETlS0z1ZtYqFQ/TIKRThFDiYozcZ8zgG4IN8pCHLhgE"}}}\n\n`, `data: {"type":"content_block_delta","index":4,"delta":{"type":"text_delta","text":"Through recent trading, the major indexes are showing modest gains for 2025:"}}\n\n`, `data: {"type":"content_block_stop","index":4}\n\n`, `data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":19455,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":829,"server_tool_use":{"web_search_requests":1}}}\n\n`, `data: {"type":"message_stop"}\n\n`, ], }; // Create a model with a predictable generateId function const mockProvider = createAnthropic({ apiKey: 'test-api-key', generateId: mockId(), }); const modelWithMockId = mockProvider('claude-3-haiku-20240307'); const { stream } = await modelWithMockId.doStream({ prompt: TEST_PROMPT, tools: [ { type: 'provider-defined', id: 'anthropic.web_search_20250305', name: 'web_search', args: {}, }, ], }); expect(await convertReadableStreamToArray(stream)).toMatchSnapshot(); }); }); }); it('should throw an api error when the server is overloaded', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'error', status: 529, body: '{"type":"error","error":{"details":null,"type":"overloaded_error","message":"Overloaded"}}', }; await expect(model.doStream({ prompt: TEST_PROMPT })).rejects.toThrow( 'Overloaded', ); }); it('should forward overloaded error during streaming', async () => { server.urls['https://api.anthropic.com/v1/messages'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"message_start","message":{"id":"msg_01KfpJoAEabmH2iHRRFjQMAG","type":"message","role":"assistant","content":[],"model":"claude-3-haiku-20240307","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":17,"output_tokens":1}}}\n\n`, `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n`, `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}\n\n`, `data: {"type":"error","error":{"details":null,"type":"overloaded_error","message":"Overloaded"}}\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "msg_01KfpJoAEabmH2iHRRFjQMAG", "modelId": "claude-3-haiku-20240307", "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "error": { "message": "Overloaded", "type": "overloaded_error", }, "type": "error", }, ] `); }); }); }); --- File: /ai/packages/anthropic/src/anthropic-messages-language-model.ts --- import { JSONObject, JSONValue, LanguageModelV2, LanguageModelV2CallWarning, LanguageModelV2Content, LanguageModelV2FinishReason, LanguageModelV2FunctionTool, LanguageModelV2Prompt, LanguageModelV2StreamPart, LanguageModelV2Usage, SharedV2ProviderMetadata, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { FetchFunction, ParseResult, Resolvable, combineHeaders, createEventSourceResponseHandler, createJsonResponseHandler, generateId, parseProviderOptions, postJsonToApi, resolve, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { anthropicFailedResponseHandler } from './anthropic-error'; import { AnthropicMessagesModelId, anthropicProviderOptions, } from './anthropic-messages-options'; import { prepareTools } from './anthropic-prepare-tools'; import { convertToAnthropicMessagesPrompt } from './convert-to-anthropic-messages-prompt'; import { mapAnthropicStopReason } from './map-anthropic-stop-reason'; const citationSchemas = { webSearchResult: z.object({ type: z.literal('web_search_result_location'), cited_text: z.string(), url: z.string(), title: z.string(), encrypted_index: z.string(), }), pageLocation: z.object({ type: z.literal('page_location'), cited_text: z.string(), document_index: z.number(), document_title: z.string().nullable(), start_page_number: z.number(), end_page_number: z.number(), }), charLocation: z.object({ type: z.literal('char_location'), cited_text: z.string(), document_index: z.number(), document_title: z.string().nullable(), start_char_index: z.number(), end_char_index: z.number(), }), }; const citationSchema = z.discriminatedUnion('type', [ citationSchemas.webSearchResult, citationSchemas.pageLocation, citationSchemas.charLocation, ]); const documentCitationSchema = z.discriminatedUnion('type', [ citationSchemas.pageLocation, citationSchemas.charLocation, ]); type Citation = z.infer<typeof citationSchema>; export type DocumentCitation = z.infer<typeof documentCitationSchema>; export type AnthropicProviderMetadata = SharedV2ProviderMetadata & { usage?: Record<string, JSONValue>; }; function processCitation( citation: Citation, citationDocuments: Array<{ title: string; filename?: string; mediaType: string; }>, generateId: () => string, onSource: (source: any) => void, ) { if (citation.type === 'page_location' || citation.type === 'char_location') { const source = createCitationSource( citation, citationDocuments, generateId, ); if (source) { onSource(source); } } } function createCitationSource( citation: DocumentCitation, citationDocuments: Array<{ title: string; filename?: string; mediaType: string; }>, generateId: () => string, ) { const documentInfo = citationDocuments[citation.document_index]; if (!documentInfo) { return null; } const providerMetadata = citation.type === 'page_location' ? { citedText: citation.cited_text, startPageNumber: citation.start_page_number, endPageNumber: citation.end_page_number, } : { citedText: citation.cited_text, startCharIndex: citation.start_char_index, endCharIndex: citation.end_char_index, }; return { type: 'source' as const, sourceType: 'document' as const, id: generateId(), mediaType: documentInfo.mediaType, title: citation.document_title ?? documentInfo.title, filename: documentInfo.filename, providerMetadata: { anthropic: providerMetadata, }, }; } type AnthropicMessagesConfig = { provider: string; baseURL: string; headers: Resolvable<Record<string, string | undefined>>; fetch?: FetchFunction; buildRequestUrl?: (baseURL: string, isStreaming: boolean) => string; transformRequestBody?: (args: Record<string, any>) => Record<string, any>; supportedUrls?: () => LanguageModelV2['supportedUrls']; generateId?: () => string; }; export class AnthropicMessagesLanguageModel implements LanguageModelV2 { readonly specificationVersion = 'v2'; readonly modelId: AnthropicMessagesModelId; private readonly config: AnthropicMessagesConfig; private readonly generateId: () => string; constructor( modelId: AnthropicMessagesModelId, config: AnthropicMessagesConfig, ) { this.modelId = modelId; this.config = config; this.generateId = config.generateId ?? generateId; } supportsUrl(url: URL): boolean { return url.protocol === 'https:'; } get provider(): string { return this.config.provider; } get supportedUrls() { return this.config.supportedUrls?.() ?? {}; } private async getArgs({ prompt, maxOutputTokens = 4096, // 4096: max model output tokens TODO update default in v5 temperature, topP, topK, frequencyPenalty, presencePenalty, stopSequences, responseFormat, seed, tools, toolChoice, providerOptions, }: Parameters<LanguageModelV2['doGenerate']>[0]) { const warnings: LanguageModelV2CallWarning[] = []; if (frequencyPenalty != null) { warnings.push({ type: 'unsupported-setting', setting: 'frequencyPenalty', }); } if (presencePenalty != null) { warnings.push({ type: 'unsupported-setting', setting: 'presencePenalty', }); } if (seed != null) { warnings.push({ type: 'unsupported-setting', setting: 'seed', }); } if (responseFormat?.type === 'json') { if (responseFormat.schema == null) { warnings.push({ type: 'unsupported-setting', setting: 'responseFormat', details: 'JSON response format requires a schema. ' + 'The response format is ignored.', }); } else if (tools != null) { warnings.push({ type: 'unsupported-setting', setting: 'tools', details: 'JSON response format does not support tools. ' + 'The provided tools are ignored.', }); } } const jsonResponseTool: LanguageModelV2FunctionTool | undefined = responseFormat?.type === 'json' && responseFormat.schema != null ? { type: 'function', name: 'json', description: 'Respond with a JSON object.', inputSchema: responseFormat.schema, } : undefined; const anthropicOptions = await parseProviderOptions({ provider: 'anthropic', providerOptions, schema: anthropicProviderOptions, }); const { prompt: messagesPrompt, betas: messagesBetas } = await convertToAnthropicMessagesPrompt({ prompt, sendReasoning: anthropicOptions?.sendReasoning ?? true, warnings, }); const isThinking = anthropicOptions?.thinking?.type === 'enabled'; const thinkingBudget = anthropicOptions?.thinking?.budgetTokens; const baseArgs = { // model id: model: this.modelId, // standardized settings: max_tokens: maxOutputTokens, temperature, top_k: topK, top_p: topP, stop_sequences: stopSequences, // provider specific settings: ...(isThinking && { thinking: { type: 'enabled', budget_tokens: thinkingBudget }, }), // prompt: system: messagesPrompt.system, messages: messagesPrompt.messages, }; if (isThinking) { if (thinkingBudget == null) { throw new UnsupportedFunctionalityError({ functionality: 'thinking requires a budget', }); } if (baseArgs.temperature != null) { baseArgs.temperature = undefined; warnings.push({ type: 'unsupported-setting', setting: 'temperature', details: 'temperature is not supported when thinking is enabled', }); } if (topK != null) { baseArgs.top_k = undefined; warnings.push({ type: 'unsupported-setting', setting: 'topK', details: 'topK is not supported when thinking is enabled', }); } if (topP != null) { baseArgs.top_p = undefined; warnings.push({ type: 'unsupported-setting', setting: 'topP', details: 'topP is not supported when thinking is enabled', }); } // adjust max tokens to account for thinking: baseArgs.max_tokens = maxOutputTokens + thinkingBudget; } const { tools: anthropicTools, toolChoice: anthropicToolChoice, toolWarnings, betas: toolsBetas, } = prepareTools( jsonResponseTool != null ? { tools: [jsonResponseTool], toolChoice: { type: 'tool', toolName: jsonResponseTool.name }, disableParallelToolUse: anthropicOptions?.disableParallelToolUse, } : { tools: tools ?? [], toolChoice, disableParallelToolUse: anthropicOptions?.disableParallelToolUse, }, ); return { args: { ...baseArgs, tools: anthropicTools, tool_choice: anthropicToolChoice, }, warnings: [...warnings, ...toolWarnings], betas: new Set([...messagesBetas, ...toolsBetas]), usesJsonResponseTool: jsonResponseTool != null, }; } private async getHeaders({ betas, headers, }: { betas: Set<string>; headers: Record<string, string | undefined> | undefined; }) { return combineHeaders( await resolve(this.config.headers), betas.size > 0 ? { 'anthropic-beta': Array.from(betas).join(',') } : {}, headers, ); } private buildRequestUrl(isStreaming: boolean): string { return ( this.config.buildRequestUrl?.(this.config.baseURL, isStreaming) ?? `${this.config.baseURL}/messages` ); } private transformRequestBody(args: Record<string, any>): Record<string, any> { return this.config.transformRequestBody?.(args) ?? args; } private extractCitationDocuments(prompt: LanguageModelV2Prompt): Array<{ title: string; filename?: string; mediaType: string; }> { const isCitationPart = (part: { type: string; mediaType?: string; providerOptions?: { anthropic?: { citations?: { enabled?: boolean } } }; }) => { if (part.type !== 'file') { return false; } if ( part.mediaType !== 'application/pdf' && part.mediaType !== 'text/plain' ) { return false; } const anthropic = part.providerOptions?.anthropic; const citationsConfig = anthropic?.citations as | { enabled?: boolean } | undefined; return citationsConfig?.enabled ?? false; }; return prompt .filter(message => message.role === 'user') .flatMap(message => message.content) .filter(isCitationPart) .map(part => { // TypeScript knows this is a file part due to our filter const filePart = part as Extract<typeof part, { type: 'file' }>; return { title: filePart.filename ?? 'Untitled Document', filename: filePart.filename, mediaType: filePart.mediaType, }; }); } async doGenerate( options: Parameters<LanguageModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>> { const { args, warnings, betas, usesJsonResponseTool } = await this.getArgs(options); // Extract citation documents for response processing const citationDocuments = this.extractCitationDocuments(options.prompt); const { responseHeaders, value: response, rawValue: rawResponse, } = await postJsonToApi({ url: this.buildRequestUrl(false), headers: await this.getHeaders({ betas, headers: options.headers }), body: this.transformRequestBody(args), failedResponseHandler: anthropicFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( anthropicMessagesResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const content: Array<LanguageModelV2Content> = []; // map response content to content array for (const part of response.content) { switch (part.type) { case 'text': { // when a json response tool is used, the tool call is returned as text, // so we ignore the text content: if (!usesJsonResponseTool) { content.push({ type: 'text', text: part.text }); // Process citations if present if (part.citations) { for (const citation of part.citations) { processCitation( citation, citationDocuments, this.generateId, source => content.push(source), ); } } } break; } case 'thinking': { content.push({ type: 'reasoning', text: part.thinking, providerMetadata: { anthropic: { signature: part.signature, } satisfies AnthropicReasoningMetadata, }, }); break; } case 'redacted_thinking': { content.push({ type: 'reasoning', text: '', providerMetadata: { anthropic: { redactedData: part.data, } satisfies AnthropicReasoningMetadata, }, }); break; } case 'tool_use': { content.push( // when a json response tool is used, the tool call becomes the text: usesJsonResponseTool ? { type: 'text', text: JSON.stringify(part.input), } : { type: 'tool-call', toolCallId: part.id, toolName: part.name, input: JSON.stringify(part.input), }, ); break; } case 'server_tool_use': { if (part.name === 'web_search') { content.push({ type: 'tool-call', toolCallId: part.id, toolName: part.name, input: JSON.stringify(part.input), providerExecuted: true, }); } break; } case 'web_search_tool_result': { if (Array.isArray(part.content)) { content.push({ type: 'tool-result', toolCallId: part.tool_use_id, toolName: 'web_search', result: part.content.map(result => ({ url: result.url, title: result.title, pageAge: result.page_age ?? null, encryptedContent: result.encrypted_content, type: result.type, })), providerExecuted: true, }); for (const result of part.content) { content.push({ type: 'source', sourceType: 'url', id: this.generateId(), url: result.url, title: result.title, providerMetadata: { anthropic: { pageAge: result.page_age ?? null, }, }, }); } } else { content.push({ type: 'tool-result', toolCallId: part.tool_use_id, toolName: 'web_search', isError: true, result: { type: 'web_search_tool_result_error', errorCode: part.content.error_code, }, providerExecuted: true, }); } break; } } } return { content, finishReason: mapAnthropicStopReason({ finishReason: response.stop_reason, isJsonResponseFromTool: usesJsonResponseTool, }), usage: { inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens, totalTokens: response.usage.input_tokens + response.usage.output_tokens, cachedInputTokens: response.usage.cache_read_input_tokens ?? undefined, }, request: { body: args }, response: { id: response.id ?? undefined, modelId: response.model ?? undefined, headers: responseHeaders, body: rawResponse, }, warnings, providerMetadata: { anthropic: { usage: response.usage as JSONObject, cacheCreationInputTokens: response.usage.cache_creation_input_tokens ?? null, }, }, }; } async doStream( options: Parameters<LanguageModelV2['doStream']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>> { const { args, warnings, betas, usesJsonResponseTool } = await this.getArgs(options); // Extract citation documents for response processing const citationDocuments = this.extractCitationDocuments(options.prompt); const body = { ...args, stream: true }; const { responseHeaders, value: response } = await postJsonToApi({ url: this.buildRequestUrl(true), headers: await this.getHeaders({ betas, headers: options.headers }), body: this.transformRequestBody(body), failedResponseHandler: anthropicFailedResponseHandler, successfulResponseHandler: createEventSourceResponseHandler( anthropicMessagesChunkSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); let finishReason: LanguageModelV2FinishReason = 'unknown'; const usage: LanguageModelV2Usage = { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, }; const contentBlocks: Record< number, | { type: 'tool-call'; toolCallId: string; toolName: string; input: string; providerExecuted?: boolean; } | { type: 'text' | 'reasoning' } > = {}; let providerMetadata: AnthropicProviderMetadata | undefined = undefined; let blockType: | 'text' | 'thinking' | 'tool_use' | 'redacted_thinking' | 'server_tool_use' | 'web_search_tool_result' | undefined = undefined; const generateId = this.generateId; return { stream: response.pipeThrough( new TransformStream< ParseResult<z.infer<typeof anthropicMessagesChunkSchema>>, LanguageModelV2StreamPart >({ start(controller) { controller.enqueue({ type: 'stream-start', warnings }); }, transform(chunk, controller) { if (options.includeRawChunks) { controller.enqueue({ type: 'raw', rawValue: chunk.rawValue }); } if (!chunk.success) { controller.enqueue({ type: 'error', error: chunk.error }); return; } const value = chunk.value; switch (value.type) { case 'ping': { return; // ignored } case 'content_block_start': { const contentBlockType = value.content_block.type; blockType = contentBlockType; switch (contentBlockType) { case 'text': { contentBlocks[value.index] = { type: 'text' }; controller.enqueue({ type: 'text-start', id: String(value.index), }); return; } case 'thinking': { contentBlocks[value.index] = { type: 'reasoning' }; controller.enqueue({ type: 'reasoning-start', id: String(value.index), }); return; } case 'redacted_thinking': { contentBlocks[value.index] = { type: 'reasoning' }; controller.enqueue({ type: 'reasoning-start', id: String(value.index), providerMetadata: { anthropic: { redactedData: value.content_block.data, } satisfies AnthropicReasoningMetadata, }, }); return; } case 'tool_use': { contentBlocks[value.index] = usesJsonResponseTool ? { type: 'text' } : { type: 'tool-call', toolCallId: value.content_block.id, toolName: value.content_block.name, input: '', }; controller.enqueue( usesJsonResponseTool ? { type: 'text-start', id: String(value.index) } : { type: 'tool-input-start', id: value.content_block.id, toolName: value.content_block.name, }, ); return; } case 'server_tool_use': { if (value.content_block.name === 'web_search') { contentBlocks[value.index] = { type: 'tool-call', toolCallId: value.content_block.id, toolName: value.content_block.name, input: '', providerExecuted: true, }; controller.enqueue({ type: 'tool-input-start', id: value.content_block.id, toolName: value.content_block.name, providerExecuted: true, }); } return; } case 'web_search_tool_result': { const part = value.content_block; if (Array.isArray(part.content)) { controller.enqueue({ type: 'tool-result', toolCallId: part.tool_use_id, toolName: 'web_search', result: part.content.map(result => ({ url: result.url, title: result.title, pageAge: result.page_age ?? null, encryptedContent: result.encrypted_content, type: result.type, })), providerExecuted: true, }); for (const result of part.content) { controller.enqueue({ type: 'source', sourceType: 'url', id: generateId(), url: result.url, title: result.title, providerMetadata: { anthropic: { pageAge: result.page_age ?? null, }, }, }); } } else { controller.enqueue({ type: 'tool-result', toolCallId: part.tool_use_id, toolName: 'web_search', isError: true, result: { type: 'web_search_tool_result_error', errorCode: part.content.error_code, }, providerExecuted: true, }); } return; } default: { const _exhaustiveCheck: never = contentBlockType; throw new Error( `Unsupported content block type: ${_exhaustiveCheck}`, ); } } } case 'content_block_stop': { // when finishing a tool call block, send the full tool call: if (contentBlocks[value.index] != null) { const contentBlock = contentBlocks[value.index]; switch (contentBlock.type) { case 'text': { controller.enqueue({ type: 'text-end', id: String(value.index), }); break; } case 'reasoning': { controller.enqueue({ type: 'reasoning-end', id: String(value.index), }); break; } case 'tool-call': // when a json response tool is used, the tool call is returned as text, // so we ignore the tool call content: if (!usesJsonResponseTool) { controller.enqueue({ type: 'tool-input-end', id: contentBlock.toolCallId, }); controller.enqueue(contentBlock); } break; } delete contentBlocks[value.index]; } blockType = undefined; // reset block type return; } case 'content_block_delta': { const deltaType = value.delta.type; switch (deltaType) { case 'text_delta': { // when a json response tool is used, the tool call is returned as text, // so we ignore the text content: if (usesJsonResponseTool) { return; } controller.enqueue({ type: 'text-delta', id: String(value.index), delta: value.delta.text, }); return; } case 'thinking_delta': { controller.enqueue({ type: 'reasoning-delta', id: String(value.index), delta: value.delta.thinking, }); return; } case 'signature_delta': { // signature are only supported on thinking blocks: if (blockType === 'thinking') { controller.enqueue({ type: 'reasoning-delta', id: String(value.index), delta: '', providerMetadata: { anthropic: { signature: value.delta.signature, } satisfies AnthropicReasoningMetadata, }, }); } return; } case 'input_json_delta': { const contentBlock = contentBlocks[value.index]; const delta = value.delta.partial_json; if (usesJsonResponseTool) { if (contentBlock?.type !== 'text') { return; } controller.enqueue({ type: 'text-delta', id: String(value.index), delta, }); } else { if (contentBlock?.type !== 'tool-call') { return; } controller.enqueue({ type: 'tool-input-delta', id: contentBlock.toolCallId, delta, }); contentBlock.input += delta; } return; } case 'citations_delta': { const citation = value.delta.citation; processCitation( citation, citationDocuments, generateId, source => controller.enqueue(source), ); // Web search citations are handled in web_search_tool_result content block return; } default: { const _exhaustiveCheck: never = deltaType; throw new Error( `Unsupported delta type: ${_exhaustiveCheck}`, ); } } } case 'message_start': { usage.inputTokens = value.message.usage.input_tokens; usage.cachedInputTokens = value.message.usage.cache_read_input_tokens ?? undefined; providerMetadata = { anthropic: { usage: value.message.usage as JSONObject, cacheCreationInputTokens: value.message.usage.cache_creation_input_tokens ?? null, }, }; controller.enqueue({ type: 'response-metadata', id: value.message.id ?? undefined, modelId: value.message.model ?? undefined, }); return; } case 'message_delta': { usage.outputTokens = value.usage.output_tokens; usage.totalTokens = (usage.inputTokens ?? 0) + (value.usage.output_tokens ?? 0); finishReason = mapAnthropicStopReason({ finishReason: value.delta.stop_reason, isJsonResponseFromTool: usesJsonResponseTool, }); return; } case 'message_stop': { controller.enqueue({ type: 'finish', finishReason, usage, providerMetadata, }); return; } case 'error': { controller.enqueue({ type: 'error', error: value.error }); return; } default: { const _exhaustiveCheck: never = value; throw new Error(`Unsupported chunk type: ${_exhaustiveCheck}`); } } }, }), ), request: { body }, response: { headers: responseHeaders }, }; } } // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const anthropicMessagesResponseSchema = z.object({ type: z.literal('message'), id: z.string().nullish(), model: z.string().nullish(), content: z.array( z.discriminatedUnion('type', [ z.object({ type: z.literal('text'), text: z.string(), citations: z.array(citationSchema).optional(), }), z.object({ type: z.literal('thinking'), thinking: z.string(), signature: z.string(), }), z.object({ type: z.literal('redacted_thinking'), data: z.string(), }), z.object({ type: z.literal('tool_use'), id: z.string(), name: z.string(), input: z.unknown(), }), z.object({ type: z.literal('server_tool_use'), id: z.string(), name: z.string(), input: z.record(z.string(), z.unknown()).nullish(), }), z.object({ type: z.literal('web_search_tool_result'), tool_use_id: z.string(), content: z.union([ z.array( z.object({ type: z.literal('web_search_result'), url: z.string(), title: z.string(), encrypted_content: z.string(), page_age: z.string().nullish(), }), ), z.object({ type: z.literal('web_search_tool_result_error'), error_code: z.string(), }), ]), }), ]), ), stop_reason: z.string().nullish(), usage: z.looseObject({ input_tokens: z.number(), output_tokens: z.number(), cache_creation_input_tokens: z.number().nullish(), cache_read_input_tokens: z.number().nullish(), }), }); // limited version of the schema, focused on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const anthropicMessagesChunkSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('message_start'), message: z.object({ id: z.string().nullish(), model: z.string().nullish(), usage: z.looseObject({ input_tokens: z.number(), output_tokens: z.number(), cache_creation_input_tokens: z.number().nullish(), cache_read_input_tokens: z.number().nullish(), }), }), }), z.object({ type: z.literal('content_block_start'), index: z.number(), content_block: z.discriminatedUnion('type', [ z.object({ type: z.literal('text'), text: z.string(), }), z.object({ type: z.literal('thinking'), thinking: z.string(), }), z.object({ type: z.literal('tool_use'), id: z.string(), name: z.string(), }), z.object({ type: z.literal('redacted_thinking'), data: z.string(), }), z.object({ type: z.literal('server_tool_use'), id: z.string(), name: z.string(), input: z.record(z.string(), z.unknown()).nullish(), }), z.object({ type: z.literal('web_search_tool_result'), tool_use_id: z.string(), content: z.union([ z.array( z.object({ type: z.literal('web_search_result'), url: z.string(), title: z.string(), encrypted_content: z.string(), page_age: z.string().nullish(), }), ), z.object({ type: z.literal('web_search_tool_result_error'), error_code: z.string(), }), ]), }), ]), }), z.object({ type: z.literal('content_block_delta'), index: z.number(), delta: z.discriminatedUnion('type', [ z.object({ type: z.literal('input_json_delta'), partial_json: z.string(), }), z.object({ type: z.literal('text_delta'), text: z.string(), }), z.object({ type: z.literal('thinking_delta'), thinking: z.string(), }), z.object({ type: z.literal('signature_delta'), signature: z.string(), }), z.object({ type: z.literal('citations_delta'), citation: citationSchema, }), ]), }), z.object({ type: z.literal('content_block_stop'), index: z.number(), }), z.object({ type: z.literal('error'), error: z.object({ type: z.string(), message: z.string(), }), }), z.object({ type: z.literal('message_delta'), delta: z.object({ stop_reason: z.string().nullish() }), usage: z.object({ output_tokens: z.number() }), }), z.object({ type: z.literal('message_stop'), }), z.object({ type: z.literal('ping'), }), ]); export const anthropicReasoningMetadataSchema = z.object({ signature: z.string().optional(), redactedData: z.string().optional(), }); export type AnthropicReasoningMetadata = z.infer< typeof anthropicReasoningMetadataSchema >; --- File: /ai/packages/anthropic/src/anthropic-messages-options.ts --- import { z } from 'zod/v4'; // https://docs.anthropic.com/claude/docs/models-overview export type AnthropicMessagesModelId = | 'claude-opus-4-20250514' | 'claude-sonnet-4-20250514' | 'claude-3-7-sonnet-20250219' | 'claude-3-5-sonnet-latest' | 'claude-3-5-sonnet-20241022' | 'claude-3-5-sonnet-20240620' | 'claude-3-5-haiku-latest' | 'claude-3-5-haiku-20241022' | 'claude-3-opus-latest' | 'claude-3-opus-20240229' | 'claude-3-sonnet-20240229' | 'claude-3-haiku-20240307' | (string & {}); /** * Anthropic file part provider options for document-specific features. * These options apply to individual file parts (documents). */ export const anthropicFilePartProviderOptions = z.object({ /** * Citation configuration for this document. * When enabled, this document will generate citations in the response. */ citations: z .object({ /** * Enable citations for this document */ enabled: z.boolean(), }) .optional(), /** * Custom title for the document. * If not provided, the filename will be used. */ title: z.string().optional(), /** * Context about the document that will be passed to the model * but not used towards cited content. * Useful for storing document metadata as text or stringified JSON. */ context: z.string().optional(), }); export type AnthropicFilePartProviderOptions = z.infer< typeof anthropicFilePartProviderOptions >; export const anthropicProviderOptions = z.object({ sendReasoning: z.boolean().optional(), thinking: z .object({ type: z.union([z.literal('enabled'), z.literal('disabled')]), budgetTokens: z.number().optional(), }) .optional(), /** * Whether to disable parallel function calling during tool use. Default is false. * When set to true, Claude will use at most one tool per response. */ disableParallelToolUse: z.boolean().optional(), }); export type AnthropicProviderOptions = z.infer<typeof anthropicProviderOptions>; --- File: /ai/packages/anthropic/src/anthropic-prepare-tools.test.ts --- import { prepareTools } from './anthropic-prepare-tools'; describe('prepareTools', () => { it('should return undefined tools and tool_choice when tools are null', () => { const result = prepareTools({ tools: undefined }); expect(result).toEqual({ tools: undefined, tool_choice: undefined, toolWarnings: [], betas: new Set(), }); }); it('should return undefined tools and tool_choice when tools are empty', () => { const result = prepareTools({ tools: [] }); expect(result).toEqual({ tools: undefined, tool_choice: undefined, toolWarnings: [], betas: new Set(), }); }); it('should correctly prepare function tools', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'A test function', inputSchema: { type: 'object', properties: {} }, }, ], }); expect(result.tools).toEqual([ { name: 'testFunction', description: 'A test function', input_schema: { type: 'object', properties: {} }, }, ]); expect(result.toolChoice).toBeUndefined(); expect(result.toolWarnings).toEqual([]); }); it('should correctly prepare provider-defined tools', () => { const result = prepareTools({ tools: [ { type: 'provider-defined', id: 'anthropic.computer_20241022', name: 'computer', args: { displayWidthPx: 800, displayHeightPx: 600, displayNumber: 1 }, }, { type: 'provider-defined', id: 'anthropic.text_editor_20241022', name: 'text_editor', args: {}, }, { type: 'provider-defined', id: 'anthropic.bash_20241022', name: 'bash', args: {}, }, ], }); expect(result.tools).toEqual([ { name: 'computer', type: 'computer_20241022', display_width_px: 800, display_height_px: 600, display_number: 1, }, { name: 'str_replace_editor', type: 'text_editor_20241022', }, { name: 'bash', type: 'bash_20241022', }, ]); expect(result.toolChoice).toBeUndefined(); expect(result.toolWarnings).toEqual([]); }); it('should add warnings for unsupported tools', () => { const result = prepareTools({ tools: [ { type: 'provider-defined', id: 'unsupported.tool', name: 'unsupported_tool', args: {}, }, ], }); expect(result.tools).toEqual([]); expect(result.toolChoice).toBeUndefined(); expect(result.toolWarnings).toMatchInlineSnapshot(` [ { "tool": { "args": {}, "id": "unsupported.tool", "name": "unsupported_tool", "type": "provider-defined", }, "type": "unsupported-tool", }, ] `); }); it('should handle tool choice "auto"', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'Test', inputSchema: {}, }, ], toolChoice: { type: 'auto' }, }); expect(result.toolChoice).toEqual({ type: 'auto' }); }); it('should handle tool choice "required"', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'Test', inputSchema: {}, }, ], toolChoice: { type: 'required' }, }); expect(result.toolChoice).toEqual({ type: 'any' }); }); it('should handle tool choice "none"', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'Test', inputSchema: {}, }, ], toolChoice: { type: 'none' }, }); expect(result.tools).toBeUndefined(); expect(result.toolChoice).toBeUndefined(); }); it('should handle tool choice "tool"', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'Test', inputSchema: {}, }, ], toolChoice: { type: 'tool', toolName: 'testFunction' }, }); expect(result.toolChoice).toEqual({ type: 'tool', name: 'testFunction' }); }); it('should set cache control', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'Test', inputSchema: {}, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, ], }); expect(result.tools).toMatchInlineSnapshot(` [ { "cache_control": { "type": "ephemeral", }, "description": "Test", "input_schema": {}, "name": "testFunction", }, ] `); }); }); --- File: /ai/packages/anthropic/src/anthropic-prepare-tools.ts --- import { LanguageModelV2CallOptions, LanguageModelV2CallWarning, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { AnthropicTool, AnthropicToolChoice } from './anthropic-api-types'; import { getCacheControl } from './get-cache-control'; import { webSearch_20250305ArgsSchema } from './tool/web-search_20250305'; function isWebSearchTool( tool: unknown, ): tool is Extract<AnthropicTool, { type: 'web_search_20250305' }> { return ( typeof tool === 'object' && tool !== null && 'type' in tool && tool.type === 'web_search_20250305' ); } export function prepareTools({ tools, toolChoice, disableParallelToolUse, }: { tools: LanguageModelV2CallOptions['tools']; toolChoice?: LanguageModelV2CallOptions['toolChoice']; disableParallelToolUse?: boolean; }): { tools: Array<AnthropicTool> | undefined; toolChoice: AnthropicToolChoice | undefined; toolWarnings: LanguageModelV2CallWarning[]; betas: Set<string>; } { // when the tools array is empty, change it to undefined to prevent errors: tools = tools?.length ? tools : undefined; const toolWarnings: LanguageModelV2CallWarning[] = []; const betas = new Set<string>(); if (tools == null) { return { tools: undefined, toolChoice: undefined, toolWarnings, betas }; } const anthropicTools: AnthropicTool[] = []; for (const tool of tools) { // handle direct web search tool objects passed from provider options if (isWebSearchTool(tool)) { anthropicTools.push(tool); continue; } switch (tool.type) { case 'function': const cacheControl = getCacheControl(tool.providerOptions); anthropicTools.push({ name: tool.name, description: tool.description, input_schema: tool.inputSchema, cache_control: cacheControl, }); break; case 'provider-defined': switch (tool.id) { case 'anthropic.computer_20250124': betas.add('computer-use-2025-01-24'); anthropicTools.push({ name: 'computer', type: 'computer_20250124', display_width_px: tool.args.displayWidthPx as number, display_height_px: tool.args.displayHeightPx as number, display_number: tool.args.displayNumber as number, }); break; case 'anthropic.computer_20241022': betas.add('computer-use-2024-10-22'); anthropicTools.push({ name: 'computer', type: 'computer_20241022', display_width_px: tool.args.displayWidthPx as number, display_height_px: tool.args.displayHeightPx as number, display_number: tool.args.displayNumber as number, }); break; case 'anthropic.text_editor_20250124': betas.add('computer-use-2025-01-24'); anthropicTools.push({ name: 'str_replace_editor', type: 'text_editor_20250124', }); break; case 'anthropic.text_editor_20241022': betas.add('computer-use-2024-10-22'); anthropicTools.push({ name: 'str_replace_editor', type: 'text_editor_20241022', }); break; case 'anthropic.text_editor_20250429': betas.add('computer-use-2025-01-24'); anthropicTools.push({ name: 'str_replace_based_edit_tool', type: 'text_editor_20250429', }); break; case 'anthropic.bash_20250124': betas.add('computer-use-2025-01-24'); anthropicTools.push({ name: 'bash', type: 'bash_20250124', }); break; case 'anthropic.bash_20241022': betas.add('computer-use-2024-10-22'); anthropicTools.push({ name: 'bash', type: 'bash_20241022', }); break; case 'anthropic.web_search_20250305': { const args = webSearch_20250305ArgsSchema.parse(tool.args); anthropicTools.push({ type: 'web_search_20250305', name: 'web_search', max_uses: args.maxUses, allowed_domains: args.allowedDomains, blocked_domains: args.blockedDomains, user_location: args.userLocation, }); break; } default: toolWarnings.push({ type: 'unsupported-tool', tool }); break; } break; default: toolWarnings.push({ type: 'unsupported-tool', tool }); break; } } if (toolChoice == null) { return { tools: anthropicTools, toolChoice: disableParallelToolUse ? { type: 'auto', disable_parallel_tool_use: disableParallelToolUse } : undefined, toolWarnings, betas, }; } const type = toolChoice.type; switch (type) { case 'auto': return { tools: anthropicTools, toolChoice: { type: 'auto', disable_parallel_tool_use: disableParallelToolUse, }, toolWarnings, betas, }; case 'required': return { tools: anthropicTools, toolChoice: { type: 'any', disable_parallel_tool_use: disableParallelToolUse, }, toolWarnings, betas, }; case 'none': // Anthropic does not support 'none' tool choice, so we remove the tools: return { tools: undefined, toolChoice: undefined, toolWarnings, betas }; case 'tool': return { tools: anthropicTools, toolChoice: { type: 'tool', name: toolChoice.toolName, disable_parallel_tool_use: disableParallelToolUse, }, toolWarnings, betas, }; default: { const _exhaustiveCheck: never = type; throw new UnsupportedFunctionalityError({ functionality: `tool choice type: ${_exhaustiveCheck}`, }); } } } --- File: /ai/packages/anthropic/src/anthropic-provider.ts --- import { LanguageModelV2, NoSuchModelError, ProviderV2, } from '@ai-sdk/provider'; import { FetchFunction, generateId, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { AnthropicMessagesLanguageModel } from './anthropic-messages-language-model'; import { AnthropicMessagesModelId } from './anthropic-messages-options'; import { anthropicTools } from './anthropic-tools'; export interface AnthropicProvider extends ProviderV2 { /** Creates a model for text generation. */ (modelId: AnthropicMessagesModelId): LanguageModelV2; /** Creates a model for text generation. */ languageModel(modelId: AnthropicMessagesModelId): LanguageModelV2; chat(modelId: AnthropicMessagesModelId): LanguageModelV2; messages(modelId: AnthropicMessagesModelId): LanguageModelV2; /** Anthropic-specific computer use tool. */ tools: typeof anthropicTools; } export interface AnthropicProviderSettings { /** Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.anthropic.com/v1`. */ baseURL?: string; /** API key that is being send using the `x-api-key` header. It defaults to the `ANTHROPIC_API_KEY` environment variable. */ apiKey?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; generateId?: () => string; } /** Create an Anthropic provider instance. */ export function createAnthropic( options: AnthropicProviderSettings = {}, ): AnthropicProvider { const baseURL = withoutTrailingSlash(options.baseURL) ?? 'https://api.anthropic.com/v1'; const getHeaders = () => ({ 'anthropic-version': '2023-06-01', 'x-api-key': loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'ANTHROPIC_API_KEY', description: 'Anthropic', }), ...options.headers, }); const createChatModel = (modelId: AnthropicMessagesModelId) => new AnthropicMessagesLanguageModel(modelId, { provider: 'anthropic.messages', baseURL, headers: getHeaders, fetch: options.fetch, generateId: options.generateId ?? generateId, supportedUrls: () => ({ 'image/*': [/^https?:\/\/.*$/], }), }); const provider = function (modelId: AnthropicMessagesModelId) { if (new.target) { throw new Error( 'The Anthropic model function cannot be called with the new keyword.', ); } return createChatModel(modelId); }; provider.languageModel = createChatModel; provider.chat = createChatModel; provider.messages = createChatModel; provider.textEmbeddingModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'textEmbeddingModel' }); }; provider.imageModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'imageModel' }); }; provider.tools = anthropicTools; return provider; } /** Default Anthropic provider instance. */ export const anthropic = createAnthropic(); --- File: /ai/packages/anthropic/src/anthropic-tools.ts --- import { bash_20241022 } from './tool/bash_20241022'; import { bash_20250124 } from './tool/bash_20250124'; import { computer_20241022 } from './tool/computer_20241022'; import { computer_20250124 } from './tool/computer_20250124'; import { textEditor_20241022 } from './tool/text-editor_20241022'; import { textEditor_20250124 } from './tool/text-editor_20250124'; import { textEditor_20250429 } from './tool/text-editor_20250429'; import { webSearch_20250305 } from './tool/web-search_20250305'; export const anthropicTools = { /** * Creates a tool for running a bash command. Must have name "bash". * * Image results are supported. * * @param execute - The function to execute the tool. Optional. */ bash_20241022, /** * Creates a tool for running a bash command. Must have name "bash". * * Image results are supported. * * @param execute - The function to execute the tool. Optional. */ bash_20250124, /** * Creates a tool for editing text. Must have name "str_replace_editor". */ textEditor_20241022, /** * Creates a tool for editing text. Must have name "str_replace_editor". */ textEditor_20250124, /** * Creates a tool for editing text. Must have name "str_replace_based_edit_tool". * Note: This version does not support the "undo_edit" command. */ textEditor_20250429, /** * Creates a tool for executing actions on a computer. Must have name "computer". * * Image results are supported. * * @param displayWidthPx - The width of the display being controlled by the model in pixels. * @param displayHeightPx - The height of the display being controlled by the model in pixels. * @param displayNumber - The display number to control (only relevant for X11 environments). If specified, the tool will be provided a display number in the tool definition. */ computer_20241022, /** * Creates a tool for executing actions on a computer. Must have name "computer". * * Image results are supported. * * @param displayWidthPx - The width of the display being controlled by the model in pixels. * @param displayHeightPx - The height of the display being controlled by the model in pixels. * @param displayNumber - The display number to control (only relevant for X11 environments). If specified, the tool will be provided a display number in the tool definition. * @param execute - The function to execute the tool. Optional. */ computer_20250124, /** * Creates a web search tool that gives Claude direct access to real-time web content. * Must have name "web_search". * * @param maxUses - Maximum number of web searches Claude can perform during the conversation. * @param allowedDomains - Optional list of domains that Claude is allowed to search. * @param blockedDomains - Optional list of domains that Claude should avoid when searching. * @param userLocation - Optional user location information to provide geographically relevant search results. */ webSearch_20250305, }; --- File: /ai/packages/anthropic/src/convert-to-anthropic-messages-prompt.test.ts --- import { LanguageModelV2CallWarning } from '@ai-sdk/provider'; import { convertToAnthropicMessagesPrompt } from './convert-to-anthropic-messages-prompt'; describe('system messages', () => { it('should convert a single system message into an anthropic system message', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [{ role: 'system', content: 'This is a system message' }], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [], system: [{ type: 'text', text: 'This is a system message' }], }, betas: new Set(), }); }); it('should convert multiple system messages into an anthropic system message', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'system', content: 'This is a system message' }, { role: 'system', content: 'This is another system message' }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [], system: [ { type: 'text', text: 'This is a system message' }, { type: 'text', text: 'This is another system message' }, ], }, betas: new Set(), }); }); }); describe('user messages', () => { it('should add image parts for UInt8Array images', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [ { type: 'file', data: 'AAECAw==', mediaType: 'image/png', }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [ { type: 'image', source: { data: 'AAECAw==', media_type: 'image/png', type: 'base64', }, }, ], }, ], system: undefined, }, betas: new Set(), }); }); it('should add image parts for URL images', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [ { type: 'file', data: new URL('https://example.com/image.png'), mediaType: 'image/*', }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [ { type: 'image', source: { type: 'url', url: 'https://example.com/image.png', }, }, ], }, ], system: undefined, }, betas: new Set(), }); }); it('should add PDF file parts for base64 PDFs', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [ { type: 'file', data: 'base64PDFdata', mediaType: 'application/pdf', }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [ { type: 'document', source: { type: 'base64', media_type: 'application/pdf', data: 'base64PDFdata', }, }, ], }, ], system: undefined, }, betas: new Set(['pdfs-2024-09-25']), }); }); it('should add PDF file parts for URL PDFs', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [ { type: 'file', data: new URL('https://example.com/document.pdf'), mediaType: 'application/pdf', }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [ { type: 'document', source: { type: 'url', url: 'https://example.com/document.pdf', }, }, ], }, ], system: undefined, }, betas: new Set(['pdfs-2024-09-25']), }); }); it('should add text file parts for text/plain documents', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [ { type: 'file', data: Buffer.from('sample text content', 'utf-8').toString( 'base64', ), mediaType: 'text/plain', filename: 'sample.txt', }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [ { type: 'document', source: { type: 'text', media_type: 'text/plain', data: 'sample text content', }, title: 'sample.txt', cache_control: undefined, }, ], }, ], system: undefined, }, betas: new Set(), }); }); it('should throw error for unsupported file types', async () => { await expect( convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [ { type: 'file', data: 'base64data', mediaType: 'video/mp4', }, ], }, ], sendReasoning: true, warnings: [], }), ).rejects.toThrow('media type: video/mp4'); }); }); describe('tool messages', () => { it('should convert a single tool result into an anthropic user message', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'tool', content: [ { type: 'tool-result', toolName: 'tool-1', toolCallId: 'tool-call-1', output: { type: 'json', value: { test: 'This is a tool message' }, }, }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'tool-call-1', is_error: undefined, content: JSON.stringify({ test: 'This is a tool message' }), }, ], }, ], system: undefined, }, betas: new Set(), }); }); it('should convert multiple tool results into an anthropic user message', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'tool', content: [ { type: 'tool-result', toolName: 'tool-1', toolCallId: 'tool-call-1', output: { type: 'json', value: { test: 'This is a tool message' }, }, }, { type: 'tool-result', toolName: 'tool-2', toolCallId: 'tool-call-2', output: { type: 'json', value: { something: 'else' } }, }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'tool-call-1', is_error: undefined, content: JSON.stringify({ test: 'This is a tool message' }), }, { type: 'tool_result', tool_use_id: 'tool-call-2', is_error: undefined, content: JSON.stringify({ something: 'else' }), }, ], }, ], system: undefined, }, betas: new Set(), }); }); it('should combine user and tool messages', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'tool', content: [ { type: 'tool-result', toolName: 'tool-1', toolCallId: 'tool-call-1', output: { type: 'json', value: { test: 'This is a tool message' }, }, }, ], }, { role: 'user', content: [{ type: 'text', text: 'This is a user message' }], }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'tool-call-1', is_error: undefined, content: JSON.stringify({ test: 'This is a tool message' }), }, { type: 'text', text: 'This is a user message' }, ], }, ], system: undefined, }, betas: new Set(), }); }); it('should handle tool result with content parts', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'tool', content: [ { type: 'tool-result', toolName: 'image-generator', toolCallId: 'image-gen-1', output: { type: 'content', value: [ { type: 'text', text: 'Image generated successfully', }, { type: 'media', data: 'AAECAw==', mediaType: 'image/png', }, ], }, }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toMatchInlineSnapshot(` { "betas": Set {}, "prompt": { "messages": [ { "content": [ { "cache_control": undefined, "content": [ { "cache_control": undefined, "text": "Image generated successfully", "type": "text", }, { "cache_control": undefined, "source": { "data": "AAECAw==", "media_type": "image/png", "type": "base64", }, "type": "image", }, ], "is_error": undefined, "tool_use_id": "image-gen-1", "type": "tool_result", }, ], "role": "user", }, ], "system": undefined, }, } `); }); }); describe('assistant messages', () => { it('should remove trailing whitespace from last assistant message when there is no further user message', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'user content' }], }, { role: 'assistant', content: [{ type: 'text', text: 'assistant content ' }], }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [{ type: 'text', text: 'user content' }], }, { role: 'assistant', content: [{ type: 'text', text: 'assistant content' }], }, ], }, betas: new Set(), }); }); it('should remove trailing whitespace from last assistant message with multi-part content when there is no further user message', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'user content' }], }, { role: 'assistant', content: [ { type: 'text', text: 'assistant ' }, { type: 'text', text: 'content ' }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [{ type: 'text', text: 'user content' }], }, { role: 'assistant', content: [ { type: 'text', text: 'assistant ' }, { type: 'text', text: 'content' }, ], }, ], }, betas: new Set(), }); }); it('should keep trailing whitespace from assistant message when there is a further user message', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'user content' }], }, { role: 'assistant', content: [{ type: 'text', text: 'assistant content ' }], }, { role: 'user', content: [{ type: 'text', text: 'user content 2' }], }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [{ type: 'text', text: 'user content' }], }, { role: 'assistant', content: [{ type: 'text', text: 'assistant content ' }], }, { role: 'user', content: [{ type: 'text', text: 'user content 2' }], }, ], }, betas: new Set(), }); }); it('should combine multiple sequential assistant messages into a single message', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'Hi!' }] }, { role: 'assistant', content: [{ type: 'text', text: 'Hello' }] }, { role: 'assistant', content: [{ type: 'text', text: 'World' }] }, { role: 'assistant', content: [{ type: 'text', text: '!' }] }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [{ type: 'text', text: 'Hi!' }] }, { role: 'assistant', content: [ { type: 'text', text: 'Hello' }, { type: 'text', text: 'World' }, { type: 'text', text: '!' }, ], }, ], }, betas: new Set(), }); }); it('should convert assistant message reasoning parts with signature into thinking parts when sendReasoning is true', async () => { const warnings: LanguageModelV2CallWarning[] = []; const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'assistant', content: [ { type: 'reasoning', text: 'I need to count the number of "r"s in the word "strawberry".', providerOptions: { anthropic: { signature: 'test-signature', }, }, }, { type: 'text', text: 'The word "strawberry" has 2 "r"s.', }, ], }, ], sendReasoning: true, warnings, }); expect(result).toEqual({ prompt: { messages: [ { role: 'assistant', content: [ { type: 'thinking', thinking: 'I need to count the number of "r"s in the word "strawberry".', signature: 'test-signature', }, { type: 'text', text: 'The word "strawberry" has 2 "r"s.', }, ], }, ], }, betas: new Set(), }); expect(warnings).toEqual([]); }); it('should ignore reasoning parts without signature into thinking parts when sendReasoning is true', async () => { const warnings: LanguageModelV2CallWarning[] = []; const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'assistant', content: [ { type: 'reasoning', text: 'I need to count the number of "r"s in the word "strawberry".', }, { type: 'text', text: 'The word "strawberry" has 2 "r"s.', }, ], }, ], sendReasoning: true, warnings, }); expect(result).toMatchInlineSnapshot(` { "betas": Set {}, "prompt": { "messages": [ { "content": [ { "cache_control": undefined, "text": "The word "strawberry" has 2 "r"s.", "type": "text", }, ], "role": "assistant", }, ], "system": undefined, }, } `); expect(warnings).toMatchInlineSnapshot(` [ { "message": "unsupported reasoning metadata", "type": "other", }, ] `); }); it('should omit assistant message reasoning parts with signature when sendReasoning is false', async () => { const warnings: LanguageModelV2CallWarning[] = []; const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'assistant', content: [ { type: 'reasoning', text: 'I need to count the number of "r"s in the word "strawberry".', providerOptions: { anthropic: { signature: 'test-signature', }, }, }, { type: 'text', text: 'The word "strawberry" has 2 "r"s.', }, ], }, ], sendReasoning: false, warnings, }); expect(result).toEqual({ prompt: { messages: [ { role: 'assistant', content: [ { type: 'text', text: 'The word "strawberry" has 2 "r"s.', }, ], }, ], }, betas: new Set(), }); expect(warnings).toEqual([ { type: 'other', message: 'sending reasoning content is disabled for this model', }, ]); }); it('should omit reasoning parts without signature when sendReasoning is false', async () => { const warnings: LanguageModelV2CallWarning[] = []; const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'assistant', content: [ { type: 'reasoning', text: 'I need to count the number of "r"s in the word "strawberry".', }, { type: 'text', text: 'The word "strawberry" has 2 "r"s.', }, ], }, ], sendReasoning: false, warnings, }); expect(result).toEqual({ prompt: { messages: [ { role: 'assistant', content: [ { type: 'text', text: 'The word "strawberry" has 2 "r"s.', }, ], }, ], }, betas: new Set(), }); expect(warnings).toEqual([ { type: 'other', message: 'sending reasoning content is disabled for this model', }, ]); }); it('should convert anthropic web_search tool call and result parts', async () => { const warnings: LanguageModelV2CallWarning[] = []; const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'assistant', content: [ { input: { query: 'San Francisco major news events June 22 2025', }, providerExecuted: true, toolCallId: 'srvtoolu_011cNtbtzFARKPcAcp7w4nh9', toolName: 'web_search', type: 'tool-call', }, { output: { type: 'json', value: [ { url: 'https://patch.com/california/san-francisco/calendar', title: 'San Francisco Calendar', pageAge: null, encryptedContent: 'encrypted-content', type: 'event', }, ], }, toolCallId: 'srvtoolu_011cNtbtzFARKPcAcp7w4nh9', toolName: 'web_search', type: 'tool-result', }, ], }, ], sendReasoning: false, warnings, }); expect(result).toMatchInlineSnapshot(` { "betas": Set {}, "prompt": { "messages": [ { "content": [ { "cache_control": undefined, "id": "srvtoolu_011cNtbtzFARKPcAcp7w4nh9", "input": { "query": "San Francisco major news events June 22 2025", }, "name": "web_search", "type": "server_tool_use", }, { "cache_control": undefined, "content": [ { "encrypted_content": "encrypted-content", "page_age": null, "title": "San Francisco Calendar", "type": "event", "url": "https://patch.com/california/san-francisco/calendar", }, ], "tool_use_id": "srvtoolu_011cNtbtzFARKPcAcp7w4nh9", "type": "web_search_tool_result", }, ], "role": "assistant", }, ], "system": undefined, }, } `); expect(warnings).toMatchInlineSnapshot(`[]`); }); }); describe('cache control', () => { describe('system message', () => { it('should set cache_control on system message with message cache control', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'system', content: 'system message', providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } }, }, }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [], system: [ { type: 'text', text: 'system message', cache_control: { type: 'ephemeral' }, }, ], }, betas: new Set(), }); }); }); describe('user message', () => { it('should set cache_control on user message part with part cache control', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [ { type: 'text', text: 'test', providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [ { type: 'text', text: 'test', cache_control: { type: 'ephemeral' }, }, ], }, ], }, betas: new Set(), }); }); it('should set cache_control on last user message part with message cache control', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [ { type: 'text', text: 'part1' }, { type: 'text', text: 'part2' }, ], providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [ { type: 'text', text: 'part1', cache_control: undefined, }, { type: 'text', text: 'part2', cache_control: { type: 'ephemeral' }, }, ], }, ], }, betas: new Set(), }); }); }); describe('assistant message', () => { it('should set cache_control on assistant message text part with part cache control', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'user-content' }] }, { role: 'assistant', content: [ { type: 'text', text: 'test', providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [{ type: 'text', text: 'user-content' }] }, { role: 'assistant', content: [ { type: 'text', text: 'test', cache_control: { type: 'ephemeral' }, }, ], }, ], }, betas: new Set(), }); }); it('should set cache_control on assistant tool call part with part cache control', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'user-content' }] }, { role: 'assistant', content: [ { type: 'tool-call', toolCallId: 'test-id', toolName: 'test-tool', input: { some: 'arg' }, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [{ type: 'text', text: 'user-content' }] }, { role: 'assistant', content: [ { type: 'tool_use', name: 'test-tool', id: 'test-id', input: { some: 'arg' }, cache_control: { type: 'ephemeral' }, }, ], }, ], }, betas: new Set(), }); }); it('should set cache_control on last assistant message part with message cache control', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'user-content' }] }, { role: 'assistant', content: [ { type: 'text', text: 'part1' }, { type: 'text', text: 'part2' }, ], providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [{ type: 'text', text: 'user-content' }] }, { role: 'assistant', content: [ { type: 'text', text: 'part1', cache_control: undefined, }, { type: 'text', text: 'part2', cache_control: { type: 'ephemeral' }, }, ], }, ], }, betas: new Set(), }); }); }); describe('tool message', () => { it('should set cache_control on tool result message part with part cache control', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'tool', content: [ { type: 'tool-result', toolName: 'test', toolCallId: 'test', output: { type: 'json', value: { test: 'test' } }, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [ { type: 'tool_result', content: '{"test":"test"}', is_error: undefined, tool_use_id: 'test', cache_control: { type: 'ephemeral' }, }, ], }, ], }, betas: new Set(), }); }); it('should set cache_control on last tool result message part with message cache control', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'tool', content: [ { type: 'tool-result', toolName: 'test', toolCallId: 'part1', output: { type: 'json', value: { test: 'part1' } }, }, { type: 'tool-result', toolName: 'test', toolCallId: 'part2', output: { type: 'json', value: { test: 'part2' } }, }, ], providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' }, }, }, }, ], sendReasoning: true, warnings: [], }); expect(result).toEqual({ prompt: { messages: [ { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'part1', content: '{"test":"part1"}', is_error: undefined, cache_control: undefined, }, { type: 'tool_result', tool_use_id: 'part2', content: '{"test":"part2"}', is_error: undefined, cache_control: { type: 'ephemeral' }, }, ], }, ], }, betas: new Set(), }); }); }); }); describe('citations', () => { it('should not include citations by default', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [ { type: 'file', data: 'base64PDFdata', mediaType: 'application/pdf', }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toMatchInlineSnapshot(` { "betas": Set { "pdfs-2024-09-25", }, "prompt": { "messages": [ { "content": [ { "cache_control": undefined, "source": { "data": "base64PDFdata", "media_type": "application/pdf", "type": "base64", }, "title": undefined, "type": "document", }, ], "role": "user", }, ], "system": undefined, }, } `); }); it('should include citations when enabled on file part', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [ { type: 'file', data: 'base64PDFdata', mediaType: 'application/pdf', providerOptions: { anthropic: { citations: { enabled: true }, }, }, }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toMatchInlineSnapshot(` { "betas": Set { "pdfs-2024-09-25", }, "prompt": { "messages": [ { "content": [ { "cache_control": undefined, "citations": { "enabled": true, }, "source": { "data": "base64PDFdata", "media_type": "application/pdf", "type": "base64", }, "title": undefined, "type": "document", }, ], "role": "user", }, ], "system": undefined, }, } `); }); it('should include custom title and context when provided', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [ { type: 'file', data: 'base64PDFdata', mediaType: 'application/pdf', filename: 'original-name.pdf', providerOptions: { anthropic: { title: 'Custom Document Title', context: 'This is metadata about the document', citations: { enabled: true }, }, }, }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toMatchInlineSnapshot(` { "betas": Set { "pdfs-2024-09-25", }, "prompt": { "messages": [ { "content": [ { "cache_control": undefined, "citations": { "enabled": true, }, "context": "This is metadata about the document", "source": { "data": "base64PDFdata", "media_type": "application/pdf", "type": "base64", }, "title": "Custom Document Title", "type": "document", }, ], "role": "user", }, ], "system": undefined, }, } `); }); it('should handle multiple documents with consistent citation settings', async () => { const result = await convertToAnthropicMessagesPrompt({ prompt: [ { role: 'user', content: [ { type: 'file', data: 'base64PDFdata1', mediaType: 'application/pdf', filename: 'doc1.pdf', providerOptions: { anthropic: { citations: { enabled: true }, title: 'Custom Title 1', }, }, }, { type: 'file', data: 'base64PDFdata2', mediaType: 'application/pdf', filename: 'doc2.pdf', providerOptions: { anthropic: { citations: { enabled: true }, title: 'Custom Title 2', context: 'Additional context for document 2', }, }, }, { type: 'text', text: 'Analyze both documents', }, ], }, ], sendReasoning: true, warnings: [], }); expect(result).toMatchInlineSnapshot(` { "betas": Set { "pdfs-2024-09-25", }, "prompt": { "messages": [ { "content": [ { "cache_control": undefined, "citations": { "enabled": true, }, "source": { "data": "base64PDFdata1", "media_type": "application/pdf", "type": "base64", }, "title": "Custom Title 1", "type": "document", }, { "cache_control": undefined, "citations": { "enabled": true, }, "context": "Additional context for document 2", "source": { "data": "base64PDFdata2", "media_type": "application/pdf", "type": "base64", }, "title": "Custom Title 2", "type": "document", }, { "cache_control": undefined, "text": "Analyze both documents", "type": "text", }, ], "role": "user", }, ], "system": undefined, }, } `); }); }); --- File: /ai/packages/anthropic/src/convert-to-anthropic-messages-prompt.ts --- import { LanguageModelV2CallWarning, LanguageModelV2DataContent, LanguageModelV2Message, LanguageModelV2Prompt, SharedV2ProviderMetadata, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { convertToBase64, parseProviderOptions } from '@ai-sdk/provider-utils'; import { AnthropicAssistantMessage, AnthropicMessagesPrompt, AnthropicToolResultContent, AnthropicUserMessage, } from './anthropic-api-types'; import { anthropicReasoningMetadataSchema } from './anthropic-messages-language-model'; import { anthropicFilePartProviderOptions } from './anthropic-messages-options'; import { getCacheControl } from './get-cache-control'; import { webSearch_20250305OutputSchema } from './tool/web-search_20250305'; function convertToString(data: LanguageModelV2DataContent): string { if (typeof data === 'string') { return Buffer.from(data, 'base64').toString('utf-8'); } if (data instanceof Uint8Array) { return new TextDecoder().decode(data); } if (data instanceof URL) { throw new UnsupportedFunctionalityError({ functionality: 'URL-based text documents are not supported for citations', }); } throw new UnsupportedFunctionalityError({ functionality: `unsupported data type for text documents: ${typeof data}`, }); } export async function convertToAnthropicMessagesPrompt({ prompt, sendReasoning, warnings, }: { prompt: LanguageModelV2Prompt; sendReasoning: boolean; warnings: LanguageModelV2CallWarning[]; }): Promise<{ prompt: AnthropicMessagesPrompt; betas: Set<string>; }> { const betas = new Set<string>(); const blocks = groupIntoBlocks(prompt); let system: AnthropicMessagesPrompt['system'] = undefined; const messages: AnthropicMessagesPrompt['messages'] = []; async function shouldEnableCitations( providerMetadata: SharedV2ProviderMetadata | undefined, ): Promise<boolean> { const anthropicOptions = await parseProviderOptions({ provider: 'anthropic', providerOptions: providerMetadata, schema: anthropicFilePartProviderOptions, }); return anthropicOptions?.citations?.enabled ?? false; } async function getDocumentMetadata( providerMetadata: SharedV2ProviderMetadata | undefined, ): Promise<{ title?: string; context?: string }> { const anthropicOptions = await parseProviderOptions({ provider: 'anthropic', providerOptions: providerMetadata, schema: anthropicFilePartProviderOptions, }); return { title: anthropicOptions?.title, context: anthropicOptions?.context, }; } for (let i = 0; i < blocks.length; i++) { const block = blocks[i]; const isLastBlock = i === blocks.length - 1; const type = block.type; switch (type) { case 'system': { if (system != null) { throw new UnsupportedFunctionalityError({ functionality: 'Multiple system messages that are separated by user/assistant messages', }); } system = block.messages.map(({ content, providerOptions }) => ({ type: 'text', text: content, cache_control: getCacheControl(providerOptions), })); break; } case 'user': { // combines all user and tool messages in this block into a single message: const anthropicContent: AnthropicUserMessage['content'] = []; for (const message of block.messages) { const { role, content } = message; switch (role) { case 'user': { for (let j = 0; j < content.length; j++) { const part = content[j]; // cache control: first add cache control from part. // for the last part of a message, // check also if the message has cache control. const isLastPart = j === content.length - 1; const cacheControl = getCacheControl(part.providerOptions) ?? (isLastPart ? getCacheControl(message.providerOptions) : undefined); switch (part.type) { case 'text': { anthropicContent.push({ type: 'text', text: part.text, cache_control: cacheControl, }); break; } case 'file': { if (part.mediaType.startsWith('image/')) { anthropicContent.push({ type: 'image', source: part.data instanceof URL ? { type: 'url', url: part.data.toString(), } : { type: 'base64', media_type: part.mediaType === 'image/*' ? 'image/jpeg' : part.mediaType, data: convertToBase64(part.data), }, cache_control: cacheControl, }); } else if (part.mediaType === 'application/pdf') { betas.add('pdfs-2024-09-25'); const enableCitations = await shouldEnableCitations( part.providerOptions, ); const metadata = await getDocumentMetadata( part.providerOptions, ); anthropicContent.push({ type: 'document', source: part.data instanceof URL ? { type: 'url', url: part.data.toString(), } : { type: 'base64', media_type: 'application/pdf', data: convertToBase64(part.data), }, title: metadata.title ?? part.filename, ...(metadata.context && { context: metadata.context }), ...(enableCitations && { citations: { enabled: true }, }), cache_control: cacheControl, }); } else if (part.mediaType === 'text/plain') { const enableCitations = await shouldEnableCitations( part.providerOptions, ); const metadata = await getDocumentMetadata( part.providerOptions, ); anthropicContent.push({ type: 'document', source: part.data instanceof URL ? { type: 'url', url: part.data.toString(), } : { type: 'text', media_type: 'text/plain', data: convertToString(part.data), }, title: metadata.title ?? part.filename, ...(metadata.context && { context: metadata.context }), ...(enableCitations && { citations: { enabled: true }, }), cache_control: cacheControl, }); } else { throw new UnsupportedFunctionalityError({ functionality: `media type: ${part.mediaType}`, }); } break; } } } break; } case 'tool': { for (let i = 0; i < content.length; i++) { const part = content[i]; // cache control: first add cache control from part. // for the last part of a message, // check also if the message has cache control. const isLastPart = i === content.length - 1; const cacheControl = getCacheControl(part.providerOptions) ?? (isLastPart ? getCacheControl(message.providerOptions) : undefined); const output = part.output; let contentValue: AnthropicToolResultContent['content']; switch (output.type) { case 'content': contentValue = output.value.map(contentPart => { switch (contentPart.type) { case 'text': return { type: 'text', text: contentPart.text, cache_control: undefined, }; case 'media': { if (contentPart.mediaType.startsWith('image/')) { return { type: 'image', source: { type: 'base64', media_type: contentPart.mediaType, data: contentPart.data, }, cache_control: undefined, }; } throw new UnsupportedFunctionalityError({ functionality: `media type: ${contentPart.mediaType}`, }); } } }); break; case 'text': case 'error-text': contentValue = output.value; break; case 'json': case 'error-json': default: contentValue = JSON.stringify(output.value); break; } anthropicContent.push({ type: 'tool_result', tool_use_id: part.toolCallId, content: contentValue, is_error: output.type === 'error-text' || output.type === 'error-json' ? true : undefined, cache_control: cacheControl, }); } break; } default: { const _exhaustiveCheck: never = role; throw new Error(`Unsupported role: ${_exhaustiveCheck}`); } } } messages.push({ role: 'user', content: anthropicContent }); break; } case 'assistant': { // combines multiple assistant messages in this block into a single message: const anthropicContent: AnthropicAssistantMessage['content'] = []; for (let j = 0; j < block.messages.length; j++) { const message = block.messages[j]; const isLastMessage = j === block.messages.length - 1; const { content } = message; for (let k = 0; k < content.length; k++) { const part = content[k]; const isLastContentPart = k === content.length - 1; // cache control: first add cache control from part. // for the last part of a message, // check also if the message has cache control. const cacheControl = getCacheControl(part.providerOptions) ?? (isLastContentPart ? getCacheControl(message.providerOptions) : undefined); switch (part.type) { case 'text': { anthropicContent.push({ type: 'text', text: // trim the last text part if it's the last message in the block // because Anthropic does not allow trailing whitespace // in pre-filled assistant responses isLastBlock && isLastMessage && isLastContentPart ? part.text.trim() : part.text, cache_control: cacheControl, }); break; } case 'reasoning': { if (sendReasoning) { const reasoningMetadata = await parseProviderOptions({ provider: 'anthropic', providerOptions: part.providerOptions, schema: anthropicReasoningMetadataSchema, }); if (reasoningMetadata != null) { if (reasoningMetadata.signature != null) { anthropicContent.push({ type: 'thinking', thinking: part.text, signature: reasoningMetadata.signature, cache_control: cacheControl, }); } else if (reasoningMetadata.redactedData != null) { anthropicContent.push({ type: 'redacted_thinking', data: reasoningMetadata.redactedData, cache_control: cacheControl, }); } else { warnings.push({ type: 'other', message: 'unsupported reasoning metadata', }); } } else { warnings.push({ type: 'other', message: 'unsupported reasoning metadata', }); } } else { warnings.push({ type: 'other', message: 'sending reasoning content is disabled for this model', }); } break; } case 'tool-call': { if (part.providerExecuted) { if (part.toolName === 'web_search') { anthropicContent.push({ type: 'server_tool_use', id: part.toolCallId, name: 'web_search', input: part.input, cache_control: cacheControl, }); break; } warnings.push({ type: 'other', message: `provider executed tool call for tool ${part.toolName} is not supported`, }); break; } anthropicContent.push({ type: 'tool_use', id: part.toolCallId, name: part.toolName, input: part.input, cache_control: cacheControl, }); break; } case 'tool-result': { if (part.toolName === 'web_search') { const output = part.output; if (output.type !== 'json') { warnings.push({ type: 'other', message: `provider executed tool result output type ${output.type} for tool ${part.toolName} is not supported`, }); break; } const webSearchOutput = webSearch_20250305OutputSchema.parse( output.value, ); anthropicContent.push({ type: 'web_search_tool_result', tool_use_id: part.toolCallId, content: webSearchOutput.map(result => ({ url: result.url, title: result.title, page_age: result.pageAge, encrypted_content: result.encryptedContent, type: result.type, })), cache_control: cacheControl, }); break; } warnings.push({ type: 'other', message: `provider executed tool result for tool ${part.toolName} is not supported`, }); break; } } } } messages.push({ role: 'assistant', content: anthropicContent }); break; } default: { const _exhaustiveCheck: never = type; throw new Error(`content type: ${_exhaustiveCheck}`); } } } return { prompt: { system, messages }, betas, }; } type SystemBlock = { type: 'system'; messages: Array<LanguageModelV2Message & { role: 'system' }>; }; type AssistantBlock = { type: 'assistant'; messages: Array<LanguageModelV2Message & { role: 'assistant' }>; }; type UserBlock = { type: 'user'; messages: Array<LanguageModelV2Message & { role: 'user' | 'tool' }>; }; function groupIntoBlocks( prompt: LanguageModelV2Prompt, ): Array<SystemBlock | AssistantBlock | UserBlock> { const blocks: Array<SystemBlock | AssistantBlock | UserBlock> = []; let currentBlock: SystemBlock | AssistantBlock | UserBlock | undefined = undefined; for (const message of prompt) { const { role } = message; switch (role) { case 'system': { if (currentBlock?.type !== 'system') { currentBlock = { type: 'system', messages: [] }; blocks.push(currentBlock); } currentBlock.messages.push(message); break; } case 'assistant': { if (currentBlock?.type !== 'assistant') { currentBlock = { type: 'assistant', messages: [] }; blocks.push(currentBlock); } currentBlock.messages.push(message); break; } case 'user': { if (currentBlock?.type !== 'user') { currentBlock = { type: 'user', messages: [] }; blocks.push(currentBlock); } currentBlock.messages.push(message); break; } case 'tool': { if (currentBlock?.type !== 'user') { currentBlock = { type: 'user', messages: [] }; blocks.push(currentBlock); } currentBlock.messages.push(message); break; } default: { const _exhaustiveCheck: never = role; throw new Error(`Unsupported role: ${_exhaustiveCheck}`); } } } return blocks; } --- File: /ai/packages/anthropic/src/get-cache-control.ts --- import { SharedV2ProviderMetadata } from '@ai-sdk/provider'; import { AnthropicCacheControl } from './anthropic-api-types'; export function getCacheControl( providerMetadata: SharedV2ProviderMetadata | undefined, ): AnthropicCacheControl | undefined { const anthropic = providerMetadata?.anthropic; // allow both cacheControl and cache_control: const cacheControlValue = anthropic?.cacheControl ?? anthropic?.cache_control; // Pass through value assuming it is of the correct type. // The Anthropic API will validate the value. return cacheControlValue as AnthropicCacheControl | undefined; } --- File: /ai/packages/anthropic/src/index.ts --- export type { AnthropicProviderOptions } from './anthropic-messages-options'; export { anthropic, createAnthropic } from './anthropic-provider'; export type { AnthropicProvider, AnthropicProviderSettings, } from './anthropic-provider'; --- File: /ai/packages/anthropic/src/map-anthropic-stop-reason.ts --- import { LanguageModelV2FinishReason } from '@ai-sdk/provider'; export function mapAnthropicStopReason({ finishReason, isJsonResponseFromTool, }: { finishReason: string | null | undefined; isJsonResponseFromTool?: boolean; }): LanguageModelV2FinishReason { switch (finishReason) { case 'end_turn': case 'stop_sequence': return 'stop'; case 'tool_use': return isJsonResponseFromTool ? 'stop' : 'tool-calls'; case 'max_tokens': return 'length'; default: return 'unknown'; } } --- File: /ai/packages/anthropic/internal.d.ts --- export * from './dist/internal'; --- File: /ai/packages/anthropic/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, { entry: ['src/internal/index.ts'], outDir: 'dist/internal', format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/anthropic/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/anthropic/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/assemblyai/src/assemblyai-api-types.ts --- export type AssemblyAITranscriptionAPITypes = { /** * The URL of the audio or video file to transcribe. */ audio_url: string; /** * The point in time, in milliseconds, to stop transcribing in your media file */ audio_end_at?: number; /** * The point in time, in milliseconds, to begin transcribing in your media file */ audio_start_from?: number; /** * Enable Auto Chapters, can be true or false * @default false */ auto_chapters?: boolean; /** * Enable Key Phrases, either true or false * @default false */ auto_highlights?: boolean; /** * How much to boost specified words */ boost_param?: 'low' | 'default' | 'high'; /** * Enable Content Moderation, can be true or false * @default false */ content_safety?: boolean; /** * The confidence threshold for the Content Moderation model. Values must be between 25 and 100. * @default 50 */ content_safety_confidence?: number; /** * Customize how words are spelled and formatted using to and from values */ custom_spelling?: Array<{ /** * Words or phrases to replace */ from: string[]; /** * Word to replace with */ to: string; }>; /** * Transcribe Filler Words, like "umm", in your media file; can be true or false * @default false */ disfluencies?: boolean; /** * Enable Entity Detection, can be true or false * @default false */ entity_detection?: boolean; /** * Filter profanity from the transcribed text, can be true or false * @default false */ filter_profanity?: boolean; /** * Enable Text Formatting, can be true or false * @default true */ format_text?: boolean; /** * Enable Topic Detection, can be true or false * @default false */ iab_categories?: boolean; /** * The language of your audio file. Possible values are found in Supported Languages. * @default 'en_us' */ language_code?: | 'en' | 'en_au' | 'en_uk' | 'en_us' | 'es' | 'fr' | 'de' | 'it' | 'pt' | 'nl' | 'af' | 'sq' | 'am' | 'ar' | 'hy' | 'as' | 'az' | 'ba' | 'eu' | 'be' | 'bn' | 'bs' | 'br' | 'bg' | 'my' | 'ca' | 'zh' | 'hr' | 'cs' | 'da' | 'et' | 'fo' | 'fi' | 'gl' | 'ka' | 'el' | 'gu' | 'ht' | 'ha' | 'haw' | 'he' | 'hi' | 'hu' | 'is' | 'id' | 'ja' | 'jw' | 'kn' | 'kk' | 'km' | 'ko' | 'lo' | 'la' | 'lv' | 'ln' | 'lt' | 'lb' | 'mk' | 'mg' | 'ms' | 'ml' | 'mt' | 'mi' | 'mr' | 'mn' | 'ne' | 'no' | 'nn' | 'oc' | 'pa' | 'ps' | 'fa' | 'pl' | 'ro' | 'ru' | 'sa' | 'sr' | 'sn' | 'sd' | 'si' | 'sk' | 'sl' | 'so' | 'su' | 'sw' | 'sv' | 'tl' | 'tg' | 'ta' | 'tt' | 'te' | 'th' | 'bo' | 'tr' | 'tk' | 'uk' | 'ur' | 'uz' | 'vi' | 'cy' | 'yi' | 'yo'; /** * The confidence threshold for the automatically detected language. An error will be returned if the language confidence is below this threshold. * @default 0 */ language_confidence_threshold?: number; /** * Enable Automatic language detection, either true or false. * @default false */ language_detection?: boolean; /** * Enable Multichannel transcription, can be true or false. * @default false */ multichannel?: boolean; /** * Enable Automatic Punctuation, can be true or false * @default true */ punctuate?: boolean; /** * Redact PII from the transcribed text using the Redact PII model, can be true or false * @default false */ redact_pii?: boolean; /** * Generate a copy of the original media file with spoken PII "beeped" out, can be true or false. * @default false */ redact_pii_audio?: boolean; /** * Controls the filetype of the audio created by redact_pii_audio. Currently supports mp3 (default) and wav. */ redact_pii_audio_quality?: 'mp3' | 'wav'; /** * The list of PII Redaction policies to enable. */ redact_pii_policies?: Array< | 'account_number' | 'banking_information' | 'blood_type' | 'credit_card_cvv' | 'credit_card_expiration' | 'credit_card_number' | 'date' | 'date_interval' | 'date_of_birth' | 'drivers_license' | 'drug' | 'duration' | 'email_address' | 'event' | 'filename' | 'gender_sexuality' | 'healthcare_number' | 'injury' | 'ip_address' | 'language' | 'location' | 'marital_status' | 'medical_condition' | 'medical_process' | 'money_amount' | 'nationality' | 'number_sequence' | 'occupation' | 'organization' | 'passport_number' | 'password' | 'person_age' | 'person_name' | 'phone_number' | 'physical_attribute' | 'political_affiliation' | 'religion' | 'statistics' | 'time' | 'url' | 'us_social_security_number' | 'username' | 'vehicle_id' | 'zodiac_sign' >; /** * The replacement logic for detected PII, can be "entity_name" or "hash". */ redact_pii_sub?: 'entity_name' | 'hash'; /** * Enable Sentiment Analysis, can be true or false * @default false */ sentiment_analysis?: boolean; /** * Enable Speaker diarization, can be true or false * @default false */ speaker_labels?: boolean; /** * Tells the speaker label model how many speakers it should attempt to identify, up to 10. */ speakers_expected?: number; /** * The speech model to use for the transcription. */ speech_model?: 'best' | 'nano'; /** * Reject audio files that contain less than this fraction of speech. Valid values are in the range [0, 1] inclusive. */ speech_threshold?: number; /** * Enable Summarization, can be true or false * @default false */ summarization?: boolean; /** * The model to summarize the transcript */ summary_model?: 'informative' | 'conversational' | 'catchy'; /** * The type of summary */ summary_type?: | 'bullets' | 'bullets_verbose' | 'gist' | 'headline' | 'paragraph'; /** * The list of custom topics */ topics?: string[]; /** * The header name to be sent with the transcript completed or failed webhook requests */ webhook_auth_header_name?: string; /** * The header value to send back with the transcript completed or failed webhook requests for added security */ webhook_auth_header_value?: string; /** * The URL to which we send webhook requests. We sends two different types of webhook requests. * One request when a transcript is completed or failed, and one request when the redacted audio is ready if redact_pii_audio is enabled. */ webhook_url?: string; /** * The list of custom vocabulary to boost transcription probability for */ word_boost?: string[]; }; --- File: /ai/packages/assemblyai/src/assemblyai-config.ts --- import { FetchFunction } from '@ai-sdk/provider-utils'; export type AssemblyAIConfig = { provider: string; url: (options: { modelId: string; path: string }) => string; headers: () => Record<string, string | undefined>; fetch?: FetchFunction; generateId?: () => string; }; --- File: /ai/packages/assemblyai/src/assemblyai-error.test.ts --- import { safeParseJSON } from '@ai-sdk/provider-utils'; import { assemblyaiErrorDataSchema } from './assemblyai-error'; describe('assemblyaiErrorDataSchema', () => { it('should parse AssemblyAI resource exhausted error', async () => { const error = ` {"error":{"message":"{\\n \\"error\\": {\\n \\"code\\": 429,\\n \\"message\\": \\"Resource has been exhausted (e.g. check quota).\\",\\n \\"status\\": \\"RESOURCE_EXHAUSTED\\"\\n }\\n}\\n","code":429}} `; const result = await safeParseJSON({ text: error, schema: assemblyaiErrorDataSchema, }); expect(result).toStrictEqual({ success: true, value: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, rawValue: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, }); }); }); --- File: /ai/packages/assemblyai/src/assemblyai-error.ts --- import { z } from 'zod/v4'; import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; export const assemblyaiErrorDataSchema = z.object({ error: z.object({ message: z.string(), code: z.number(), }), }); export type AssemblyAIErrorData = z.infer<typeof assemblyaiErrorDataSchema>; export const assemblyaiFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: assemblyaiErrorDataSchema, errorToMessage: data => data.error.message, }); --- File: /ai/packages/assemblyai/src/assemblyai-provider.ts --- import { TranscriptionModelV2, ProviderV2, NoSuchModelError, } from '@ai-sdk/provider'; import { FetchFunction, loadApiKey } from '@ai-sdk/provider-utils'; import { AssemblyAITranscriptionModel } from './assemblyai-transcription-model'; import { AssemblyAITranscriptionModelId } from './assemblyai-transcription-settings'; export interface AssemblyAIProvider extends ProviderV2 { ( modelId: 'best', settings?: {}, ): { transcription: AssemblyAITranscriptionModel; }; /** Creates a model for transcription. */ transcription(modelId: AssemblyAITranscriptionModelId): TranscriptionModelV2; } export interface AssemblyAIProviderSettings { /** API key for authenticating requests. */ apiKey?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } /** Create an AssemblyAI provider instance. */ export function createAssemblyAI( options: AssemblyAIProviderSettings = {}, ): AssemblyAIProvider { const getHeaders = () => ({ authorization: loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'ASSEMBLYAI_API_KEY', description: 'AssemblyAI', }), ...options.headers, }); const createTranscriptionModel = (modelId: AssemblyAITranscriptionModelId) => new AssemblyAITranscriptionModel(modelId, { provider: `assemblyai.transcription`, url: ({ path }) => `https://api.assemblyai.com${path}`, headers: getHeaders, fetch: options.fetch, }); const provider = function (modelId: AssemblyAITranscriptionModelId) { return { transcription: createTranscriptionModel(modelId), }; }; provider.transcription = createTranscriptionModel; provider.transcriptionModel = createTranscriptionModel; provider.languageModel = () => { throw new NoSuchModelError({ modelId: 'unknown', modelType: 'languageModel', message: 'AssemblyAI does not provide language models', }); }; provider.textEmbeddingModel = () => { throw new NoSuchModelError({ modelId: 'unknown', modelType: 'textEmbeddingModel', message: 'AssemblyAI does not provide text embedding models', }); }; provider.imageModel = () => { throw new NoSuchModelError({ modelId: 'unknown', modelType: 'imageModel', message: 'AssemblyAI does not provide image models', }); }; return provider as AssemblyAIProvider; } /** Default AssemblyAI provider instance. */ export const assemblyai = createAssemblyAI(); --- File: /ai/packages/assemblyai/src/assemblyai-transcription-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { AssemblyAITranscriptionModel } from './assemblyai-transcription-model'; import { createAssemblyAI } from './assemblyai-provider'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; const audioData = await readFile(path.join(__dirname, 'transcript-test.mp3')); const provider = createAssemblyAI({ apiKey: 'test-api-key' }); const model = provider.transcription('best'); const server = createTestServer({ 'https://api.assemblyai.com/v2/transcript': {}, 'https://api.assemblyai.com/v2/upload': { response: { type: 'json-value', body: { id: '9ea68fd3-f953-42c1-9742-976c447fb463', upload_url: 'https://storage.assemblyai.com/mock-upload-url', }, }, }, }); describe('doGenerate', () => { function prepareJsonResponse({ headers, }: { headers?: Record<string, string>; } = {}) { server.urls['https://api.assemblyai.com/v2/transcript'].response = { type: 'json-value', headers, body: { id: '9ea68fd3-f953-42c1-9742-976c447fb463', audio_url: 'https://assembly.ai/test.mp3', status: 'completed', webhook_auth: true, auto_highlights: true, redact_pii: true, summarization: true, language_model: 'assemblyai_default', acoustic_model: 'assemblyai_default', language_code: 'en_us', language_detection: true, language_confidence_threshold: 0.7, language_confidence: 0.9959, speech_model: 'best', text: 'Hello, world!', words: [ { confidence: 0.97465, start: 250, end: 650, text: 'Hello,', channel: 'channel', speaker: 'speaker', }, { confidence: 0.99999, start: 730, end: 1022, text: 'world', channel: 'channel', speaker: 'speaker', }, ], utterances: [ { confidence: 0.9359033333333334, start: 250, end: 26950, text: 'Hello, world!', words: [ { confidence: 0.97503, start: 250, end: 650, text: 'Hello,', speaker: 'A', }, { confidence: 0.99999, start: 730, end: 1022, text: 'world', speaker: 'A', }, ], speaker: 'A', channel: 'channel', }, ], confidence: 0.9404651451800253, audio_duration: 281, punctuate: true, format_text: true, disfluencies: false, multichannel: false, audio_channels: 1, webhook_url: 'https://your-webhook-url.tld/path', webhook_status_code: 200, webhook_auth_header_name: 'webhook-secret', auto_highlights_result: { status: 'success', results: [ { count: 1, rank: 0.08, text: 'Hello, world!', timestamps: [ { start: 250, end: 26950, }, ], }, ], }, audio_start_from: 10, audio_end_at: 280, word_boost: ['hello', 'world'], boost_param: 'high', filter_profanity: true, redact_pii_audio: true, redact_pii_audio_quality: 'mp3', redact_pii_policies: [ 'us_social_security_number', 'credit_card_number', ], redact_pii_sub: 'hash', speaker_labels: true, speakers_expected: 2, content_safety: true, content_safety_labels: { status: 'success', results: [ { text: 'Hello, world!', labels: [ { label: 'disasters', confidence: 0.8142836093902588, severity: 0.4093044400215149, }, ], sentences_idx_start: 0, sentences_idx_end: 5, timestamp: { start: 250, end: 28840, }, }, ], summary: { disasters: 0.9940800441842205, health_issues: 0.9216489289040967, }, severity_score_summary: { disasters: { low: 0.5733263024656846, medium: 0.42667369753431533, high: 0, }, health_issues: { low: 0.22863814977924785, medium: 0.45014154926938227, high: 0.32122030095136983, }, }, }, iab_categories: true, iab_categories_result: { status: 'success', results: [ { text: 'Hello, world!', labels: [ { relevance: 0.988274097442627, label: 'Home&Garden>IndoorEnvironmentalQuality', }, { relevance: 0.5821335911750793, label: 'NewsAndPolitics>Weather', }, ], timestamp: { start: 250, end: 28840, }, }, ], summary: { 'NewsAndPolitics>Weather': 1, 'Home&Garden>IndoorEnvironmentalQuality': 0.9043831825256348, }, }, auto_chapters: true, chapters: [ { gist: 'Hello, world!', headline: 'Hello, world!', summary: 'Hello, world!', start: 250, end: 28840, }, { gist: 'Hello, world!', headline: 'Hello, world!', summary: 'Hello, world!', start: 29610, end: 280340, }, ], summary_type: 'bullets', summary_model: 'informative', summary: '- Hello, world!', topics: ['topics'], sentiment_analysis: true, entity_detection: true, entities: [ { entity_type: 'location', text: 'Canada', start: 2548, end: 3130, }, { entity_type: 'location', text: 'the US', start: 5498, end: 6382, }, ], speech_threshold: 0.5, error: 'error', dual_channel: false, speed_boost: true, custom_topics: true, }, }; } it('should pass the model', async () => { prepareJsonResponse(); await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(await server.calls[1].requestBodyJson).toMatchObject({ audio_url: 'https://storage.assemblyai.com/mock-upload-url', speech_model: 'best', }); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createAssemblyAI({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.transcription('best').doGenerate({ audio: audioData, mediaType: 'audio/wav', headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toMatchObject({ authorization: 'test-api-key', 'content-type': 'application/octet-stream', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should extract the transcription text', async () => { prepareJsonResponse(); const result = await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.text).toBe('Hello, world!'); }); it('should include response data with timestamp, modelId and headers', async () => { prepareJsonResponse({ headers: { 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); const testDate = new Date(0); const customModel = new AssemblyAITranscriptionModel('best', { provider: 'test-provider', url: ({ path }) => `https://api.assemblyai.com${path}`, headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.response).toMatchObject({ timestamp: testDate, modelId: 'best', headers: { 'content-type': 'application/json', 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); }); it('should use real date when no custom date provider is specified', async () => { prepareJsonResponse(); const testDate = new Date(0); const customModel = new AssemblyAITranscriptionModel('best', { provider: 'test-provider', url: ({ path }) => `https://api.assemblyai.com${path}`, headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.response.timestamp.getTime()).toEqual(testDate.getTime()); expect(result.response.modelId).toBe('best'); }); }); --- File: /ai/packages/assemblyai/src/assemblyai-transcription-model.ts --- import { TranscriptionModelV2, TranscriptionModelV2CallWarning, } from '@ai-sdk/provider'; import { combineHeaders, createJsonResponseHandler, parseProviderOptions, postJsonToApi, postToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { AssemblyAIConfig } from './assemblyai-config'; import { assemblyaiFailedResponseHandler } from './assemblyai-error'; import { AssemblyAITranscriptionModelId } from './assemblyai-transcription-settings'; import { AssemblyAITranscriptionAPITypes } from './assemblyai-api-types'; // https://www.assemblyai.com/docs/api-reference/transcripts/submit const assemblyaiProviderOptionsSchema = z.object({ /** * End time of the audio in milliseconds. */ audioEndAt: z.number().int().nullish(), /** * Start time of the audio in milliseconds. */ audioStartFrom: z.number().int().nullish(), /** * Whether to automatically generate chapters for the transcription. */ autoChapters: z.boolean().nullish(), /** * Whether to automatically generate highlights for the transcription. */ autoHighlights: z.boolean().nullish(), /** * Boost parameter for the transcription. * Allowed values: 'low', 'default', 'high'. */ boostParam: z.string().nullish(), /** * Whether to enable content safety filtering. */ contentSafety: z.boolean().nullish(), /** * Confidence threshold for content safety filtering (25-100). */ contentSafetyConfidence: z.number().int().min(25).max(100).nullish(), /** * Custom spelling rules for the transcription. */ customSpelling: z .array( z.object({ from: z.array(z.string()), to: z.string(), }), ) .nullish(), /** * Whether to include filler words (um, uh, etc.) in the transcription. */ disfluencies: z.boolean().nullish(), /** * Whether to enable entity detection. */ entityDetection: z.boolean().nullish(), /** * Whether to filter profanity from the transcription. */ filterProfanity: z.boolean().nullish(), /** * Whether to format text with punctuation and capitalization. */ formatText: z.boolean().nullish(), /** * Whether to enable IAB categories detection. */ iabCategories: z.boolean().nullish(), /** * Language code for the transcription. */ languageCode: z.union([z.literal('en'), z.string()]).nullish(), /** * Confidence threshold for language detection. */ languageConfidenceThreshold: z.number().nullish(), /** * Whether to enable language detection. */ languageDetection: z.boolean().nullish(), /** * Whether to process audio as multichannel. */ multichannel: z.boolean().nullish(), /** * Whether to add punctuation to the transcription. */ punctuate: z.boolean().nullish(), /** * Whether to redact personally identifiable information (PII). */ redactPii: z.boolean().nullish(), /** * Whether to redact PII in the audio file. */ redactPiiAudio: z.boolean().nullish(), /** * Audio format for PII redaction. */ redactPiiAudioQuality: z.string().nullish(), /** * List of PII types to redact. */ redactPiiPolicies: z.array(z.string()).nullish(), /** * Substitution method for redacted PII. */ redactPiiSub: z.string().nullish(), /** * Whether to enable sentiment analysis. */ sentimentAnalysis: z.boolean().nullish(), /** * Whether to identify different speakers in the audio. */ speakerLabels: z.boolean().nullish(), /** * Number of speakers expected in the audio. */ speakersExpected: z.number().int().nullish(), /** * Threshold for speech detection (0-1). */ speechThreshold: z.number().min(0).max(1).nullish(), /** * Whether to generate a summary of the transcription. */ summarization: z.boolean().nullish(), /** * Model to use for summarization. */ summaryModel: z.string().nullish(), /** * Type of summary to generate. */ summaryType: z.string().nullish(), /** * List of topics to identify in the transcription. */ topics: z.array(z.string()).nullish(), /** * Name of the authentication header for webhook requests. */ webhookAuthHeaderName: z.string().nullish(), /** * Value of the authentication header for webhook requests. */ webhookAuthHeaderValue: z.string().nullish(), /** * URL to send webhook notifications to. */ webhookUrl: z.string().nullish(), /** * List of words to boost recognition for. */ wordBoost: z.array(z.string()).nullish(), }); export type AssemblyAITranscriptionCallOptions = z.infer< typeof assemblyaiProviderOptionsSchema >; interface AssemblyAITranscriptionModelConfig extends AssemblyAIConfig { _internal?: { currentDate?: () => Date; }; } export class AssemblyAITranscriptionModel implements TranscriptionModelV2 { readonly specificationVersion = 'v2'; get provider(): string { return this.config.provider; } constructor( readonly modelId: AssemblyAITranscriptionModelId, private readonly config: AssemblyAITranscriptionModelConfig, ) {} private async getArgs({ providerOptions, }: Parameters<TranscriptionModelV2['doGenerate']>[0]) { const warnings: TranscriptionModelV2CallWarning[] = []; // Parse provider options const assemblyaiOptions = await parseProviderOptions({ provider: 'assemblyai', providerOptions, schema: assemblyaiProviderOptionsSchema, }); const body: Omit<AssemblyAITranscriptionAPITypes, 'audio_url'> = { speech_model: this.modelId, }; // Add provider-specific options if (assemblyaiOptions) { body.audio_end_at = assemblyaiOptions.audioEndAt ?? undefined; body.audio_start_from = assemblyaiOptions.audioStartFrom ?? undefined; body.auto_chapters = assemblyaiOptions.autoChapters ?? undefined; body.auto_highlights = assemblyaiOptions.autoHighlights ?? undefined; body.boost_param = (assemblyaiOptions.boostParam as never) ?? undefined; body.content_safety = assemblyaiOptions.contentSafety ?? undefined; body.content_safety_confidence = assemblyaiOptions.contentSafetyConfidence ?? undefined; body.custom_spelling = (assemblyaiOptions.customSpelling as never) ?? undefined; body.disfluencies = assemblyaiOptions.disfluencies ?? undefined; body.entity_detection = assemblyaiOptions.entityDetection ?? undefined; body.filter_profanity = assemblyaiOptions.filterProfanity ?? undefined; body.format_text = assemblyaiOptions.formatText ?? undefined; body.iab_categories = assemblyaiOptions.iabCategories ?? undefined; body.language_code = (assemblyaiOptions.languageCode as never) ?? undefined; body.language_confidence_threshold = assemblyaiOptions.languageConfidenceThreshold ?? undefined; body.language_detection = assemblyaiOptions.languageDetection ?? undefined; body.multichannel = assemblyaiOptions.multichannel ?? undefined; body.punctuate = assemblyaiOptions.punctuate ?? undefined; body.redact_pii = assemblyaiOptions.redactPii ?? undefined; body.redact_pii_audio = assemblyaiOptions.redactPiiAudio ?? undefined; body.redact_pii_audio_quality = (assemblyaiOptions.redactPiiAudioQuality as never) ?? undefined; body.redact_pii_policies = (assemblyaiOptions.redactPiiPolicies as never) ?? undefined; body.redact_pii_sub = (assemblyaiOptions.redactPiiSub as never) ?? undefined; body.sentiment_analysis = assemblyaiOptions.sentimentAnalysis ?? undefined; body.speaker_labels = assemblyaiOptions.speakerLabels ?? undefined; body.speakers_expected = assemblyaiOptions.speakersExpected ?? undefined; body.speech_threshold = assemblyaiOptions.speechThreshold ?? undefined; body.summarization = assemblyaiOptions.summarization ?? undefined; body.summary_model = (assemblyaiOptions.summaryModel as never) ?? undefined; body.summary_type = (assemblyaiOptions.summaryType as never) ?? undefined; body.topics = assemblyaiOptions.topics ?? undefined; body.webhook_auth_header_name = assemblyaiOptions.webhookAuthHeaderName ?? undefined; body.webhook_auth_header_value = assemblyaiOptions.webhookAuthHeaderValue ?? undefined; body.webhook_url = assemblyaiOptions.webhookUrl ?? undefined; body.word_boost = assemblyaiOptions.wordBoost ?? undefined; } return { body, warnings, }; } async doGenerate( options: Parameters<TranscriptionModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<TranscriptionModelV2['doGenerate']>>> { const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { value: uploadResponse } = await postToApi({ url: this.config.url({ path: '/v2/upload', modelId: '', }), headers: { 'Content-Type': 'application/octet-stream', ...combineHeaders(this.config.headers(), options.headers), }, body: { content: options.audio, values: options.audio, }, failedResponseHandler: assemblyaiFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( assemblyaiUploadResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const { body, warnings } = await this.getArgs(options); const { value: response, responseHeaders, rawValue: rawResponse, } = await postJsonToApi({ url: this.config.url({ path: '/v2/transcript', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body: { ...body, audio_url: uploadResponse.upload_url, }, failedResponseHandler: assemblyaiFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( assemblyaiTranscriptionResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); return { text: response.text ?? '', segments: response.words?.map(word => ({ text: word.text, startSecond: word.start, endSecond: word.end, })) ?? [], language: response.language_code ?? undefined, durationInSeconds: response.audio_duration ?? response.words?.at(-1)?.end ?? undefined, warnings, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, body: rawResponse, }, }; } } const assemblyaiUploadResponseSchema = z.object({ upload_url: z.string(), }); const assemblyaiTranscriptionResponseSchema = z.object({ text: z.string().nullish(), language_code: z.string().nullish(), words: z .array( z.object({ start: z.number(), end: z.number(), text: z.string(), }), ) .nullish(), audio_duration: z.number().nullish(), }); --- File: /ai/packages/assemblyai/src/assemblyai-transcription-settings.ts --- export type AssemblyAITranscriptionModelId = 'best' | 'nano'; --- File: /ai/packages/assemblyai/src/index.ts --- export { createAssemblyAI, assemblyai } from './assemblyai-provider'; export type { AssemblyAIProvider, AssemblyAIProviderSettings, } from './assemblyai-provider'; --- File: /ai/packages/assemblyai/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/assemblyai/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/assemblyai/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/azure/src/azure-openai-provider.test.ts --- import { EmbeddingModelV2Embedding, LanguageModelV2Prompt, } from '@ai-sdk/provider'; import { createTestServer } from '@ai-sdk/provider-utils/test'; import { createAzure } from './azure-openai-provider'; const TEST_PROMPT: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; const provider = createAzure({ resourceName: 'test-resource', apiKey: 'test-api-key', }); const providerApiVersionChanged = createAzure({ resourceName: 'test-resource', apiKey: 'test-api-key', apiVersion: '2024-08-01-preview', }); const server = createTestServer({ 'https://test-resource.openai.azure.com/openai/v1/chat/completions': {}, 'https://test-resource.openai.azure.com/openai/v1/completions': {}, 'https://test-resource.openai.azure.com/openai/v1/embeddings': {}, 'https://test-resource.openai.azure.com/openai/v1/images/generations': {}, 'https://test-resource.openai.azure.com/openai/v1/responses': {}, 'https://test-resource.openai.azure.com/openai/v1/audio/transcriptions': {}, 'https://test-resource.openai.azure.com/openai/v1/audio/speech': {}, }); describe('chat', () => { describe('doGenerate', () => { function prepareJsonResponse({ content = '' }: { content?: string } = {}) { server.urls[ 'https://test-resource.openai.azure.com/openai/v1/chat/completions' ].response = { type: 'json-value', body: { id: 'chatcmpl-95ZTZkhr0mHNKqerQfiwkuox3PHAd', object: 'chat.completion', created: 1711115037, model: 'gpt-3.5-turbo-0125', choices: [ { index: 0, message: { role: 'assistant', content, }, finish_reason: 'stop', }, ], usage: { prompt_tokens: 4, total_tokens: 34, completion_tokens: 30, }, system_fingerprint: 'fp_3bc1b5746c', }, }; } it('should set the correct default api version', async () => { prepareJsonResponse(); await provider('test-deployment').doGenerate({ prompt: TEST_PROMPT, }); expect( server.calls[0].requestUrlSearchParams.get('api-version'), ).toStrictEqual('preview'); }); it('should set the correct modified api version', async () => { prepareJsonResponse(); await providerApiVersionChanged('test-deployment').doGenerate({ prompt: TEST_PROMPT, }); expect( server.calls[0].requestUrlSearchParams.get('api-version'), ).toStrictEqual('2024-08-01-preview'); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createAzure({ resourceName: 'test-resource', apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider('test-deployment').doGenerate({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'api-key': 'test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should use the baseURL correctly', async () => { prepareJsonResponse(); const provider = createAzure({ baseURL: 'https://test-resource.openai.azure.com/openai', apiKey: 'test-api-key', }); await provider('test-deployment').doGenerate({ prompt: TEST_PROMPT, }); expect(server.calls[0].requestUrl).toStrictEqual( 'https://test-resource.openai.azure.com/openai/v1/chat/completions?api-version=preview', ); }); }); }); describe('completion', () => { describe('doGenerate', () => { function prepareJsonCompletionResponse({ content = '', usage = { prompt_tokens: 4, total_tokens: 34, completion_tokens: 30, }, finish_reason = 'stop', }: { content?: string; usage?: { prompt_tokens: number; total_tokens: number; completion_tokens: number; }; finish_reason?: string; }) { server.urls[ 'https://test-resource.openai.azure.com/openai/v1/completions' ].response = { type: 'json-value', body: { id: 'cmpl-96cAM1v77r4jXa4qb2NSmRREV5oWB', object: 'text_completion', created: 1711363706, model: 'gpt-35-turbo-instruct', choices: [ { text: content, index: 0, finish_reason, }, ], usage, }, }; } it('should set the correct api version', async () => { prepareJsonCompletionResponse({ content: 'Hello World!' }); await provider.completion('gpt-35-turbo-instruct').doGenerate({ prompt: TEST_PROMPT, }); expect( server.calls[0].requestUrlSearchParams.get('api-version'), ).toStrictEqual('preview'); }); it('should pass headers', async () => { prepareJsonCompletionResponse({ content: 'Hello World!' }); const provider = createAzure({ resourceName: 'test-resource', apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.completion('gpt-35-turbo-instruct').doGenerate({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'api-key': 'test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); }); }); describe('transcription', () => { describe('doGenerate', () => { it('should use correct URL format', async () => { server.urls[ 'https://test-resource.openai.azure.com/openai/v1/audio/transcriptions' ].response = { type: 'json-value', body: { text: 'Hello, world!', segments: [], language: 'en', duration: 5.0, }, }; await provider.transcription('whisper-1').doGenerate({ audio: new Uint8Array(), mediaType: 'audio/wav', }); expect(server.calls[0].requestUrl).toStrictEqual( 'https://test-resource.openai.azure.com/openai/v1/audio/transcriptions?api-version=preview', ); }); }); }); describe('speech', () => { describe('doGenerate', () => { it('should use correct URL format', async () => { server.urls[ 'https://test-resource.openai.azure.com/openai/v1/audio/speech' ].response = { type: 'json-value', body: new Uint8Array([1, 2, 3]), }; await provider.speech('tts-1').doGenerate({ text: 'Hello, world!', }); expect(server.calls[0].requestUrl).toStrictEqual( 'https://test-resource.openai.azure.com/openai/v1/audio/speech?api-version=preview', ); }); }); }); describe('embedding', () => { const dummyEmbeddings = [ [0.1, 0.2, 0.3, 0.4, 0.5], [0.6, 0.7, 0.8, 0.9, 1.0], ]; const testValues = ['sunny day at the beach', 'rainy day in the city']; describe('doEmbed', () => { const model = provider.embedding('my-embedding'); function prepareJsonResponse({ embeddings = dummyEmbeddings, }: { embeddings?: EmbeddingModelV2Embedding[]; } = {}) { server.urls[ 'https://test-resource.openai.azure.com/openai/v1/embeddings' ].response = { type: 'json-value', body: { object: 'list', data: embeddings.map((embedding, i) => ({ object: 'embedding', index: i, embedding, })), model: 'my-embedding', usage: { prompt_tokens: 8, total_tokens: 8 }, }, }; } it('should set the correct api version', async () => { prepareJsonResponse(); await model.doEmbed({ values: testValues, }); expect( server.calls[0].requestUrlSearchParams.get('api-version'), ).toStrictEqual('preview'); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createAzure({ resourceName: 'test-resource', apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.embedding('my-embedding').doEmbed({ values: testValues, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'api-key': 'test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); }); }); describe('image', () => { const prompt = 'A cute baby sea otter'; describe('doGenerate', () => { function prepareJsonResponse() { server.urls[ 'https://test-resource.openai.azure.com/openai/v1/images/generations' ].response = { type: 'json-value', body: { created: 1733837122, data: [ { revised_prompt: 'A charming visual illustration of a baby sea otter swimming joyously.', b64_json: 'base64-image-1', }, { b64_json: 'base64-image-2', }, ], }, }; } it('should set the correct default api version', async () => { prepareJsonResponse(); await provider.imageModel('dalle-deployment').doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect( server.calls[0].requestUrlSearchParams.get('api-version'), ).toStrictEqual('preview'); }); it('should set the correct modified api version', async () => { prepareJsonResponse(); await providerApiVersionChanged .imageModel('dalle-deployment') .doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect( server.calls[0].requestUrlSearchParams.get('api-version'), ).toStrictEqual('2024-08-01-preview'); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createAzure({ resourceName: 'test-resource', apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.imageModel('dalle-deployment').doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: undefined, providerOptions: {}, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'api-key': 'test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should use the baseURL correctly', async () => { prepareJsonResponse(); const provider = createAzure({ baseURL: 'https://test-resource.openai.azure.com/openai', apiKey: 'test-api-key', }); await provider.imageModel('dalle-deployment').doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(server.calls[0].requestUrl).toStrictEqual( 'https://test-resource.openai.azure.com/openai/v1/images/generations?api-version=preview', ); }); it('should extract the generated images', async () => { prepareJsonResponse(); const result = await provider.imageModel('dalle-deployment').doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.images).toStrictEqual(['base64-image-1', 'base64-image-2']); }); it('should send the correct request body', async () => { prepareJsonResponse(); await provider.imageModel('dalle-deployment').doGenerate({ prompt, n: 2, size: '1024x1024', aspectRatio: undefined, seed: undefined, providerOptions: { openai: { style: 'natural' } }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'dalle-deployment', prompt, n: 2, size: '1024x1024', style: 'natural', response_format: 'b64_json', }); }); }); describe('imageModel method', () => { it('should create the same model as image method', () => { const imageModel = provider.imageModel('dalle-deployment'); const imageModelAlias = provider.imageModel('dalle-deployment'); expect(imageModel.provider).toBe(imageModelAlias.provider); expect(imageModel.modelId).toBe(imageModelAlias.modelId); }); }); }); describe('responses', () => { describe('doGenerate', () => { function prepareJsonResponse({ content = '', usage = { input_tokens: 4, output_tokens: 30, total_tokens: 34, }, } = {}) { server.urls[ 'https://test-resource.openai.azure.com/openai/v1/responses' ].response = { type: 'json-value', body: { id: 'resp_67c97c0203188190a025beb4a75242bc', object: 'response', created_at: 1741257730, status: 'completed', model: 'test-deployment', output: [ { id: 'msg_67c97c02656c81908e080dfdf4a03cd1', type: 'message', status: 'completed', role: 'assistant', content: [ { type: 'output_text', text: content, annotations: [], }, ], }, ], usage, incomplete_details: null, }, }; } it('should set the correct api version', async () => { prepareJsonResponse(); await provider.responses('test-deployment').doGenerate({ prompt: TEST_PROMPT, }); expect( server.calls[0].requestUrlSearchParams.get('api-version'), ).toStrictEqual('preview'); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createAzure({ resourceName: 'test-resource', apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.responses('test-deployment').doGenerate({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'api-key': 'test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should use the baseURL correctly', async () => { prepareJsonResponse(); const provider = createAzure({ baseURL: 'https://test-resource.openai.azure.com/openai', apiKey: 'test-api-key', }); await provider.responses('test-deployment').doGenerate({ prompt: TEST_PROMPT, }); expect(server.calls[0].requestUrl).toStrictEqual( 'https://test-resource.openai.azure.com/openai/v1/responses?api-version=preview', ); }); }); }); --- File: /ai/packages/azure/src/azure-openai-provider.ts --- import { OpenAIChatLanguageModel, OpenAICompletionLanguageModel, OpenAIEmbeddingModel, OpenAIImageModel, OpenAIResponsesLanguageModel, OpenAISpeechModel, OpenAITranscriptionModel, } from '@ai-sdk/openai/internal'; import { EmbeddingModelV2, LanguageModelV2, ProviderV2, ImageModelV2, SpeechModelV2, TranscriptionModelV2, } from '@ai-sdk/provider'; import { FetchFunction, loadApiKey, loadSetting } from '@ai-sdk/provider-utils'; export interface AzureOpenAIProvider extends ProviderV2 { (deploymentId: string): LanguageModelV2; /** Creates an Azure OpenAI chat model for text generation. */ languageModel(deploymentId: string): LanguageModelV2; /** Creates an Azure OpenAI chat model for text generation. */ chat(deploymentId: string): LanguageModelV2; /** Creates an Azure OpenAI responses API model for text generation. */ responses(deploymentId: string): LanguageModelV2; /** Creates an Azure OpenAI completion model for text generation. */ completion(deploymentId: string): LanguageModelV2; /** @deprecated Use `textEmbedding` instead. */ embedding(deploymentId: string): EmbeddingModelV2<string>; /** * Creates an Azure OpenAI DALL-E model for image generation. */ image(deploymentId: string): ImageModelV2; /** * Creates an Azure OpenAI DALL-E model for image generation. */ imageModel(deploymentId: string): ImageModelV2; textEmbedding(deploymentId: string): EmbeddingModelV2<string>; /** Creates an Azure OpenAI model for text embeddings. */ textEmbeddingModel(deploymentId: string): EmbeddingModelV2<string>; /** * Creates an Azure OpenAI model for audio transcription. */ transcription(deploymentId: string): TranscriptionModelV2; /** * Creates an Azure OpenAI model for speech generation. */ speech(deploymentId: string): SpeechModelV2; } export interface AzureOpenAIProviderSettings { /** Name of the Azure OpenAI resource. Either this or `baseURL` can be used. The resource name is used in the assembled URL: `https://{resourceName}.openai.azure.com/openai/v1{path}`. */ resourceName?: string; /** Use a different URL prefix for API calls, e.g. to use proxy servers. Either this or `resourceName` can be used. When a baseURL is provided, the resourceName is ignored. With a baseURL, the resolved URL is `{baseURL}/v1{path}`. */ baseURL?: string; /** API key for authenticating requests. */ apiKey?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; /** Custom api version to use. Defaults to `preview`. */ apiVersion?: string; } /** Create an Azure OpenAI provider instance. */ export function createAzure( options: AzureOpenAIProviderSettings = {}, ): AzureOpenAIProvider { const getHeaders = () => ({ 'api-key': loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'AZURE_API_KEY', description: 'Azure OpenAI', }), ...options.headers, }); const getResourceName = () => loadSetting({ settingValue: options.resourceName, settingName: 'resourceName', environmentVariableName: 'AZURE_RESOURCE_NAME', description: 'Azure OpenAI resource name', }); const apiVersion = options.apiVersion ?? 'preview'; const url = ({ path, modelId }: { path: string; modelId: string }) => { const baseUrlPrefix = options.baseURL ?? `https://${getResourceName()}.openai.azure.com/openai`; // Use v1 API format - no deployment ID in URL const fullUrl = new URL(`${baseUrlPrefix}/v1${path}`); fullUrl.searchParams.set('api-version', apiVersion); return fullUrl.toString(); }; const createChatModel = (deploymentName: string) => new OpenAIChatLanguageModel(deploymentName, { provider: 'azure.chat', url, headers: getHeaders, fetch: options.fetch, }); const createCompletionModel = (modelId: string) => new OpenAICompletionLanguageModel(modelId, { provider: 'azure.completion', url, headers: getHeaders, fetch: options.fetch, }); const createEmbeddingModel = (modelId: string) => new OpenAIEmbeddingModel(modelId, { provider: 'azure.embeddings', headers: getHeaders, url, fetch: options.fetch, }); const createResponsesModel = (modelId: string) => new OpenAIResponsesLanguageModel(modelId, { provider: 'azure.responses', url, headers: getHeaders, fetch: options.fetch, }); const createImageModel = (modelId: string) => new OpenAIImageModel(modelId, { provider: 'azure.image', url, headers: getHeaders, fetch: options.fetch, }); const createTranscriptionModel = (modelId: string) => new OpenAITranscriptionModel(modelId, { provider: 'azure.transcription', url, headers: getHeaders, fetch: options.fetch, }); const createSpeechModel = (modelId: string) => new OpenAISpeechModel(modelId, { provider: 'azure.speech', url, headers: getHeaders, fetch: options.fetch, }); const provider = function (deploymentId: string) { if (new.target) { throw new Error( 'The Azure OpenAI model function cannot be called with the new keyword.', ); } return createChatModel(deploymentId); }; provider.languageModel = createChatModel; provider.chat = createChatModel; provider.completion = createCompletionModel; provider.embedding = createEmbeddingModel; provider.image = createImageModel; provider.imageModel = createImageModel; provider.textEmbedding = createEmbeddingModel; provider.textEmbeddingModel = createEmbeddingModel; provider.responses = createResponsesModel; provider.transcription = createTranscriptionModel; provider.speech = createSpeechModel; return provider; } /** Default Azure OpenAI provider instance. */ export const azure = createAzure(); --- File: /ai/packages/azure/src/index.ts --- export { azure, createAzure } from './azure-openai-provider'; export type { AzureOpenAIProvider, AzureOpenAIProviderSettings, } from './azure-openai-provider'; --- File: /ai/packages/azure/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/azure/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/azure/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/cerebras/src/cerebras-chat-options.ts --- // https://inference-docs.cerebras.ai/introduction export type CerebrasChatModelId = | 'llama3.1-8b' | 'llama3.1-70b' | 'llama-3.3-70b' | (string & {}); --- File: /ai/packages/cerebras/src/cerebras-provider.test.ts --- import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; import { createCerebras } from './cerebras-provider'; import { loadApiKey } from '@ai-sdk/provider-utils'; import { OpenAICompatibleChatLanguageModel } from '@ai-sdk/openai-compatible'; // Add type assertion for the mocked class const OpenAICompatibleChatLanguageModelMock = OpenAICompatibleChatLanguageModel as unknown as Mock; vi.mock('@ai-sdk/openai-compatible', () => ({ OpenAICompatibleChatLanguageModel: vi.fn(), })); vi.mock('@ai-sdk/provider-utils', () => ({ loadApiKey: vi.fn().mockReturnValue('mock-api-key'), withoutTrailingSlash: vi.fn(url => url), })); describe('CerebrasProvider', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('createCerebras', () => { it('should create a CerebrasProvider instance with default options', () => { const provider = createCerebras(); const model = provider('model-id'); const constructorCall = OpenAICompatibleChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; config.headers(); expect(loadApiKey).toHaveBeenCalledWith({ apiKey: undefined, environmentVariableName: 'CEREBRAS_API_KEY', description: 'Cerebras API key', }); }); it('should create a CerebrasProvider instance with custom options', () => { const options = { apiKey: 'custom-key', baseURL: 'https://custom.url', headers: { 'Custom-Header': 'value' }, }; const provider = createCerebras(options); provider('model-id'); const constructorCall = OpenAICompatibleChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; config.headers(); expect(loadApiKey).toHaveBeenCalledWith({ apiKey: 'custom-key', environmentVariableName: 'CEREBRAS_API_KEY', description: 'Cerebras API key', }); }); it('should return a chat model when called as a function', () => { const provider = createCerebras(); const modelId = 'foo-model-id'; const model = provider(modelId); expect(model).toBeInstanceOf(OpenAICompatibleChatLanguageModel); }); }); describe('languageModel', () => { it('should construct a language model with correct configuration', () => { const provider = createCerebras(); const modelId = 'foo-model-id'; const model = provider.languageModel(modelId); expect(model).toBeInstanceOf(OpenAICompatibleChatLanguageModel); }); }); describe('textEmbeddingModel', () => { it('should throw NoSuchModelError when attempting to create embedding model', () => { const provider = createCerebras(); expect(() => provider.textEmbeddingModel('any-model')).toThrow( 'No such textEmbeddingModel: any-model', ); }); }); describe('chat', () => { it('should construct a chat model with correct configuration', () => { const provider = createCerebras(); const modelId = 'foo-model-id'; const model = provider.chat(modelId); expect(model).toBeInstanceOf(OpenAICompatibleChatLanguageModel); }); }); }); --- File: /ai/packages/cerebras/src/cerebras-provider.ts --- import { OpenAICompatibleChatLanguageModel } from '@ai-sdk/openai-compatible'; import { LanguageModelV2, NoSuchModelError, ProviderV2, } from '@ai-sdk/provider'; import { FetchFunction, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { CerebrasChatModelId } from './cerebras-chat-options'; import { z } from 'zod/v4'; import { ProviderErrorStructure } from '@ai-sdk/openai-compatible'; // Add error schema and structure const cerebrasErrorSchema = z.object({ message: z.string(), type: z.string(), param: z.string(), code: z.string(), }); export type CerebrasErrorData = z.infer<typeof cerebrasErrorSchema>; const cerebrasErrorStructure: ProviderErrorStructure<CerebrasErrorData> = { errorSchema: cerebrasErrorSchema, errorToMessage: data => data.message, }; export interface CerebrasProviderSettings { /** Cerebras API key. */ apiKey?: string; /** Base URL for the API calls. */ baseURL?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } export interface CerebrasProvider extends ProviderV2 { /** Creates a Cerebras model for text generation. */ (modelId: CerebrasChatModelId): LanguageModelV2; /** Creates a Cerebras model for text generation. */ languageModel(modelId: CerebrasChatModelId): LanguageModelV2; /** Creates a Cerebras chat model for text generation. */ chat(modelId: CerebrasChatModelId): LanguageModelV2; } export function createCerebras( options: CerebrasProviderSettings = {}, ): CerebrasProvider { const baseURL = withoutTrailingSlash( options.baseURL ?? 'https://api.cerebras.ai/v1', ); const getHeaders = () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'CEREBRAS_API_KEY', description: 'Cerebras API key', })}`, ...options.headers, }); const createLanguageModel = (modelId: CerebrasChatModelId) => { return new OpenAICompatibleChatLanguageModel(modelId, { provider: `cerebras.chat`, url: ({ path }) => `${baseURL}${path}`, headers: getHeaders, fetch: options.fetch, errorStructure: cerebrasErrorStructure, }); }; const provider = (modelId: CerebrasChatModelId) => createLanguageModel(modelId); provider.languageModel = createLanguageModel; provider.chat = createLanguageModel; provider.textEmbeddingModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'textEmbeddingModel' }); }; provider.imageModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'imageModel' }); }; return provider; } export const cerebras = createCerebras(); --- File: /ai/packages/cerebras/src/index.ts --- export { createCerebras, cerebras } from './cerebras-provider'; export type { CerebrasProvider, CerebrasProviderSettings, } from './cerebras-provider'; export type { CerebrasErrorData } from './cerebras-provider'; --- File: /ai/packages/cerebras/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/cerebras/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/cerebras/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/codemod/scripts/generate-readme.ts --- import fs from 'fs'; import path from 'path'; // Read actual codemod files from the filesystem const codemodsDir = path.join(process.cwd(), 'src', 'codemods'); if (!fs.existsSync(codemodsDir)) { throw new Error(`Codemods directory not found: ${codemodsDir}`); } function scanCodemodsRecursively(dir: string, prefix: string = ''): string[] { const files: string[] = []; const items = fs.readdirSync(dir); for (const item of items) { const itemPath = path.join(dir, item); const stat = fs.statSync(itemPath); if (stat.isDirectory() && item !== 'lib') { files.push(...scanCodemodsRecursively(itemPath, prefix + item + '/')); } else if ( stat.isFile() && item.endsWith('.ts') && !item.includes('lib/') ) { files.push(prefix + item.replace('.ts', '')); } } return files; } const codemodFiles = scanCodemodsRecursively(codemodsDir).sort(); function generateDescription(name: string): string { const readable = name.replace(/-/g, ' '); if (name.startsWith('remove-')) { return `Removes ${readable.replace('remove ', '')}`; } if (name.startsWith('replace-')) { return `Replaces ${readable.replace('replace ', '')}`; } if (name.startsWith('rename-')) { return `Renames ${readable.replace('rename ', '')}`; } if (name.startsWith('rewrite-')) { return `Rewrites ${readable.replace('rewrite ', '')}`; } return `Transforms ${readable}`; } function categorizeCodemod(name: string): string { if (name.startsWith('v4/')) { return 'v4 Codemods (v3 → v4 Migration)'; } if (name.startsWith('v5/')) { return 'v5 Codemods (v4 → v5 Migration)'; } return 'General Codemods'; } const categories: Record< string, Array<{ name: string; description: string }> > = {}; codemodFiles.forEach(codemod => { const category = categorizeCodemod(codemod); const description = generateDescription(codemod); if (!categories[category]) { categories[category] = []; } categories[category].push({ name: codemod, description }); }); const categoryOrder = [ 'v4 Codemods (v3 → v4 Migration)', 'v5 Codemods (v4 → v5 Migration)', 'General Codemods', ]; function generateCategoryTable( categoryName: string, codemods: Array<{ name: string; description: string }>, ) { const header = `### ${categoryName} | Codemod | Description | | ------- | ----------- |`; const rows = codemods .map(codemod => `| \`${codemod.name}\` | ${codemod.description} |`) .join('\n'); return `${header}\n${rows}`; } const availableCodemodsSection = `## Available Codemods ${categoryOrder .filter(categoryName => categories[categoryName]) .map(categoryName => generateCategoryTable(categoryName, categories[categoryName]), ) .join('\n\n')}`; const readmePath = path.join(process.cwd(), 'README.md'); const readmeContent = fs.readFileSync(readmePath, 'utf8'); const startMarker = '## Available Codemods'; const endMarker = '## CLI Options'; const startIndex = readmeContent.indexOf(startMarker); const endIndex = readmeContent.indexOf(endMarker); if (startIndex === -1 || endIndex === -1) { throw new Error( 'Could not find Available Codemods section markers in README.md', ); } const newReadmeContent = readmeContent.substring(0, startIndex) + availableCodemodsSection + '\n\n' + readmeContent.substring(endIndex); fs.writeFileSync(readmePath, newReadmeContent); console.log('README.md updated with current codemods'); --- File: /ai/packages/codemod/scripts/scaffold-codemod.ts --- import fs from 'fs'; import path from 'path'; const codemodName = process.argv[2]; if (!codemodName) { console.error('Please provide a codemod name'); process.exit(1); } // Templates const codemodTemplate = `import { API, FileInfo } from 'jscodeshift'; export default function transformer(file: FileInfo, api: API) { const j = api.jscodeshift; const root = j(file.source); // TODO: Implement transform return root.toSource(); } `; const testTemplate = `import { describe, it } from 'vitest'; import transformer from '../codemods/${codemodName}'; import { testTransform } from './test-utils'; describe('${codemodName}', () => { it('transforms correctly', () => { testTransform(transformer, '${codemodName}'); }); }); `; const inputTemplate = `// @ts-nocheck // TODO: Add input code `; const outputTemplate = `// @ts-nocheck // TODO: Add expected output code `; // File paths const paths = { codemod: path.join(process.cwd(), 'src', 'codemods', `${codemodName}.ts`), test: path.join(process.cwd(), 'src', 'test', `${codemodName}.test.ts`), fixtures: path.join(process.cwd(), 'src', 'test', '__testfixtures__'), }; // Create files fs.writeFileSync(paths.codemod, codemodTemplate); fs.writeFileSync(paths.test, testTemplate); fs.writeFileSync( path.join(paths.fixtures, `${codemodName}.input.ts`), inputTemplate, ); fs.writeFileSync( path.join(paths.fixtures, `${codemodName}.output.ts`), outputTemplate, ); // Update bundle array const upgradePath = path.join(process.cwd(), 'src', 'lib', 'upgrade.ts'); const upgradeContent = fs.readFileSync(upgradePath, 'utf8'); const bundleMatch = upgradeContent.match(/const bundle = \[([\s\S]*?)\];/); if (bundleMatch) { const currentBundle = bundleMatch[1] .split('\n') .filter(line => line.trim()) .map(line => line.trim().replace(/[',]/g, '')); const newBundle = [...currentBundle, codemodName] .sort() .map(name => ` '${name}',`) .join('\n'); const newContent = upgradeContent.replace( /const bundle = \[([\s\S]*?)\];/, `const bundle = [\n${newBundle}\n];`, ); fs.writeFileSync(upgradePath, newContent); } console.log(`Created codemod files for '${codemodName}'`); --- File: /ai/packages/codemod/src/bin/codemod.ts --- #!/usr/bin/env node import debug from 'debug'; import { Command } from 'commander'; import { transform } from '../lib/transform'; import { upgrade, upgradeV4, upgradeV5 } from '../lib/upgrade'; import { TransformOptions } from '../lib/transform-options'; const log = debug('codemod'); const error = debug('codemod:error'); debug.enable('codemod:*'); const program = new Command(); const addTransformOptions = (command: Command): Command => { return command .option('-d, --dry', 'Dry run (no changes are made to files)') .option('-p, --print', 'Print transformed files to stdout') .option('--verbose', 'Show more information about the transform process') .option( '-j, --jscodeshift <options>', 'Pass options directly to jscodeshift', ); }; addTransformOptions( program .name('codemod') .description('CLI tool for running codemods') .argument('<codemod>', 'Codemod to run (e.g., rewrite-framework-imports)') .argument('<source>', 'Path to source files or directory to transform'), ).action((codemod, source, options: TransformOptions) => { try { transform(codemod, source, options); } catch (err: any) { error(`Error transforming: ${err}`); process.exit(1); } }); addTransformOptions( program .command('upgrade') .description('Upgrade ai package dependencies and apply all codemods'), ).action((options: TransformOptions) => { try { upgrade(options); } catch (err: any) { error(`Error upgrading: ${err}`); process.exit(1); } }); addTransformOptions( program.command('v4').description('Apply v4 codemods (v3 → v4 migration)'), ).action((options: TransformOptions) => { try { upgradeV4(options); } catch (err: any) { error(`Error applying v4 codemods: ${err}`); process.exit(1); } }); addTransformOptions( program.command('v5').description('Apply v5 codemods (v4 → v5 migration)'), ).action((options: TransformOptions) => { try { upgradeV5(options); } catch (err: any) { error(`Error applying v5 codemods: ${err}`); process.exit(1); } }); program.parse(process.argv); --- File: /ai/packages/codemod/src/codemods/lib/create-transformer.ts --- import { FileInfo, API, JSCodeshift, Collection } from 'jscodeshift'; type TransformerFunction = ( fileInfo: FileInfo, api: API, options: any, context: TransformContext, ) => void; export interface TransformContext { /** * The jscodeshift API object. */ j: JSCodeshift; /** * The root collection of the AST. */ root: Collection<any>; /** * Codemods should set this to true if they make any changes to the AST. */ hasChanges: boolean; /** * Codemods can append messages to this array to report information to the user. */ messages: string[]; } export function createTransformer(transformFn: TransformerFunction) { // Note the return type of this function is explicitly designed to conform to // the signature expected by jscodeshift. For more see // https://github.com/facebook/jscodeshift return function transformer(fileInfo: FileInfo, api: API, options: any) { const j = api.jscodeshift; const root = j(fileInfo.source); const context: TransformContext = { j, root, hasChanges: false, messages: [], }; // Execute the transformation transformFn(fileInfo, api, options, context); // Report any messages context.messages.forEach(message => api.report(message)); // Return the transformed source code if changes were made return context.hasChanges ? root.toSource({ quote: 'single' }) : null; }; } --- File: /ai/packages/codemod/src/codemods/lib/remove-await-fn.ts --- import { createTransformer } from './create-transformer'; export function removeAwaitFn(functionName: string) { return createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Find import of the specified function from 'ai' const functionImportNames = new Set<string>(); root .find(j.ImportDeclaration) .filter(path => path.node.source.value === 'ai') .forEach(path => { path.node.specifiers?.forEach(specifier => { if ( specifier.type === 'ImportSpecifier' && specifier.imported.name === functionName ) { // Add local name to the set (handle aliasing) const localName = specifier.local?.name || specifier.imported.name; functionImportNames.add(localName); } }); }); if (functionImportNames.size > 0) { // Remove 'await' before calls to the specified function root .find(j.AwaitExpression) .filter(path => { const argument = path.node.argument; if ( argument && argument.type === 'CallExpression' && argument.callee.type === 'Identifier' ) { return functionImportNames.has(argument.callee.name); } return false; }) .forEach(path => { context.hasChanges = true; j(path).replaceWith(path.node.argument); }); } }); } --- File: /ai/packages/codemod/src/codemods/lib/remove-facade.ts --- import { createTransformer } from './create-transformer'; interface FacadeConfig { packageName: string; // e.g. 'openai' className: string; // e.g. 'OpenAI' createFnName: string; // e.g. 'createOpenAI' } export function removeFacade(config: FacadeConfig) { return createTransformer((fileInfo, api, options, context) => { const { j, root } = context; const importPath = `@ai-sdk/${config.packageName}`; // Track which imports came from our target package const targetImports = new Set<string>(); // First pass - collect imports from our target package root .find(j.ImportDeclaration) .filter(path => path.node.source.value === importPath) .forEach(path => { path.node.specifiers?.forEach(spec => { if ( spec.type === 'ImportSpecifier' && spec.imported.name === config.className && spec.local ) { context.hasChanges = true; targetImports.add(spec.local.name); } }); }); // Second pass - replace imports we found root .find(j.ImportDeclaration) .filter(path => path.node.source.value === importPath) .forEach(path => { const hasClassSpecifier = path.node.specifiers?.some( spec => spec.type === 'ImportSpecifier' && spec.imported.name === config.className, ); if (hasClassSpecifier) { context.hasChanges = true; path.node.specifiers = [ j.importSpecifier(j.identifier(config.createFnName)), ]; } }); // Only replace new expressions for classes from our package root .find(j.NewExpression) .filter( path => path.node.callee.type === 'Identifier' && targetImports.has(path.node.callee.name), ) .forEach(path => { context.hasChanges = true; j(path).replaceWith( j.callExpression( j.identifier(config.createFnName), path.node.arguments, ), ); }); }); } --- File: /ai/packages/codemod/src/codemods/v4/remove-ai-stream-methods-from-stream-text-result.ts --- import { createTransformer } from '../lib/create-transformer'; const REMOVED_METHODS = [ 'toAIStream', 'pipeAIStreamToResponse', 'toAIStreamResponse', ]; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Find calls to removed methods root.find(j.MemberExpression).forEach(path => { if ( path.node.property.type === 'Identifier' && REMOVED_METHODS.includes(path.node.property.name) ) { context.hasChanges = true; // Find the parent statement to add the comment const statement = path.parent.parent; if (statement && statement.node) { // Add block comment above the statement const comment = j.commentBlock( ` WARNING: ${path.node.property.name} has been removed from streamText.\n` + ` See migration guide at https://ai-sdk.dev/docs/migration-guides `, true, // leading false, // trailing ); statement.node.comments = statement.node.comments || []; statement.node.comments.unshift(comment); } } }); if (context.hasChanges) { context.messages.push( `Found usage of removed streamText methods: ${REMOVED_METHODS.join( ', ', )}. These methods have been removed. Please see migration guide.`, ); } }); --- File: /ai/packages/codemod/src/codemods/v4/remove-anthropic-facade.ts --- import { removeFacade } from '../lib/remove-facade'; export default removeFacade({ packageName: 'anthropic', className: 'Anthropic', createFnName: 'createAnthropic', }); --- File: /ai/packages/codemod/src/codemods/v4/remove-await-streamobject.ts --- import { removeAwaitFn } from '../lib/remove-await-fn'; export default removeAwaitFn('streamObject'); --- File: /ai/packages/codemod/src/codemods/v4/remove-await-streamtext.ts --- import { removeAwaitFn } from '../lib/remove-await-fn'; export default removeAwaitFn('streamText'); --- File: /ai/packages/codemod/src/codemods/v4/remove-deprecated-provider-registry-exports.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Replace imports root .find(j.ImportDeclaration) .filter(path => path.node.source.value === 'ai') .forEach(path => { const newSpecifiers = path.node.specifiers ?.map(spec => { if (spec.type !== 'ImportSpecifier') return spec; const oldName = spec.imported.name; if (oldName === 'experimental_createModelRegistry') { context.hasChanges = true; return j.importSpecifier( j.identifier('experimental_createProviderRegistry'), ); } if ( [ 'experimental_Provider', 'experimental_ProviderRegistry', 'experimental_ModelRegistry', ].includes(oldName) ) { context.hasChanges = true; return j.importSpecifier(j.identifier('Provider')); } return spec; }) .filter((spec, index, arr) => { // Deduplicate specifiers if (!spec) return false; return ( arr.findIndex( s => s?.type === 'ImportSpecifier' && spec.type === 'ImportSpecifier' && s.imported.name === spec.imported.name, ) === index ); }); path.node.specifiers = newSpecifiers; }); // Replace type references root .find(j.TSTypeReference) .filter( path => path.node.typeName.type === 'Identifier' && [ 'experimental_Provider', 'experimental_ProviderRegistry', 'experimental_ModelRegistry', ].includes(path.node.typeName.name), ) .forEach(path => { context.hasChanges = true; path.node.typeName = j.identifier('Provider'); }); // Replace function calls root .find(j.CallExpression, { callee: { type: 'Identifier', name: 'experimental_createModelRegistry', }, }) .forEach(path => { context.hasChanges = true; path.node.callee = j.identifier('experimental_createProviderRegistry'); }); }); --- File: /ai/packages/codemod/src/codemods/v4/remove-experimental-ai-fn-exports.ts --- import { createTransformer } from '../lib/create-transformer'; const EXPERIMENTAL_MAPPINGS = { experimental_generateText: 'generateText', experimental_streamText: 'streamText', experimental_generateObject: 'generateObject', experimental_streamObject: 'streamObject', } as const; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Replace imports of experimental functions root.find(j.ImportDeclaration).forEach(path => { if (path.node.source.value === 'ai') { path.node.specifiers?.forEach(specifier => { if ( specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name in EXPERIMENTAL_MAPPINGS ) { context.hasChanges = true; const newName = EXPERIMENTAL_MAPPINGS[ specifier.imported.name as keyof typeof EXPERIMENTAL_MAPPINGS ]; specifier.imported.name = newName; if (specifier.local) { specifier.local.name = newName; } } }); } }); // Replace calls to experimental functions root.find(j.CallExpression).forEach(path => { if ( path.node.callee.type === 'Identifier' && path.node.callee.name in EXPERIMENTAL_MAPPINGS ) { context.hasChanges = true; path.node.callee.name = EXPERIMENTAL_MAPPINGS[ path.node.callee.name as keyof typeof EXPERIMENTAL_MAPPINGS ]; } }); }); --- File: /ai/packages/codemod/src/codemods/v4/remove-experimental-message-types.ts --- import { Identifier } from 'jscodeshift'; import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Type mapping const typeMap = { ExperimentalMessage: 'CoreMessage', ExperimentalUserMessage: 'CoreUserMessage', ExperimentalAssistantMessage: 'CoreAssistantMessage', ExperimentalToolMessage: 'CoreToolMessage', }; // Replace imports root .find(j.ImportSpecifier) .filter(path => Object.keys(typeMap).includes(path.node.imported.name)) .forEach(path => { context.hasChanges = true; const oldName = path.node.imported.name; const newName = typeMap[oldName as keyof typeof typeMap]; j(path).replaceWith(j.importSpecifier(j.identifier(newName))); }); // Replace type references root .find(j.TSTypeReference) .filter(path => { const typeName = path.node.typeName; return ( typeName.type === 'Identifier' && Object.prototype.hasOwnProperty.call(typeMap, typeName.name) ); }) .forEach(path => { context.hasChanges = true; const typeName = path.node.typeName as Identifier; typeName.name = typeMap[typeName.name as keyof typeof typeMap]; }); }); --- File: /ai/packages/codemod/src/codemods/v4/remove-experimental-streamdata.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Track imports from 'ai' package only const targetImports = new Set<string>(); // First pass - collect imports from 'ai' package root .find(j.ImportDeclaration) .filter(path => path.node.source.value === 'ai') .forEach(path => { path.node.specifiers?.forEach(spec => { if ( spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier' && spec.imported.name === 'experimental_StreamData' ) { context.hasChanges = true; // Track local name targetImports.add(spec.local?.name || 'experimental_StreamData'); } }); }); // Second pass - replace imports from 'ai' package only root .find(j.ImportDeclaration) .filter(path => path.node.source.value === 'ai') .forEach(path => { const newSpecifiers = path.node.specifiers?.map(spec => { if ( spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier' && spec.imported.name === 'experimental_StreamData' ) { context.hasChanges = true; return j.importSpecifier( j.identifier('StreamData'), spec.local?.name === 'experimental_StreamData' ? null : spec.local, ); } return spec; }); path.node.specifiers = newSpecifiers; }); // Replace type/class references only for tracked imports root .find(j.Identifier) .filter(path => { // Only replace if: // 1. It's one of our tracked imports from 'ai' // 2. It's not part of an import declaration (to avoid replacing other imports) return ( targetImports.has(path.node.name) && !j(path).closest(j.ImportDeclaration).size() ); }) .forEach(path => { path.node.name = 'StreamData'; context.hasChanges = true; }); }); --- File: /ai/packages/codemod/src/codemods/v4/remove-experimental-tool.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Track ExperimentalTool imports from 'ai' package const targetImports = new Set<string>(); // First pass - collect imports from 'ai' package root .find(j.ImportDeclaration) .filter(path => path.node.source.value === 'ai') .forEach(path => { path.node.specifiers?.forEach(spec => { if ( spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier' && spec.imported.name === 'ExperimentalTool' ) { context.hasChanges = true; targetImports.add(spec.local?.name || spec.imported.name); spec.imported.name = 'CoreTool'; if (spec.local) { spec.local.name = 'CoreTool'; } } }); }); // Only replace type references from 'ai' package root .find(j.TSTypeReference) .filter( path => path.node.typeName.type === 'Identifier' && targetImports.has(path.node.typeName.name), ) .forEach(path => { if (path.node.typeName.type === 'Identifier') { context.hasChanges = true; path.node.typeName.name = 'CoreTool'; } }); }); --- File: /ai/packages/codemod/src/codemods/v4/remove-experimental-useassistant.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Find and replace import specifiers root .find(j.ImportSpecifier) .filter(path => path.node.imported.name === 'experimental_useAssistant') .forEach(path => { context.hasChanges = true; j(path).replaceWith(j.importSpecifier(j.identifier('useAssistant'))); }); // Find and replace usage in the code root .find(j.Identifier) .filter(path => path.node.name === 'experimental_useAssistant') .forEach(path => { context.hasChanges = true; path.node.name = 'useAssistant'; }); }); --- File: /ai/packages/codemod/src/codemods/v4/remove-google-facade.ts --- import { removeFacade } from '../lib/remove-facade'; export default removeFacade({ packageName: 'google', className: 'Google', createFnName: 'createGoogleGenerativeAI', }); --- File: /ai/packages/codemod/src/codemods/v4/remove-isxxxerror.ts --- import { createTransformer } from '../lib/create-transformer'; const ERROR_METHOD_MAPPINGS: Record<string, string> = { isAPICallError: 'APICallError.isInstance', isEmptyResponseBodyError: 'EmptyResponseBodyError.isInstance', isInvalidArgumentError: 'InvalidArgumentError.isInstance', isInvalidPromptError: 'InvalidPromptError.isInstance', isInvalidResponseDataError: 'InvalidResponseDataError.isInstance', isJSONParseError: 'JSONParseError.isInstance', isLoadAPIKeyError: 'LoadAPIKeyError.isInstance', isLoadSettingError: 'LoadSettingError.isInstance', isNoContentGeneratedError: 'NoContentGeneratedError.isInstance', isNoObjectGeneratedError: 'NoObjectGeneratedError.isInstance', isNoSuchModelError: 'NoSuchModelError.isInstance', isNoSuchProviderError: 'NoSuchProviderError.isInstance', isNoSuchToolError: 'NoSuchToolError.isInstance', isTooManyEmbeddingValuesForCallError: 'TooManyEmbeddingValuesForCallError.isInstance', isTypeValidationError: 'TypeValidationError.isInstance', isUnsupportedFunctionalityError: 'UnsupportedFunctionalityError.isInstance', isInvalidDataContentError: 'InvalidDataContentError.isInstance', isInvalidMessageRoleError: 'InvalidMessageRoleError.isInstance', isDownloadError: 'DownloadError.isInstance', isRetryError: 'RetryError.isInstance', }; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Track imports from ai packages const targetImports = new Set<string>(); // Collect imports from ai packages root .find(j.ImportDeclaration) .filter( path => path.node.source.value === 'ai' || (typeof path.node.source.value === 'string' && path.node.source.value.startsWith('@ai-sdk/')), ) .forEach(path => { path.node.specifiers?.forEach(spec => { if (spec.type === 'ImportSpecifier') { const name = spec.imported.name; if (Object.keys(ERROR_METHOD_MAPPINGS).includes(name)) { context.hasChanges = true; targetImports.add(spec.local?.name || name); } } }); }); // Replace method calls root .find(j.CallExpression) .filter(path => { const callee = path.node.callee; return ( callee.type === 'MemberExpression' && 'property' in callee && callee.property.type === 'Identifier' && Object.keys(ERROR_METHOD_MAPPINGS).includes(callee.property.name) ); }) .forEach(path => { context.hasChanges = true; const property = ( path.node.callee as import('jscodeshift').MemberExpression ).property; const methodName = property.type === 'Identifier' ? property.name : ''; const newMethodPath = ERROR_METHOD_MAPPINGS[methodName].split('.'); j(path).replaceWith( j.callExpression( j.memberExpression( j.identifier(newMethodPath[0]), j.identifier(newMethodPath[1]), ), path.node.arguments, ), ); }); }); --- File: /ai/packages/codemod/src/codemods/v4/remove-metadata-with-headers.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Track imports from 'ai' package const targetImports = new Set<string>(); // Replace imports root .find(j.ImportDeclaration) .filter(path => path.node.source.value === 'ai') .forEach(path => { path.node.specifiers = path.node.specifiers?.filter(spec => { if ( spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier' && spec.imported.name === 'LanguageModelResponseMetadataWithHeaders' ) { context.hasChanges = true; // Track local name targetImports.add(spec.local?.name || spec.imported.name); // Replace with new type spec.imported.name = 'LanguageModelResponseMetadata'; if (spec.local) { spec.local.name = 'LanguageModelResponseMetadata'; } return true; } return true; }); }); // Replace type references root .find(j.TSTypeReference) .filter( path => path.node.typeName.type === 'Identifier' && targetImports.has(path.node.typeName.name), ) .forEach(path => { if (path.node.typeName.type === 'Identifier') { context.hasChanges = true; path.node.typeName.name = 'LanguageModelResponseMetadata'; } }); }); --- File: /ai/packages/codemod/src/codemods/v4/remove-mistral-facade.ts --- import { removeFacade } from '../lib/remove-facade'; export default removeFacade({ packageName: 'mistral', className: 'Mistral', createFnName: 'createMistral', }); --- File: /ai/packages/codemod/src/codemods/v4/remove-openai-facade.ts --- import { removeFacade } from '../lib/remove-facade'; export default removeFacade({ packageName: 'openai', className: 'OpenAI', createFnName: 'createOpenAI', }); --- File: /ai/packages/codemod/src/codemods/v4/rename-format-stream-part.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Track if formatStreamPart is imported from 'ai' const targetImports = new Set<string>(); // Find and update imports from 'ai' root .find(j.ImportDeclaration) .filter(path => path.node.source.value === 'ai') .forEach(path => { path.node.specifiers?.forEach(specifier => { if ( specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name === 'formatStreamPart' ) { context.hasChanges = true; targetImports.add(specifier.local?.name || specifier.imported.name); // Update import name specifier.imported.name = 'formatDataStreamPart'; if (specifier.local) { specifier.local.name = 'formatDataStreamPart'; } } }); }); root .find(j.CallExpression) .filter( path => path.node.callee.type === 'Identifier' && targetImports.has(path.node.callee.name), ) .forEach(path => { if (path.node.callee.type === 'Identifier') { context.hasChanges = true; path.node.callee.name = 'formatDataStreamPart'; } }); }); --- File: /ai/packages/codemod/src/codemods/v4/rename-parse-stream-part.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Track if parseStreamPart is imported from 'ai' const targetImports = new Set<string>(); // Find and update imports from 'ai' root .find(j.ImportDeclaration) .filter(path => path.node.source.value === 'ai') .forEach(path => { path.node.specifiers?.forEach(specifier => { if ( specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name === 'parseStreamPart' ) { context.hasChanges = true; // Track local name targetImports.add(specifier.local?.name || specifier.imported.name); // Update import name specifier.imported.name = 'parseDataStreamPart'; if (specifier.local) { specifier.local.name = 'parseDataStreamPart'; } } }); }); // Update function calls only if imported from 'ai' root .find(j.CallExpression) .filter( path => path.node.callee.type === 'Identifier' && targetImports.has(path.node.callee.name), ) .forEach(path => { if (path.node.callee.type === 'Identifier') { context.hasChanges = true; path.node.callee.name = 'parseDataStreamPart'; } }); }); --- File: /ai/packages/codemod/src/codemods/v4/replace-baseurl.ts --- import { createTransformer } from '../lib/create-transformer'; const PROVIDER_CREATORS = [ 'createAnthropic', 'createAzure', 'createCohere', 'createGoogle', 'createGoogleGenerativeAI', 'createGroq', 'createMistral', 'createOpenAI', ]; function isWithinProviderCall(j: any, path: any): boolean { // Walk up the AST to find parent CallExpression let current = path; while (current) { if ( current.parent?.node.type === 'CallExpression' && current.parent.node.callee.type === 'Identifier' && PROVIDER_CREATORS.includes(current.parent.node.callee.name) ) { return true; } current = current.parent; } return false; } export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Find and rename baseUrl properties root .find(j.ObjectProperty, { key: { type: 'Identifier', name: 'baseUrl', }, }) .filter(path => isWithinProviderCall(j, path)) .forEach(path => { // Rename baseUrl to baseURL if (path.node.key.type === 'Identifier') { context.hasChanges = true; path.node.key.name = 'baseURL'; } }); }); --- File: /ai/packages/codemod/src/codemods/v4/replace-continuation-steps.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Track imports from 'ai' package const targetImports = new Set<string>(); // First pass - collect imports root .find(j.ImportDeclaration) .filter(path => path.node.source.value === 'ai') .forEach(path => { path.node.specifiers?.forEach(spec => { if ( spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier' && spec.imported.name === 'generateText' ) { targetImports.add(spec.local?.name || spec.imported.name); } }); }); function isWithinGenerateTextCall(path: any): boolean { let current = path; while (current) { if ( current.node.type === 'CallExpression' && current.node.callee.type === 'Identifier' && targetImports.has(current.node.callee.name) ) { return true; } current = current.parent; } return false; } // Replace property name only within generateText calls root .find(j.ObjectProperty) .filter( path => path.node.key.type === 'Identifier' && path.node.key.name === 'experimental_continuationSteps' && isWithinGenerateTextCall(path), ) .forEach(path => { if (path.node.key.type === 'Identifier') { context.hasChanges = true; path.node.key.name = 'experimental_continueSteps'; } }); }); --- File: /ai/packages/codemod/src/codemods/v4/replace-langchain-toaistream.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Replace LangChainAdapter.toAIStream with LangChainAdapter.toDataStream root .find(j.MemberExpression, { object: { type: 'Identifier', name: 'LangChainAdapter', }, property: { type: 'Identifier', name: 'toAIStream', }, }) .forEach(path => { if (path.node.property.type === 'Identifier') { context.hasChanges = true; path.node.property.name = 'toDataStream'; } }); }); --- File: /ai/packages/codemod/src/codemods/v4/replace-nanoid.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Find and replace import specifiers from 'ai' root .find(j.ImportDeclaration) .filter(path => path.node.source.value === 'ai') .forEach(path => { j(path) .find(j.ImportSpecifier) .filter(specifierPath => specifierPath.node.imported.name === 'nanoid') .forEach(specifierPath => { context.hasChanges = true; specifierPath.replace(j.importSpecifier(j.identifier('generateId'))); }); }); // If we found changes, also replace object properties in the code if (context.hasChanges) { root .find(j.ObjectProperty, { key: { name: 'generateId' }, value: { name: 'nanoid' }, }) .forEach(path => { const newProperty = j.objectProperty( j.identifier('generateId'), j.identifier('generateId'), ); newProperty.shorthand = true; path.replace(newProperty); }); } }); --- File: /ai/packages/codemod/src/codemods/v4/replace-roundtrips-with-maxsteps.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; root .find(j.CallExpression) .filter( path => path.node.callee.type === 'Identifier' && ['generateText', 'streamText'].includes(path.node.callee.name), ) .forEach(path => { const optionsArg = path.node.arguments[0]; if (optionsArg?.type !== 'ObjectExpression') return; let maxStepsValue = 1; let foundRoundtrips = false; optionsArg.properties = optionsArg.properties.filter(prop => { if ( prop.type === 'ObjectProperty' && prop.key.type === 'Identifier' && ['maxToolRoundtrips', 'maxAutomaticRoundtrips'].includes( prop.key.name, ) ) { foundRoundtrips = true; if (prop.value.type === 'NumericLiteral') { maxStepsValue = prop.value.value + 1; } return false; // Remove the property } return true; }); if (foundRoundtrips) { context.hasChanges = true; optionsArg.properties.push( j.objectProperty( j.identifier('maxSteps'), j.numericLiteral(maxStepsValue), ), ); } }); // Replace property access root .find(j.MemberExpression) .filter( path => path.node.property.type === 'Identifier' && path.node.property.name === 'roundtrips', ) .forEach(path => { if (path.node.property.type === 'Identifier') { context.hasChanges = true; path.node.property.name = 'steps'; } }); }); --- File: /ai/packages/codemod/src/codemods/v4/replace-token-usage-types.ts --- import { createTransformer } from '../lib/create-transformer'; import { ImportSpecifier, ImportDefaultSpecifier, ImportNamespaceSpecifier, } from 'jscodeshift'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Define the mapping from old names to new names const renameMap: Record<string, string> = { TokenUsage: 'LanguageModelUsage', CompletionTokenUsage: 'LanguageModelUsage', EmbeddingTokenUsage: 'EmbeddingModelUsage', }; // Set to keep track of already imported new names to avoid duplicates const importedNewNames = new Set<string>(); // Replace imports at ImportDeclaration level root .find(j.ImportDeclaration) .filter(path => path.node.source.value === 'ai') .forEach(path => { const importSpecifiers = path.node.specifiers || []; const newSpecifiers: ( | ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier )[] = []; const addedNewSpecifiers = new Set<string>(); importSpecifiers.forEach(spec => { if (spec.type !== 'ImportSpecifier') { // Retain non-ImportSpecifier specifiers (e.g., default imports, namespace imports) newSpecifiers.push(spec); return; } const oldName = spec.imported.name; if (!renameMap.hasOwnProperty(oldName)) { // Retain specifiers that are not part of the renaming newSpecifiers.push(spec); return; } const newName = renameMap[oldName]; if (!addedNewSpecifiers.has(newName)) { // Add the new specifier only if it hasn't been added yet newSpecifiers.push(j.importSpecifier(j.identifier(newName))); addedNewSpecifiers.add(newName); context.hasChanges = true; } }); // Replace the specifiers with the new specifiers if changes were made if (addedNewSpecifiers.size > 0) { path.node.specifiers = newSpecifiers; } }); // Replace type references root .find(j.TSTypeReference) .filter( path => path.node.typeName.type === 'Identifier' && Object.keys(renameMap).includes(path.node.typeName.name), ) .forEach(path => { if (path.node.typeName.type === 'Identifier') { const oldName = path.node.typeName.name; const newName = renameMap[oldName]; if (newName) { context.hasChanges = true; path.node.typeName = j.identifier(newName); } } }); }); --- File: /ai/packages/codemod/src/codemods/v4/rewrite-framework-imports.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; root.find(j.ImportDeclaration).forEach(path => { const sourceValue = path.node.source.value as string; const match = sourceValue.match(/^ai\/(svelte|vue|solid)$/); if (match) { context.hasChanges = true; path.node.source.value = `@ai-sdk/${match[1]}`; } }); }); --- File: /ai/packages/codemod/src/codemods/v5/flatten-streamtext-file-properties.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Find member expressions that match: delta.file.mediaType or delta.file.data root .find(j.MemberExpression) .filter(path => { const node = path.node; // Check if this is a nested member expression: something.file.property if (!j.MemberExpression.check(node.object)) { return false; } const outerObject = node.object; // Check if the middle property is 'file' if ( !j.Identifier.check(outerObject.property) || outerObject.property.name !== 'file' ) { return false; } // Check if the outermost object is 'delta' if ( !j.Identifier.check(outerObject.object) || outerObject.object.name !== 'delta' ) { return false; } // Check if the final property is 'mediaType' or 'data' if (!j.Identifier.check(node.property)) { return false; } const propertyName = node.property.name; return propertyName === 'mediaType' || propertyName === 'data'; }) .forEach(path => { const node = path.node; const outerObject = node.object as any; // We know this is a MemberExpression from the filter const propertyName = (node.property as any).name; // We know this is an Identifier // Transform delta.file.mediaType to delta.mediaType // Transform delta.file.data to delta.data const newMemberExpression = j.memberExpression( outerObject.object, // delta j.identifier(propertyName), // mediaType or data ); path.replace(newMemberExpression); context.hasChanges = true; }); }); --- File: /ai/packages/codemod/src/codemods/v5/import-LanguageModelV2-from-provider-package.ts --- import { createTransformer } from '../lib/create-transformer'; const ImportMappings: Record<string, string> = { LanguageModelV1: 'LanguageModelV2', LanguageModelV2: 'LanguageModelV2', LanguageModelV1Middleware: 'LanguageModelV2Middleware', LanguageModelV2Middleware: 'LanguageModelV2Middleware', }; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Find all import declarations root.find(j.ImportDeclaration).forEach(importPath => { const node = importPath.node; // Check if the source value is exactly '@ai-sdk/provider' if (node.source.value !== '@ai-sdk/provider') return; // Check if the named import includes 'LanguageModelV1' or 'LanguageModelV2' const specifiers = node.specifiers?.filter( s => j.ImportSpecifier.check(s) && Object.keys(ImportMappings).includes(s.imported.name), ) ?? []; // Rename LanguageModelV1 to LanguageModelV2 if present for (const specifier of specifiers) { if ( specifier.type === 'ImportSpecifier' && ImportMappings[specifier.imported.name] ) { specifier.imported.name = ImportMappings[specifier.imported.name]; } } // Set hasChanges to true since we will modify the AST context.hasChanges = true; // Change the module source from '@ai-sdk/provider' to 'ai' node.source.value = 'ai'; context.messages.push(`Updated import from '@ai-sdk/provider' to 'ai'`); }); }); --- File: /ai/packages/codemod/src/codemods/v5/migrate-to-data-stream-protocol-v2.ts --- import { createTransformer } from '../lib/create-transformer'; /** * Migrates from Data Stream Protocol v1 to v2: * - writer.writeData(value) → writer.write({ type: 'data', value: [value] }) * - writer.writeMessageAnnotation(obj) → writer.write({ type: 'message-annotations', value: [obj] }) * - writer.writeSource(obj) → writer.write({ type: 'source', value: obj }) * - formatDataStreamPart('tool_result', obj) → { type: 'tool-result', value: obj } * - Removes unused formatDataStreamPart imports */ export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Transform writer.writeData() calls root .find(j.CallExpression, { callee: { type: 'MemberExpression', property: { name: 'writeData' }, }, }) .forEach((path: any) => { const args = path.node.arguments; if (args.length === 1) { // Transform writeData(value) to write({ type: 'data', value: [value] }) path.node.callee.property.name = 'write'; path.node.arguments = [ j.objectExpression([ j.property('init', j.literal('type'), j.literal('data')), j.property( 'init', j.literal('value'), j.arrayExpression([args[0]]), ), ]), ]; context.hasChanges = true; } }); // Transform writer.writeMessageAnnotation() calls root .find(j.CallExpression, { callee: { type: 'MemberExpression', property: { name: 'writeMessageAnnotation' }, }, }) .forEach((path: any) => { const args = path.node.arguments; if (args.length === 1) { // Transform writeMessageAnnotation(obj) to write({ type: 'message-annotations', value: [obj] }) path.node.callee.property.name = 'write'; path.node.arguments = [ j.objectExpression([ j.property( 'init', j.literal('type'), j.literal('message-annotations'), ), j.property( 'init', j.literal('value'), j.arrayExpression([args[0]]), ), ]), ]; context.hasChanges = true; } }); // Transform writer.writeSource() calls root .find(j.CallExpression, { callee: { type: 'MemberExpression', property: { name: 'writeSource' }, }, }) .forEach((path: any) => { const args = path.node.arguments; if (args.length === 1 && args[0].type === 'ObjectExpression') { // Transform writeSource(obj) to write({ type: 'source', value: obj }) const sourceObj = args[0]; path.node.callee.property.name = 'write'; path.node.arguments = [ j.objectExpression([ j.property('init', j.literal('type'), j.literal('source')), j.property('init', j.literal('value'), sourceObj), ]), ]; context.hasChanges = true; } }); // Transform formatDataStreamPart() calls for tool results root .find(j.CallExpression, { callee: { name: 'formatDataStreamPart' }, }) .forEach((path: any) => { const args = path.node.arguments; if (args.length === 2) { const typeArg = args[0]; const valueArg = args[1]; // Replace the formatDataStreamPart call with the new format if ( (typeArg.type === 'Literal' || typeArg.type === 'StringLiteral') && typeArg.value === 'tool_result' ) { // Transform formatDataStreamPart('tool_result', obj) to { type: 'tool-result', value: obj } j(path).replaceWith( j.objectExpression([ j.property('init', j.literal('type'), j.literal('tool-result')), j.property('init', j.literal('value'), valueArg), ]), ); context.hasChanges = true; } } }); // Remove formatDataStreamPart import if it's no longer used // Check after all transformations are done const formatDataStreamPartUsages = root.find(j.CallExpression, { callee: { name: 'formatDataStreamPart' }, }); if (formatDataStreamPartUsages.length === 0) { root.find(j.ImportDeclaration).forEach((path: any) => { if ( path.node.source.value === 'ai' && path.node.specifiers?.some( (spec: any) => spec.type === 'ImportSpecifier' && spec.imported.name === 'formatDataStreamPart', ) ) { // Remove the import path.node.specifiers = path.node.specifiers?.filter( (spec: any) => !( spec.type === 'ImportSpecifier' && spec.imported.name === 'formatDataStreamPart' ), ); // If no specifiers left, remove the entire import if (path.node.specifiers?.length === 0) { path.prune(); } context.hasChanges = true; } }); } }); --- File: /ai/packages/codemod/src/codemods/v5/move-image-model-maxImagesPerCall.ts --- import { createTransformer } from '../lib/create-transformer'; /** * Transforms image model settings from model construction to generateImage options * * Before: * await generateImage({ * model: provider.image('model-id', { * maxImagesPerCall: 5, * pollIntervalMillis: 500, * }), * prompt, * n: 10, * }); * * After: * await generateImage({ * model: provider.image('model-id'), * prompt, * n: 10, * maxImagesPerCall: 5, * providerOptions: { * provider: { pollIntervalMillis: 500 }, * }, * }); */ export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Find generateImage call expressions root .find(j.CallExpression, { callee: { type: 'Identifier', name: 'generateImage', }, }) .forEach(path => { const args = path.node.arguments; if (args.length !== 1 || args[0].type !== 'ObjectExpression') { return; } const configObject = args[0]; let modelProperty: any = null; let maxImagesPerCallFromModel: any = null; let providerSettingsFromModel: Record<string, any> = {}; let providerName: string | null = null; // Find the model property configObject.properties.forEach((prop: any) => { if ( (prop.type === 'Property' || prop.type === 'ObjectProperty') && ((prop.key.type === 'Identifier' && prop.key.name === 'model') || (prop.key.type === 'Literal' && prop.key.value === 'model')) ) { modelProperty = prop; } }); if (!modelProperty) { return; } // Check if the model property value is a call expression (provider.image(...)) if ( modelProperty.value.type === 'CallExpression' && modelProperty.value.callee.type === 'MemberExpression' && modelProperty.value.callee.property.type === 'Identifier' && modelProperty.value.callee.property.name === 'image' ) { const imageCall = modelProperty.value; // Extract provider name from the callee if (imageCall.callee.object.type === 'Identifier') { providerName = imageCall.callee.object.name; } // Check if there's a second argument with settings if ( imageCall.arguments.length === 2 && imageCall.arguments[1].type === 'ObjectExpression' ) { const settingsObject = imageCall.arguments[1]; // Extract settings from the model constructor settingsObject.properties.forEach((settingProp: any) => { if ( settingProp.type === 'Property' || settingProp.type === 'ObjectProperty' ) { const keyName = settingProp.key.type === 'Identifier' ? settingProp.key.name : settingProp.key.value; if (keyName === 'maxImagesPerCall') { maxImagesPerCallFromModel = settingProp.value; } else { providerSettingsFromModel[keyName] = settingProp.value; } } }); // Remove the settings argument from the image call imageCall.arguments = [imageCall.arguments[0]]; context.hasChanges = true; } } // Add maxImagesPerCall to the generateImage config if it was in the model if (maxImagesPerCallFromModel) { const maxImagesPerCallProp = j.property( 'init', j.identifier('maxImagesPerCall'), maxImagesPerCallFromModel, ); configObject.properties.push(maxImagesPerCallProp); } // Add or update providerOptions with the extracted settings if (Object.keys(providerSettingsFromModel).length > 0 && providerName) { let providerOptionsProperty: any = null; // Find existing providerOptions property configObject.properties.forEach((prop: any) => { if ( (prop.type === 'Property' || prop.type === 'ObjectProperty') && ((prop.key.type === 'Identifier' && prop.key.name === 'providerOptions') || (prop.key.type === 'Literal' && prop.key.value === 'providerOptions')) ) { providerOptionsProperty = prop; } }); if (!providerOptionsProperty) { // Create new providerOptions property const providerSettingsProperties = Object.entries( providerSettingsFromModel, ).map(([key, value]) => j.property('init', j.identifier(key), value)); const providerOptionsValue = j.objectExpression([ j.property( 'init', j.identifier(providerName), j.objectExpression(providerSettingsProperties), ), ]); providerOptionsProperty = j.property( 'init', j.identifier('providerOptions'), providerOptionsValue, ); configObject.properties.push(providerOptionsProperty); } else { // Update existing providerOptions if (providerOptionsProperty.value.type === 'ObjectExpression') { let providerProperty: any = null; // Find the provider property in providerOptions providerOptionsProperty.value.properties.forEach((prop: any) => { if ( (prop.type === 'Property' || prop.type === 'ObjectProperty') && ((prop.key.type === 'Identifier' && prop.key.name === providerName) || (prop.key.type === 'Literal' && prop.key.value === providerName)) ) { providerProperty = prop; } }); if (!providerProperty) { // Create new provider property const providerSettingsProperties = Object.entries( providerSettingsFromModel, ).map(([key, value]) => j.property('init', j.identifier(key), value), ); providerProperty = j.property( 'init', j.identifier(providerName), j.objectExpression(providerSettingsProperties), ); providerOptionsProperty.value.properties.push(providerProperty); } else { // Merge with existing provider property if (providerProperty.value.type === 'ObjectExpression') { const newSettingsProperties = Object.entries( providerSettingsFromModel, ).map(([key, value]) => j.property('init', j.identifier(key), value), ); providerProperty.value.properties.push( ...newSettingsProperties, ); } } } } } }); }); --- File: /ai/packages/codemod/src/codemods/v5/move-langchain-adapter.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer( (fileInfo: any, api: any, options: any, context: any) => { const { j, root } = context; // Track all local names for LangChainAdapter and their new names const renamedLocals: { old: string; new: string }[] = []; // Transform import declarations root .find(j.ImportDeclaration, { source: { value: 'ai' } }) .forEach((path: any) => { const specifiers = path.node.specifiers; specifiers.forEach((specifier: any) => { if ( specifier.type === 'ImportSpecifier' && specifier.imported.name === 'toDataStreamResponse' ) { // Already migrated return; } if ( specifier.type === 'ImportSpecifier' && specifier.imported.name === 'LangChainAdapter' ) { const oldLocal = specifier.local.name; let newLocal = oldLocal; specifier.imported.name = 'toDataStreamResponse'; if (oldLocal === 'LangChainAdapter') { specifier.local.name = 'toDataStreamResponse'; newLocal = 'toDataStreamResponse'; } renamedLocals.push({ old: oldLocal, new: newLocal }); context.hasChanges = true; } }); path.node.source.value = '@ai-sdk/langchain'; context.messages.push( "Updated import of LangChainAdapter from 'ai' to '@ai-sdk/langchain' and renamed to toDataStreamResponse", ); }); // Transform X.toDataStreamResponse(...) to X(...) renamedLocals.forEach(({ old: localName, new: newName }) => { root .find(j.CallExpression, { callee: { type: 'MemberExpression', object: { type: 'Identifier', name: localName }, property: { type: 'Identifier', name: 'toDataStreamResponse' }, }, }) .forEach((path: any) => { path.node.callee = j.identifier(localName); context.hasChanges = true; context.messages.push( `Replaced ${localName}.toDataStreamResponse(...) with ${localName}(...)`, ); }); // If the local name was changed (not aliased), update all identifiers if (localName !== newName) { root.find(j.Identifier, { name: localName }).forEach((idPath: any) => { // Don't change import specifiers if (j.ImportSpecifier.check(idPath.parent.node)) return; idPath.node.name = newName; context.hasChanges = true; }); } }); }, ); --- File: /ai/packages/codemod/src/codemods/v5/move-provider-options.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // AI methods that accept a model parameter const aiMethods = [ 'embed', 'embedMany', 'generateText', 'generateObject', 'streamObject', 'streamText', 'generateSpeech', 'transcribe', ]; // Common provider function names (these typically come from @ai-sdk/* packages) const providerFunctions = [ 'openai', 'anthropic', 'google', 'mistral', 'groq', 'cohere', 'bedrock', 'vertex', 'perplexity', ]; const foundPatterns: Array<{ method: string; provider: string; line: number; hasExistingProviderOptions: boolean; }> = []; // Find AI method calls and detect provider options patterns root.find(j.CallExpression).forEach(path => { const { callee, arguments: args } = path.node; // Check if this is an AI method call if ( callee.type === 'Identifier' && aiMethods.includes(callee.name) && args.length > 0 ) { const firstArg = args[0]; // The first argument should be an object with a model property if (firstArg.type === 'ObjectExpression') { let modelProperty: any = null; let hasProviderOptions = false; // Find the model property and check for existing providerOptions firstArg.properties.forEach(prop => { const isPropertyType = prop.type === 'Property' || prop.type === 'ObjectProperty'; if (isPropertyType && prop.key && prop.key.type === 'Identifier') { if (prop.key.name === 'model') { modelProperty = prop; } else if (prop.key.name === 'providerOptions') { hasProviderOptions = true; } } }); if ( modelProperty && modelProperty.value && modelProperty.value.type === 'CallExpression' ) { const modelCall = modelProperty.value; // Check if the model call is a provider function with options (second argument) if ( modelCall.callee.type === 'Identifier' && providerFunctions.includes(modelCall.callee.name) && modelCall.arguments.length >= 2 ) { const providerName = modelCall.callee.name; const lineNumber = path.node.loc?.start?.line || 0; foundPatterns.push({ method: callee.name, provider: providerName, line: lineNumber, hasExistingProviderOptions: hasProviderOptions, }); } } } } }); // Generate helpful messages for found patterns if (foundPatterns.length > 0) { context.messages.push( `Found ${foundPatterns.length} AI method call(s) that need provider options migration:`, ); foundPatterns.forEach(pattern => { const action = pattern.hasExistingProviderOptions ? `add "${pattern.provider}: { ... }" to existing providerOptions` : `move provider options to providerOptions: { ${pattern.provider}: { ... } }`; context.messages.push( ` Line ${pattern.line}: ${pattern.method}() - ${action}`, ); }); context.messages.push(''); context.messages.push('Migration example:'); context.messages.push( ' Before: model: openai("gpt-4o", { dimensions: 10 })', ); context.messages.push(' After: model: openai("gpt-4o"),'); context.messages.push( ' providerOptions: { openai: { dimensions: 10 } }', ); context.messages.push(''); // TODO: add link to migration guide // context.messages.push('See migration guide: https://ai-sdk.dev/docs/migration/switch-to-provider-options'); } }); --- File: /ai/packages/codemod/src/codemods/v5/move-react-to-ai-sdk.ts --- import { createTransformer } from '../lib/create-transformer'; /** * Migrates from ai/react to @ai-sdk/react: * - import { useChat } from 'ai/react' → import { useChat } from '@ai-sdk/react' */ export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Transform imports from 'ai/react' to '@ai-sdk/react' root .find(j.ImportDeclaration, { source: { value: 'ai/react', }, }) .forEach((path: any) => { path.node.source.value = '@ai-sdk/react'; context.hasChanges = true; }); }); --- File: /ai/packages/codemod/src/codemods/v5/move-ui-utils-to-ai.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; root .find(j.ImportDeclaration, { source: { value: '@ai-sdk/ui-utils' } }) .forEach(path => { path.node.source.value = 'ai'; context.hasChanges = true; }); }); --- File: /ai/packages/codemod/src/codemods/v5/remove-experimental-wrap-language-model.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Track if the experimental function was imported and what local name it uses let importedLocalName: string | null = null; // Find import declarations from 'ai' and rename experimental_wrapLanguageModel root.find(j.ImportDeclaration).forEach(importPath => { const node = importPath.node; // Check if the source is 'ai' if (node.source.value !== 'ai') return; // Check named imports and rename them const specifiers = node.specifiers?.filter(s => j.ImportSpecifier.check(s)) ?? []; for (const specifier of specifiers) { if ( specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name === 'experimental_wrapLanguageModel' ) { // Track the local name that's being used in the code importedLocalName = specifier.local?.name || 'experimental_wrapLanguageModel'; // Update the import name specifier.imported.name = 'wrapLanguageModel'; // If there's no alias, we also need to update the local name if ( !specifier.local || specifier.local.name === 'experimental_wrapLanguageModel' ) { specifier.local = j.identifier('wrapLanguageModel'); } context.hasChanges = true; } } }); // If we found an import, also rename all usages in the code if ( importedLocalName && importedLocalName === 'experimental_wrapLanguageModel' ) { root.find(j.Identifier).forEach(identifierPath => { const node = identifierPath.node; // Only rename if this identifier matches the imported name if (node.name === 'experimental_wrapLanguageModel') { // Skip if this identifier is part of an import declaration (already handled above) const parent = identifierPath.parent; if ( parent && (j.ImportSpecifier.check(parent.node) || j.ImportDefaultSpecifier.check(parent.node) || j.ImportNamespaceSpecifier.check(parent.node)) ) { return; } // Skip if this is a property name in an object (e.g., { experimental_wrapLanguageModel: something }) if ( parent && j.Property.check(parent.node) && parent.node.key === node ) { return; } // Rename the identifier node.name = 'wrapLanguageModel'; context.hasChanges = true; } }); } }); --- File: /ai/packages/codemod/src/codemods/v5/remove-get-ui-text.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Track aliases for getUIText const aliases = new Set<string>(); aliases.add('getUIText'); // Include the original name // Extract leading comments from the original source const originalLines = fileInfo.source.split('\n'); const leadingComments: string[] = []; for (const line of originalLines) { const trimmedLine = line.trim(); if ( trimmedLine.startsWith('//') || trimmedLine.startsWith('/*') || trimmedLine === '' ) { leadingComments.push(line); } else { break; // Stop at the first non-comment, non-empty line } } // Remove getUIText from import statements and track aliases root .find(j.ImportDeclaration) .filter(path => { return ( path.node.source.type === 'StringLiteral' && path.node.source.value === 'ai' ); }) .forEach(path => { if (path.node.specifiers) { const filteredSpecifiers = path.node.specifiers.filter(specifier => { if ( specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name === 'getUIText' ) { // Track the local name (alias) if it exists const localName = specifier.local?.name || 'getUIText'; aliases.add(localName); context.hasChanges = true; return false; // Remove this specifier } return true; // Keep other specifiers }); if (filteredSpecifiers.length === 0) { // Remove entire import if no specifiers left j(path).remove(); } else { // Update with filtered specifiers path.node.specifiers = filteredSpecifiers; } } }); // Replace function calls for getUIText and its aliases root .find(j.CallExpression) .filter(path => { return ( path.node.callee.type === 'Identifier' && aliases.has(path.node.callee.name) && path.node.arguments.length === 1 ); }) .forEach(path => { const argument = path.node.arguments[0]; // Ensure argument is an expression (not a spread element) if (argument.type === 'SpreadElement') { return; // Skip spread elements } // Create the replacement: argument.map(part => (part.type === 'text' ? part.text : '')).join('') const replacement = j.callExpression( j.memberExpression( j.callExpression(j.memberExpression(argument, j.identifier('map')), [ j.arrowFunctionExpression( [j.identifier('part')], j.conditionalExpression( j.binaryExpression( '===', j.memberExpression( j.identifier('part'), j.identifier('type'), ), j.stringLiteral('text'), ), j.memberExpression(j.identifier('part'), j.identifier('text')), j.stringLiteral(''), ), ), ]), j.identifier('join'), ), [j.stringLiteral('')], ); j(path).replaceWith(replacement); context.hasChanges = true; }); // Handle comment preservation if changes were made if (context.hasChanges && leadingComments.length > 0) { // Get the transformed source const transformedSource = root.toSource({ quote: 'single' }); const transformedLines = transformedSource.split('\n'); // Check if the transformed source starts with the same comments let needsCommentRestoration = false; for (let i = 0; i < leadingComments.length; i++) { if ( i >= transformedLines.length || leadingComments[i] !== transformedLines[i] ) { needsCommentRestoration = true; break; } } if (needsCommentRestoration) { // Find the first non-comment line in the transformed source let firstCodeLineIndex = 0; for (let i = 0; i < transformedLines.length; i++) { const trimmedLine = transformedLines[i].trim(); if ( trimmedLine !== '' && !trimmedLine.startsWith('//') && !trimmedLine.startsWith('/*') ) { firstCodeLineIndex = i; break; } } // Rebuild the source with preserved comments const preservedComments = leadingComments.join('\n'); const codeWithoutLeadingComments = transformedLines .slice(firstCodeLineIndex) .join('\n'); // Determine spacing between comments and code let spacingAfterComments = ''; const lastCommentIndex = leadingComments.length - 1; if ( lastCommentIndex >= 0 && leadingComments[lastCommentIndex].trim() === '' ) { // If the last leading comment line is empty, preserve that spacing spacingAfterComments = '\n'; } else { // Otherwise add a single newline to separate comments from code spacingAfterComments = '\n'; } // Override the default transformation result by directly returning the preserved version const preservedResult = `${preservedComments}${spacingAfterComments}${codeWithoutLeadingComments}`; // We need to manually handle the return since createTransformer expects us to modify context.hasChanges // but we want to return a custom result. We'll modify the root to contain our preserved content. const preservedAST = j(preservedResult); context.root .find(j.Program) .replaceWith(preservedAST.find(j.Program).get().node); } } }); --- File: /ai/packages/codemod/src/codemods/v5/remove-openai-compatibility.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Track which createOpenAI identifiers are imported from @ai-sdk/openai const createOpenAIFromOpenAI = new Set<string>(); // Find imports from @ai-sdk/openai and track createOpenAI identifiers root .find(j.ImportDeclaration) .filter(path => { return ( path.node.source.type === 'StringLiteral' && path.node.source.value === '@ai-sdk/openai' ); }) .forEach(path => { if (path.node.specifiers) { path.node.specifiers.forEach(specifier => { if ( specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name === 'createOpenAI' ) { // Track the local name (could be aliased) const localName = specifier.local?.name || 'createOpenAI'; createOpenAIFromOpenAI.add(localName); } }); } }); // Only process createOpenAI calls that were imported from @ai-sdk/openai if (createOpenAIFromOpenAI.size > 0) { // Find createOpenAI function calls root .find(j.CallExpression) .filter(path => { return ( path.node.callee.type === 'Identifier' && createOpenAIFromOpenAI.has(path.node.callee.name) ); }) .forEach(path => { const args = path.node.arguments; // Check if there's an object argument if (args.length > 0 && args[0].type === 'ObjectExpression') { const objectArg = args[0]; // Filter out the compatibility property const filteredProperties = objectArg.properties.filter(prop => { if ( (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier' && prop.key.name === 'compatibility' ) { context.hasChanges = true; return false; // Remove this property } return true; // Keep other properties }); // Update the properties array objectArg.properties = filteredProperties; } }); } }); --- File: /ai/packages/codemod/src/codemods/v5/remove-sendExtraMessageFields.ts --- import { createTransformer } from '../lib/create-transformer'; /** * Removes sendExtraMessageFields property from useChat calls since it's now the default behavior * * Before: * const { messages } = useChat({ * sendExtraMessageFields: true * }); * * After: * const { messages } = useChat({ * }); */ export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Find useChat call expressions root .find(j.CallExpression, { callee: { type: 'Identifier', name: 'useChat', }, }) .forEach(path => { const args = path.node.arguments; if (args.length !== 1 || args[0].type !== 'ObjectExpression') { return; } const configObject = args[0]; let foundSendExtraMessageFields = false; // Find and remove sendExtraMessageFields property configObject.properties = configObject.properties.filter((prop: any) => { if ( (prop.type === 'Property' || prop.type === 'ObjectProperty') && ((prop.key.type === 'Identifier' && prop.key.name === 'sendExtraMessageFields') || (prop.key.type === 'Literal' && prop.key.value === 'sendExtraMessageFields') || (prop.key.type === 'StringLiteral' && prop.key.value === 'sendExtraMessageFields')) ) { foundSendExtraMessageFields = true; context.hasChanges = true; return false; // Remove this property } return true; // Keep other properties }); // If the object is now empty and we removed the property, we can simplify the call if (foundSendExtraMessageFields && configObject.properties.length === 0) { // Keep the empty object for now - user can clean up manually if desired // This preserves the original structure and is safer } }); }); --- File: /ai/packages/codemod/src/codemods/v5/rename-converttocoremessages-to-converttomodelmessages.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Replace import specifiers from 'ai' package root .find(j.ImportDeclaration) .filter(path => { return ( path.node.source.type === 'StringLiteral' && path.node.source.value === 'ai' ); }) .forEach(path => { path.node.specifiers?.forEach(specifier => { if ( specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name === 'convertToCoreMessages' ) { specifier.imported.name = 'convertToModelMessages'; context.hasChanges = true; } }); }); // Replace function calls and identifiers root .find(j.Identifier) .filter(path => { // Only replace identifiers that are not part of import declarations // (those are handled above) and are not property keys const parent = path.parent; return ( path.node.name === 'convertToCoreMessages' && parent.node.type !== 'ImportSpecifier' && !( parent.node.type === 'MemberExpression' && parent.node.property === path.node ) && !(parent.node.type === 'Property' && parent.node.key === path.node) && !( parent.node.type === 'ObjectProperty' && parent.node.key === path.node ) ); }) .forEach(path => { path.node.name = 'convertToModelMessages'; context.hasChanges = true; }); }); --- File: /ai/packages/codemod/src/codemods/v5/rename-core-message-to-model-message.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Replace import specifiers from 'ai' package root .find(j.ImportDeclaration) .filter(path => { return ( path.node.source.type === 'StringLiteral' && path.node.source.value === 'ai' ); }) .forEach(path => { path.node.specifiers?.forEach(specifier => { if ( specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name === 'CoreMessage' ) { specifier.imported.name = 'ModelMessage'; context.hasChanges = true; } }); }); // Replace type references and identifiers root .find(j.Identifier) .filter(path => { // Only replace identifiers that are not part of import declarations // (those are handled above) and are not property keys const parent = path.parent; return ( path.node.name === 'CoreMessage' && parent.node.type !== 'ImportSpecifier' && !( parent.node.type === 'MemberExpression' && parent.node.property === path.node ) && !(parent.node.type === 'Property' && parent.node.key === path.node) && !( parent.node.type === 'ObjectProperty' && parent.node.key === path.node ) ); }) .forEach(path => { path.node.name = 'ModelMessage'; context.hasChanges = true; }); // Replace TypeScript type annotations root .find(j.TSTypeReference) .filter(path => { return ( path.node.typeName.type === 'Identifier' && path.node.typeName.name === 'CoreMessage' ); }) .forEach(path => { if (path.node.typeName.type === 'Identifier') { path.node.typeName.name = 'ModelMessage'; context.hasChanges = true; } }); }); --- File: /ai/packages/codemod/src/codemods/v5/rename-datastream-transform-stream.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; let hasChanges = false; // Find and rename imports root .find(j.ImportDeclaration) .filter(path => { return !!( path.node.source.value === 'ai' && path.node.specifiers && path.node.specifiers.some( spec => spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier' && spec.imported.name === 'DataStreamToSSETransformStream', ) ); }) .forEach(path => { path.node.specifiers?.forEach(spec => { if ( spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier' && spec.imported.name === 'DataStreamToSSETransformStream' ) { spec.imported.name = 'JsonToSseTransformStream'; // If there's no alias, we also need to rename all usages in the file if ( !spec.local || spec.local.name === 'DataStreamToSSETransformStream' ) { if (spec.local) { spec.local.name = 'JsonToSseTransformStream'; } // Rename all type references in the file root .find(j.TSTypeReference) .filter(typePath => { return ( typePath.node.typeName.type === 'Identifier' && typePath.node.typeName.name === 'DataStreamToSSETransformStream' ); }) .forEach(typePath => { if (typePath.node.typeName.type === 'Identifier') { typePath.node.typeName.name = 'JsonToSseTransformStream'; } }); // Also handle any other identifier usages root .find(j.Identifier) .filter(idPath => { return ( idPath.node.name === 'DataStreamToSSETransformStream' && // Make sure it's not part of an import we already handled idPath.parent.node.type !== 'ImportSpecifier' ); }) .forEach(idPath => { idPath.node.name = 'JsonToSseTransformStream'; }); } hasChanges = true; } }); }); if (hasChanges) { context.hasChanges = true; } }); --- File: /ai/packages/codemod/src/codemods/v5/rename-IDGenerator-to-IdGenerator.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Track local names of imported IDGenerator const localNames = new Set<string>(); // Find and update imports from 'ai' root .find(j.ImportDeclaration) .filter(path => path.node.source.value === 'ai') .forEach(path => { path.node.specifiers?.forEach(specifier => { if ( specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name === 'IDGenerator' ) { context.hasChanges = true; // Track the local name (could be aliased) const localName = specifier.local?.name || specifier.imported.name; localNames.add(localName); // Update import name but keep the alias if it exists specifier.imported.name = 'IdGenerator'; // Don't change the local name if it's aliased } }); }); // Find and update all references to the imported type root .find(j.Identifier) .filter(path => { return ( localNames.has(path.node.name) && // Avoid modifying the import statement itself path.parent.node.type !== 'ImportSpecifier' ); }) .forEach(path => { context.hasChanges = true; // Replace with the new name only if it was the original IDGenerator if (path.node.name === 'IDGenerator') { path.node.name = 'IdGenerator'; } }); }); --- File: /ai/packages/codemod/src/codemods/v5/rename-languagemodelv1providermetadata.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; let hasChanges = false; // Find and rename imports root .find(j.ImportDeclaration) .filter(path => { return !!( path.node.source.value === '@ai-sdk/provider' && path.node.specifiers && path.node.specifiers.some( spec => spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier' && spec.imported.name === 'LanguageModelV1ProviderMetadata', ) ); }) .forEach(path => { path.node.specifiers?.forEach(spec => { if ( spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier' && spec.imported.name === 'LanguageModelV1ProviderMetadata' ) { spec.imported.name = 'SharedV2ProviderMetadata'; // If there's an alias, we don't need to change anything else // If there's no alias, we also need to rename all usages in the file if ( !spec.local || spec.local.name === 'LanguageModelV1ProviderMetadata' ) { if (spec.local) { spec.local.name = 'SharedV2ProviderMetadata'; } // Rename all type references in the file root .find(j.TSTypeReference) .filter(typePath => { return ( typePath.node.typeName.type === 'Identifier' && typePath.node.typeName.name === 'LanguageModelV1ProviderMetadata' ); }) .forEach(typePath => { if (typePath.node.typeName.type === 'Identifier') { typePath.node.typeName.name = 'SharedV2ProviderMetadata'; } }); // Also handle any generic type references or other identifier usages root .find(j.Identifier) .filter(idPath => { return ( idPath.node.name === 'LanguageModelV1ProviderMetadata' && // Make sure it's not part of an import we already handled idPath.parent.node.type !== 'ImportSpecifier' ); }) .forEach(idPath => { idPath.node.name = 'SharedV2ProviderMetadata'; }); } hasChanges = true; } }); }); if (hasChanges) { context.hasChanges = true; } }); --- File: /ai/packages/codemod/src/codemods/v5/rename-max-tokens-to-max-output-tokens.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Find and replace object properties named 'maxTokens' with 'maxOutputTokens' root .find(j.ObjectProperty, { key: { name: 'maxTokens' }, }) .forEach(path => { context.hasChanges = true; path.node.key = j.identifier('maxOutputTokens'); }); // Find and replace Property nodes (alternative representation) root .find(j.Property, { key: { name: 'maxTokens' }, shorthand: false, }) .forEach(path => { context.hasChanges = true; path.node.key = j.identifier('maxOutputTokens'); }); // Find and replace shorthand object properties (e.g., { maxTokens }) root .find(j.Property, { key: { name: 'maxTokens' }, shorthand: true, }) .forEach(path => { context.hasChanges = true; path.node.key = j.identifier('maxOutputTokens'); path.node.value = j.identifier('maxOutputTokens'); }); // Replace member expressions (e.g., options.maxTokens) root .find(j.MemberExpression, { property: { name: 'maxTokens' }, }) .forEach(path => { context.hasChanges = true; path.node.property = j.identifier('maxOutputTokens'); }); // Replace destructuring patterns: { maxTokens } = obj root.find(j.ObjectPattern).forEach(path => { let hasPatternChanges = false; path.node.properties.forEach(prop => { // Handle both ObjectProperty and Property nodes in destructuring if ( (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier' && prop.key.name === 'maxTokens' ) { prop.key = j.identifier('maxOutputTokens'); // If it's shorthand, update the value as well if (prop.shorthand && prop.value.type === 'Identifier') { prop.value = j.identifier('maxOutputTokens'); } hasPatternChanges = true; } }); if (hasPatternChanges) { context.hasChanges = true; } }); // Replace TypeScript interface/type properties root .find(j.TSPropertySignature) .filter(path => { const key = path.node.key; return ( (key.type === 'Identifier' && key.name === 'maxTokens') || (key.type === 'StringLiteral' && key.value === 'maxTokens') ); }) .forEach(path => { const key = path.node.key; if (key.type === 'Identifier') { key.name = 'maxOutputTokens'; context.hasChanges = true; } else if (key.type === 'StringLiteral') { key.value = 'maxOutputTokens'; context.hasChanges = true; } }); }); --- File: /ai/packages/codemod/src/codemods/v5/rename-message-to-ui-message.ts --- import { createTransformer } from '../lib/create-transformer'; const renameMappings = { Message: 'UIMessage', CreateMessage: 'CreateUIMessage', }; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Track which identifiers were imported from 'ai' and should be renamed const importedFromAi = new Set<string>(); // Find import declarations from 'ai' and collect the imported names root.find(j.ImportDeclaration).forEach(importPath => { const node = importPath.node; // Check if the source is 'ai' if (node.source.value !== 'ai') return; // Check named imports and rename them const specifiers = node.specifiers?.filter(s => j.ImportSpecifier.check(s)) ?? []; for (const specifier of specifiers) { if ( specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && renameMappings[specifier.imported.name as keyof typeof renameMappings] ) { const oldName = specifier.imported.name; const newName = renameMappings[oldName as keyof typeof renameMappings]; // Track the local name that should be renamed in the code const localName = specifier.local?.name || oldName; importedFromAi.add(localName); // Update the import name specifier.imported.name = newName; // If there's no alias, we also need to update the local name if (!specifier.local || specifier.local.name === oldName) { specifier.local = j.identifier(newName); } context.hasChanges = true; } } }); // Only rename identifiers that were imported from 'ai' if (importedFromAi.size > 0) { root.find(j.Identifier).forEach(identifierPath => { const node = identifierPath.node; // Only rename if this identifier was imported from 'ai' if ( importedFromAi.has(node.name) && renameMappings[node.name as keyof typeof renameMappings] ) { // Skip if this identifier is part of an import declaration (already handled above) const parent = identifierPath.parent; if ( parent && (j.ImportSpecifier.check(parent.node) || j.ImportDefaultSpecifier.check(parent.node) || j.ImportNamespaceSpecifier.check(parent.node)) ) { return; } // Skip if this is a property name in an object (e.g., { Message: something }) if ( parent && j.Property.check(parent.node) && parent.node.key === node ) { return; } // Rename the identifier node.name = renameMappings[node.name as keyof typeof renameMappings]; context.hasChanges = true; } }); // Handle TypeScript type references by manually traversing the AST // This is needed because jscodeshift t.TSTypeReference isn't giving the generic types function traverseAndRename(node: any) { if (!node || typeof node !== 'object') return; // Handle TSTypeReference nodes (for generics like Array<Message>) if ( node.type === 'TSTypeReference' && node.typeName?.type === 'Identifier' ) { const typeName = node.typeName.name; if (importedFromAi.has(typeName)) { node.typeName.name = renameMappings[typeName as keyof typeof renameMappings]; context.hasChanges = true; } } // Recursively traverse all properties for (const key in node) { if ( key === 'type' || key === 'loc' || key === 'start' || key === 'end' || key === 'tokens' || key === 'comments' ) continue; const value = node[key]; if (Array.isArray(value)) { value.forEach(item => traverseAndRename(item)); } else if (value && typeof value === 'object') { traverseAndRename(value); } } } // Start traversal from the root root.find(j.Program).forEach(programPath => { traverseAndRename(programPath.node); }); } }); --- File: /ai/packages/codemod/src/codemods/v5/rename-mime-type-to-media-type.ts --- import type { API, FileInfo } from 'jscodeshift'; import { createTransformer } from '../lib/create-transformer'; const AI_METHODS_WITH_MESSAGES = [ 'generateText', 'streamText', 'generateObject', 'streamObject', 'generateSpeech', 'transcribe', 'streamUI', 'render', ] as const; export default createTransformer( (fileInfo: FileInfo, api: API, options, context) => { const { j, root } = context; // Find all call expressions to AI methods that accept messages root .find(j.CallExpression) .filter(path => { const callee = path.value.callee; if (j.Identifier.check(callee)) { return AI_METHODS_WITH_MESSAGES.includes(callee.name as any); } return false; }) .forEach(path => { const args = path.value.arguments; if (args.length === 0) return; const firstArg = args[0]; if (!j.ObjectExpression.check(firstArg)) return; // Look for the messages property const messagesProperty = firstArg.properties.find(prop => { if (j.Property.check(prop) || j.ObjectProperty.check(prop)) { const key = prop.key; return ( (j.Identifier.check(key) && key.name === 'messages') || (j.Literal.check(key) && key.value === 'messages') || (j.StringLiteral.check(key) && key.value === 'messages') ); } return false; }); if (!messagesProperty) return; const messagesProp = messagesProperty as any; if (!j.ArrayExpression.check(messagesProp.value)) return; // Iterate through messages array messagesProp.value.elements.forEach((messageElement: any) => { if (!j.ObjectExpression.check(messageElement)) return; // Look for content property in message const contentProperty = messageElement.properties.find( (prop: any) => { if (j.Property.check(prop) || j.ObjectProperty.check(prop)) { const key = prop.key; return ( (j.Identifier.check(key) && key.name === 'content') || (j.Literal.check(key) && key.value === 'content') || (j.StringLiteral.check(key) && key.value === 'content') ); } return false; }, ); if (!contentProperty) return; const contentProp = contentProperty as any; if (!j.ArrayExpression.check(contentProp.value)) return; // Iterate through content array contentProp.value.elements.forEach((contentElement: any) => { if (!j.ObjectExpression.check(contentElement)) return; // Check if this is a file content object (has type: 'file') const typeProperty = contentElement.properties.find((prop: any) => { if (j.Property.check(prop) || j.ObjectProperty.check(prop)) { const key = prop.key; const keyName = j.Identifier.check(key) ? key.name : j.Literal.check(key) ? key.value : j.StringLiteral.check(key) ? key.value : null; if (keyName === 'type') { const value = prop.value; return ( (j.Literal.check(value) && value.value === 'file') || (j.StringLiteral.check(value) && value.value === 'file') || (j.Identifier.check(value) && value.name === 'file') ); } } return false; }); if (!typeProperty) return; // Look for mimeType property and rename it to mediaType const mimeTypeProperty = contentElement.properties.find( (prop: any) => { if (j.Property.check(prop) || j.ObjectProperty.check(prop)) { const key = prop.key; return ( (j.Identifier.check(key) && key.name === 'mimeType') || (j.Literal.check(key) && key.value === 'mimeType') || (j.StringLiteral.check(key) && key.value === 'mimeType') ); } return false; }, ); if (mimeTypeProperty) { const mimeTypeProp = mimeTypeProperty as any; // Rename the key if (j.Identifier.check(mimeTypeProp.key)) { mimeTypeProp.key.name = 'mediaType'; } else if (j.Literal.check(mimeTypeProp.key)) { mimeTypeProp.key.value = 'mediaType'; } else if (j.StringLiteral.check(mimeTypeProp.key)) { mimeTypeProp.key.value = 'mediaType'; } context.hasChanges = true; } }); }); }); }, ); --- File: /ai/packages/codemod/src/codemods/v5/rename-pipedatastreamtoresponse-to-pipeuimessagestreamtoresponse.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Replace method calls: result.pipeDataStreamToResponse() -> result.pipeUIMessageStreamToResponse() root .find(j.CallExpression) .filter(path => { return ( path.node.callee.type === 'MemberExpression' && path.node.callee.property.type === 'Identifier' && path.node.callee.property.name === 'pipeDataStreamToResponse' ); }) .forEach(path => { if ( path.node.callee.type === 'MemberExpression' && path.node.callee.property.type === 'Identifier' ) { path.node.callee.property.name = 'pipeUIMessageStreamToResponse'; context.hasChanges = true; } }); // Replace import specifiers from 'ai' package root .find(j.ImportDeclaration) .filter(path => { return ( path.node.source.type === 'StringLiteral' && path.node.source.value === 'ai' ); }) .forEach(path => { path.node.specifiers?.forEach(specifier => { if ( specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name === 'pipeDataStreamToResponse' ) { specifier.imported.name = 'pipeUIMessageStreamToResponse'; context.hasChanges = true; } }); }); // Replace standalone function references root .find(j.Identifier) .filter(path => { // Only replace identifiers that are not property names in member expressions const parent = path.parent; return ( path.node.name === 'pipeDataStreamToResponse' && parent.node.type !== 'ImportSpecifier' && !( parent.node.type === 'MemberExpression' && parent.node.property === path.node ) && !(parent.node.type === 'Property' && parent.node.key === path.node) && !( parent.node.type === 'ObjectProperty' && parent.node.key === path.node ) ); }) .forEach(path => { path.node.name = 'pipeUIMessageStreamToResponse'; context.hasChanges = true; }); }); --- File: /ai/packages/codemod/src/codemods/v5/rename-reasoning-properties.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Replace member expressions (e.g., result.reasoning -> result.reasoningText) root .find(j.MemberExpression) .filter(path => { const property = path.node.property; return ( property.type === 'Identifier' && (property.name === 'reasoning' || property.name === 'reasoningDetails') ); }) .forEach(path => { const property = path.node.property; if (property.type === 'Identifier') { if (property.name === 'reasoning') { property.name = 'reasoningText'; context.hasChanges = true; } else if (property.name === 'reasoningDetails') { property.name = 'reasoning'; context.hasChanges = true; } } }); // Replace string literal property access (e.g., result['reasoning']) root .find(j.MemberExpression) .filter(path => { const property = path.node.property; return ( property.type === 'StringLiteral' && (property.value === 'reasoning' || property.value === 'reasoningDetails') ); }) .forEach(path => { const property = path.node.property; if (property.type === 'StringLiteral') { if (property.value === 'reasoning') { property.value = 'reasoningText'; context.hasChanges = true; } else if (property.value === 'reasoningDetails') { property.value = 'reasoning'; context.hasChanges = true; } } }); // Replace object property keys in object literals root .find(j.ObjectProperty) .filter(path => { const key = path.node.key; return ( (key.type === 'Identifier' && (key.name === 'reasoning' || key.name === 'reasoningDetails')) || (key.type === 'StringLiteral' && (key.value === 'reasoning' || key.value === 'reasoningDetails')) ); }) .forEach(path => { const key = path.node.key; if (key.type === 'Identifier') { if (key.name === 'reasoning') { key.name = 'reasoningText'; context.hasChanges = true; } else if (key.name === 'reasoningDetails') { key.name = 'reasoning'; context.hasChanges = true; } } else if (key.type === 'StringLiteral') { if (key.value === 'reasoning') { key.value = 'reasoningText'; context.hasChanges = true; } else if (key.value === 'reasoningDetails') { key.value = 'reasoning'; context.hasChanges = true; } } }); // Replace destructuring patterns in variable declarations and function parameters root.find(j.ObjectPattern).forEach(path => { path.node.properties.forEach(prop => { if (prop.type === 'ObjectProperty' || prop.type === 'Property') { const key = prop.key; if (key.type === 'Identifier') { if (key.name === 'reasoning') { // If it's shorthand, convert to explicit renaming if (prop.shorthand) { prop.shorthand = false; prop.value = j.identifier('reasoning'); } key.name = 'reasoningText'; context.hasChanges = true; } else if (key.name === 'reasoningDetails') { // If it's shorthand, convert to explicit renaming if (prop.shorthand) { prop.shorthand = false; prop.value = j.identifier('reasoningDetails'); } key.name = 'reasoning'; context.hasChanges = true; } } } }); }); // Replace TypeScript interface/type properties root .find(j.TSPropertySignature) .filter(path => { const key = path.node.key; return ( (key.type === 'Identifier' && (key.name === 'reasoning' || key.name === 'reasoningDetails')) || (key.type === 'StringLiteral' && (key.value === 'reasoning' || key.value === 'reasoningDetails')) ); }) .forEach(path => { const key = path.node.key; if (key.type === 'Identifier') { if (key.name === 'reasoning') { key.name = 'reasoningText'; context.hasChanges = true; } else if (key.name === 'reasoningDetails') { key.name = 'reasoning'; context.hasChanges = true; } } else if (key.type === 'StringLiteral') { if (key.value === 'reasoning') { key.value = 'reasoningText'; context.hasChanges = true; } else if (key.value === 'reasoningDetails') { key.value = 'reasoning'; context.hasChanges = true; } } }); }); --- File: /ai/packages/codemod/src/codemods/v5/rename-reasoning-to-reasoningText.ts --- import { ASTPath } from 'jscodeshift'; import { createTransformer } from '../lib/create-transformer'; /* `steps[].reasoning` is renamed to `steps[].reasoningText` for `{steps}` destructured from `generateText()`. */ export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Collect names of variables that are assigned `steps` from `generateText` const stepsIdentifiers = new Set<string>(); // Step 1: Find destructuring from `generateText()` root .find(j.VariableDeclarator, { id: { type: 'ObjectPattern' }, init: { type: 'AwaitExpression' }, }) .filter(path => { const init = path.node.init; if ( init?.type !== 'AwaitExpression' || init.argument?.type !== 'CallExpression' ) { return false; } const callee = init.argument.callee; return callee.type === 'Identifier' && callee.name === 'generateText'; }) .forEach(path => { if (path.node.id.type !== 'ObjectPattern') return; path.node.id.properties.forEach(prop => { if (prop.type !== 'ObjectProperty') return; if (prop.key.type !== 'Identifier') return; if (prop.key.name === 'steps' && prop.value.type === 'Identifier') { stepsIdentifiers.add(prop.value.name); // usually 'steps' } }); }); if (stepsIdentifiers.size === 0) return null; const pathsAndStepNames: { path: ASTPath; stepName: string; }[] = []; // Step 2a: Look for `forEach` calls on `steps` root .find(j.CallExpression, { callee: { type: 'MemberExpression', property: { type: 'Identifier', name: 'forEach', }, }, }) .filter(path => { const callee = path.node.callee; if (callee.type !== 'MemberExpression') return false; return ( callee.object.type === 'Identifier' && stepsIdentifiers.has(callee.object.name) ); }) .forEach(path => { const arg = path.node.arguments[0]; if (arg.type !== 'ArrowFunctionExpression') return; const param = arg.params[0]; if (param.type !== 'Identifier') return; const stepName = param.name; pathsAndStepNames.push({ path, stepName, }); }); // Step 2b: Look for `for...of` loops on `steps` const forOfPaths = root .find(j.ForOfStatement) .filter(path => { const right = path.node.right; return right.type === 'Identifier' && stepsIdentifiers.has(right.name); }) .forEach(path => { const stepVar = path.node.left; let stepName = null; if (stepVar.type === 'VariableDeclaration') { const declaration = stepVar.declarations[0]; if (declaration.type !== 'VariableDeclarator') return; if (declaration.id.type === 'Identifier') { stepName = declaration.id.name; } } else if (stepVar.type === 'Identifier') { stepName = stepVar.name; } if (!stepName) return; pathsAndStepNames.push({ path, stepName, }); }); // Step 3: Rename `reasoning` to `reasoningText` in the identified paths pathsAndStepNames.forEach(({ path, stepName }) => { j(path) .find(j.MemberExpression, { object: { type: 'Identifier', name: stepName }, property: { type: 'Identifier', name: 'reasoning' }, }) .forEach(memberPath => { if (memberPath.node.property.type !== 'Identifier') return; memberPath.node.property.name = 'reasoningText'; context.hasChanges = true; // Mark that changes were made }); }); }); --- File: /ai/packages/codemod/src/codemods/v5/rename-request-options.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; let hasChanges = false; // Find and rename imports root .find(j.ImportDeclaration) .filter(path => { return !!( path.node.source.value === 'ai' && path.node.specifiers && path.node.specifiers.some( spec => spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier' && spec.imported.name === 'RequestOptions', ) ); }) .forEach(path => { path.node.specifiers?.forEach(spec => { if ( spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier' && spec.imported.name === 'RequestOptions' ) { spec.imported.name = 'CompletionRequestOptions'; // If there's no alias, we also need to rename all usages in the file if (!spec.local || spec.local.name === 'RequestOptions') { if (spec.local) { spec.local.name = 'CompletionRequestOptions'; } // Rename all type references in the file root .find(j.TSTypeReference) .filter(typePath => { return ( typePath.node.typeName.type === 'Identifier' && typePath.node.typeName.name === 'RequestOptions' ); }) .forEach(typePath => { if (typePath.node.typeName.type === 'Identifier') { typePath.node.typeName.name = 'CompletionRequestOptions'; } }); // Also handle any other identifier usages root .find(j.Identifier) .filter(idPath => { return ( idPath.node.name === 'RequestOptions' && // Make sure it's not part of an import we already handled idPath.parent.node.type !== 'ImportSpecifier' ); }) .forEach(idPath => { idPath.node.name = 'CompletionRequestOptions'; }); } hasChanges = true; } }); }); if (hasChanges) { context.hasChanges = true; } }); --- File: /ai/packages/codemod/src/codemods/v5/rename-todatastreamresponse-to-touimessagestreamresponse.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Replace method calls: result.toDataStreamResponse() -> result.toUIMessageStreamResponse() root .find(j.CallExpression) .filter(path => { return ( path.node.callee.type === 'MemberExpression' && path.node.callee.property.type === 'Identifier' && path.node.callee.property.name === 'toDataStreamResponse' ); }) .forEach(path => { if ( path.node.callee.type === 'MemberExpression' && path.node.callee.property.type === 'Identifier' ) { path.node.callee.property.name = 'toUIMessageStreamResponse'; context.hasChanges = true; } }); // Replace standalone function references (if they exist) root .find(j.Identifier) .filter(path => { // Only replace identifiers that are not property names in member expressions const parent = path.parent; return ( path.node.name === 'toDataStreamResponse' && parent.node.type !== 'ImportSpecifier' && !( parent.node.type === 'MemberExpression' && parent.node.property === path.node ) && !(parent.node.type === 'Property' && parent.node.key === path.node) && !( parent.node.type === 'ObjectProperty' && parent.node.key === path.node ) ); }) .forEach(path => { path.node.name = 'toUIMessageStreamResponse'; context.hasChanges = true; }); }); --- File: /ai/packages/codemod/src/codemods/v5/rename-tool-parameters-to-inputschema.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Find tool() function calls and rename parameters to inputSchema root .find(j.CallExpression) .filter(path => { return ( path.node.callee.type === 'Identifier' && path.node.callee.name === 'tool' && path.node.arguments.length > 0 && path.node.arguments[0].type === 'ObjectExpression' ); }) .forEach(path => { const firstArg = path.node.arguments[0]; if (firstArg.type === 'ObjectExpression') { firstArg.properties.forEach(prop => { if ( (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier' && prop.key.name === 'parameters' ) { prop.key.name = 'inputSchema'; context.hasChanges = true; } }); } }); // Also handle object properties in tool definitions within tool objects root .find(j.ObjectProperty) .filter(path => { // Look for tool definitions in objects like { weatherTool: { parameters: ... } } return ( path.node.key.type === 'Identifier' && path.node.key.name === 'parameters' && path.node.value.type !== 'FunctionExpression' && // Not a function parameter path.node.value.type !== 'ArrowFunctionExpression' // Not arrow function parameter ); }) .forEach(path => { // Check if this looks like it's inside a tool definition // We look for sibling properties that suggest this is a tool const parent = path.parent; if (parent && parent.node.type === 'ObjectExpression') { const siblingKeys = parent.node.properties .filter( (prop: any) => (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier', ) .map((prop: any) => prop.key.name); // If we find typical tool properties, assume this is a tool definition if ( siblingKeys.includes('description') || siblingKeys.includes('execute') ) { if (path.node.key.type === 'Identifier') { path.node.key.name = 'inputSchema'; context.hasChanges = true; } } } }); // Handle Property nodes (alternative AST representation) root .find(j.Property) .filter(path => { return ( path.node.key.type === 'Identifier' && path.node.key.name === 'parameters' && path.node.value.type !== 'FunctionExpression' && path.node.value.type !== 'ArrowFunctionExpression' ); }) .forEach(path => { // Check if this looks like it's inside a tool definition const parent = path.parent; if (parent && parent.node.type === 'ObjectExpression') { const siblingKeys = parent.node.properties .filter( (prop: any) => (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier', ) .map((prop: any) => prop.key.name); if ( siblingKeys.includes('description') || siblingKeys.includes('execute') ) { if (path.node.key.type === 'Identifier') { path.node.key.name = 'inputSchema'; context.hasChanges = true; } } } }); }); --- File: /ai/packages/codemod/src/codemods/v5/replace-bedrock-snake-case.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Helper function to convert snake_case to camelCase function snakeToCamel(str: string): string { return str.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase()); } // Helper function to recursively transform snake_case properties in an object function transformBedrockProperties(objExpression: any) { if (objExpression.type !== 'ObjectExpression') return; objExpression.properties.forEach((prop: any) => { if ( (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier' ) { const originalName = prop.key.name; const camelCaseName = snakeToCamel(originalName); // Only transform if the name actually changes (contains snake_case) if (originalName !== camelCaseName && originalName.includes('_')) { context.hasChanges = true; prop.key = j.identifier(camelCaseName); } // Recursively transform nested objects if (prop.value.type === 'ObjectExpression') { transformBedrockProperties(prop.value); } } }); } // Find all ObjectExpression nodes that could contain providerOptions root.find(j.ObjectExpression).forEach(objectPath => { // Look for providerOptions property within this object objectPath.node.properties.forEach((prop: any) => { if ( (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier' && prop.key.name === 'providerOptions' && prop.value.type === 'ObjectExpression' ) { // Found providerOptions, now look for bedrock property prop.value.properties.forEach((providerProp: any) => { if ( (providerProp.type === 'ObjectProperty' || providerProp.type === 'Property') && providerProp.key.type === 'Identifier' && providerProp.key.name === 'bedrock' && providerProp.value.type === 'ObjectExpression' ) { // Found bedrock, transform its properties transformBedrockProperties(providerProp.value); } }); } }); }); }); --- File: /ai/packages/codemod/src/codemods/v5/replace-content-with-parts.ts --- import type { API, FileInfo } from 'jscodeshift'; import { createTransformer } from '../lib/create-transformer'; const AI_METHODS_WITH_MESSAGES = [ 'generateText', 'streamText', 'generateObject', 'streamObject', 'generateSpeech', 'transcribe', 'streamUI', 'render', ] as const; export default createTransformer( (fileInfo: FileInfo, api: API, options, context) => { const { j, root } = context; // Find all call expressions to AI methods that accept messages root .find(j.CallExpression) .filter(path => { const callee = path.value.callee; if (j.Identifier.check(callee)) { return AI_METHODS_WITH_MESSAGES.includes(callee.name as any); } return false; }) .forEach(path => { const args = path.value.arguments; if (args.length === 0) return; const firstArg = args[0]; if (!j.ObjectExpression.check(firstArg)) return; // Look for the messages property const messagesProperty = firstArg.properties.find(prop => { if (j.Property.check(prop) || j.ObjectProperty.check(prop)) { const key = prop.key; return ( (j.Identifier.check(key) && key.name === 'messages') || (j.Literal.check(key) && key.value === 'messages') || (j.StringLiteral.check(key) && key.value === 'messages') ); } return false; }); if (!messagesProperty) return; const messagesProp = messagesProperty as any; if (!j.ArrayExpression.check(messagesProp.value)) return; // Iterate through messages array messagesProp.value.elements.forEach((messageElement: any) => { if (!j.ObjectExpression.check(messageElement)) return; // Look for content property in message const contentPropertyIndex = messageElement.properties.findIndex( (prop: any) => { if (j.Property.check(prop) || j.ObjectProperty.check(prop)) { const key = prop.key; return ( (j.Identifier.check(key) && key.name === 'content') || (j.Literal.check(key) && key.value === 'content') || (j.StringLiteral.check(key) && key.value === 'content') ); } return false; }, ); if (contentPropertyIndex === -1) return; const contentProperty = messageElement.properties[ contentPropertyIndex ] as any; // Check if there's already a parts property const hasPartsProperty = messageElement.properties.some( (prop: any) => { if (j.Property.check(prop) || j.ObjectProperty.check(prop)) { const key = prop.key; return ( (j.Identifier.check(key) && key.name === 'parts') || (j.Literal.check(key) && key.value === 'parts') || (j.StringLiteral.check(key) && key.value === 'parts') ); } return false; }, ); // Don't transform if parts already exists if (hasPartsProperty) return; const contentValue = contentProperty.value; // Create parts array based on content type let partsArray; if ( j.StringLiteral.check(contentValue) || j.Literal.check(contentValue) ) { // String content -> parts: [{ type: 'text', text: content }] partsArray = j.arrayExpression([ j.objectExpression([ j.property('init', j.identifier('type'), j.literal('text')), j.property('init', j.identifier('text'), contentValue), ]), ]); } else if (j.TemplateLiteral.check(contentValue)) { // Template literal -> parts: [{ type: 'text', text: contentValue }] partsArray = j.arrayExpression([ j.objectExpression([ j.property('init', j.identifier('type'), j.literal('text')), j.property('init', j.identifier('text'), contentValue), ]), ]); } else if (j.ArrayExpression.check(contentValue)) { // If content is already an array, assume it's parts-like and use as-is partsArray = contentValue; } else { // For other expressions (variables, function calls, etc.) // Transform to parts: [{ type: 'text', text: content }] partsArray = j.arrayExpression([ j.objectExpression([ j.property('init', j.identifier('type'), j.literal('text')), j.property('init', j.identifier('text'), contentValue), ]), ]); } // Replace content property with parts property const partsProperty = j.property( 'init', j.identifier('parts'), partsArray, ); messageElement.properties[contentPropertyIndex] = partsProperty; context.hasChanges = true; }); }); }, ); --- File: /ai/packages/codemod/src/codemods/v5/replace-experimental-provider-metadata.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; let hasChanges = false; // AI methods that accept providerOptions const aiMethods = [ 'embed', 'embedMany', 'generateText', 'generateObject', 'streamObject', 'streamText', 'generateSpeech', 'transcribe', 'streamUI', 'render', ]; // Find AI method calls and rename experimental_providerMetadata to providerOptions root.find(j.CallExpression).forEach(path => { const { callee, arguments: args } = path.node; // Check if this is an AI method call if ( callee.type === 'Identifier' && aiMethods.includes(callee.name) && args.length > 0 ) { const firstArg = args[0]; // The first argument should be an object with properties if (firstArg.type === 'ObjectExpression') { firstArg.properties.forEach(prop => { const isPropertyType = prop.type === 'Property' || prop.type === 'ObjectProperty'; if (isPropertyType && prop.key && prop.key.type === 'Identifier') { if (prop.key.name === 'experimental_providerMetadata') { prop.key.name = 'providerOptions'; hasChanges = true; } } }); } } }); if (hasChanges) { context.hasChanges = true; } }); --- File: /ai/packages/codemod/src/codemods/v5/replace-generatetext-text-property.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Find member expressions that match: result.text, result1.text, result2.text, etc. // where result is an identifier (not already a member expression) root .find(j.MemberExpression) .filter(path => { const node = path.node; // Must be accessing a property called 'text' if (!j.Identifier.check(node.property) || node.property.name !== 'text') { return false; } // The object must be a simple identifier (not a member expression) if (!j.Identifier.check(node.object)) { return false; } // The identifier should be a common generateText result variable name const commonNames = [ 'result', 'response', 'output', 'data', 'textResult', ]; const objName = node.object.name; return commonNames.includes(objName) || objName.startsWith('result'); }) .forEach(path => { // Transform result.text to result.text.text by creating a new member expression // and setting the object to be the current member expression const newMemberExpression = j.memberExpression( path.node, j.identifier('text'), ); // Replace the entire member expression with the new nested one path.replace(newMemberExpression); context.hasChanges = true; }); }); --- File: /ai/packages/codemod/src/codemods/v5/replace-image-type-with-file-type.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Find object expressions that have a property 'type' with value 'image' root .find(j.ObjectExpression) .filter(path => { const node = path.node; // Check if this object has a 'type' property with value 'image' const typeProperty = node.properties.find(prop => { if ( j.Property.check(prop) && j.Identifier.check(prop.key) && prop.key.name === 'type' && j.Literal.check(prop.value) && prop.value.value === 'image' ) { return true; } return false; }); if (!typeProperty) { return false; } // Additional check: ensure this object also has an 'image' property // to distinguish from other objects that might have type: 'image' const hasImageProperty = node.properties.some(prop => { return ( j.Property.check(prop) && j.Identifier.check(prop.key) && prop.key.name === 'image' ); }); return hasImageProperty; }) .forEach(path => { const node = path.node; // Find and update the 'type' property node.properties.forEach(prop => { if ( j.Property.check(prop) && j.Identifier.check(prop.key) && prop.key.name === 'type' && j.Literal.check(prop.value) && prop.value.value === 'image' ) { prop.value.value = 'file'; context.hasChanges = true; } }); }); }); --- File: /ai/packages/codemod/src/codemods/v5/replace-llamaindex-adapter.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; let needsToUIMessageStreamImport = false; let shouldRemoveLlamaIndexAdapter = false; // Find LlamaIndexAdapter.toDataStreamResponse() calls and replace them root.find(j.CallExpression).forEach(path => { const { callee } = path.node; // Check if this is LlamaIndexAdapter.toDataStreamResponse() if ( callee.type === 'MemberExpression' && callee.object.type === 'Identifier' && callee.object.name === 'LlamaIndexAdapter' && callee.property.type === 'Identifier' && callee.property.name === 'toDataStreamResponse' ) { context.hasChanges = true; needsToUIMessageStreamImport = true; shouldRemoveLlamaIndexAdapter = true; // Replace LlamaIndexAdapter.toDataStreamResponse() with toUIMessageStream() path.node.callee = j.identifier('toUIMessageStream'); } }); if (needsToUIMessageStreamImport) { // Add import for toUIMessageStream from @ai-sdk/llamaindex const llamaIndexImport = j.importDeclaration( [j.importSpecifier(j.identifier('toUIMessageStream'))], j.literal('@ai-sdk/llamaindex'), ); // Find the first import declaration to add the new import after it const firstImport = root.find(j.ImportDeclaration).at(0); if (firstImport.length > 0) { firstImport.insertAfter(llamaIndexImport); } else { // If no imports exist, add at the beginning root.get().node.body.unshift(llamaIndexImport); } } if (shouldRemoveLlamaIndexAdapter) { // Remove or update the 'ai' import that includes LlamaIndexAdapter root .find(j.ImportDeclaration, { source: { value: 'ai' }, }) .forEach(path => { const specifiers = path.node.specifiers || []; // Filter out LlamaIndexAdapter const filteredSpecifiers = specifiers.filter( spec => !( spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier' && spec.imported.name === 'LlamaIndexAdapter' ), ); if (filteredSpecifiers.length === 0) { // Remove the entire import if no other specifiers remain j(path).remove(); } else if (filteredSpecifiers.length !== specifiers.length) { // Update the import to remove LlamaIndexAdapter path.node.specifiers = filteredSpecifiers; } }); } }); --- File: /ai/packages/codemod/src/codemods/v5/replace-oncompletion-with-onfinal.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Find toAIStream method calls and transform onCompletion to onFinal root.find(j.CallExpression).forEach(path => { const { callee } = path.node; // Check if this is a toAIStream method call if ( callee.type === 'MemberExpression' && callee.property.type === 'Identifier' && callee.property.name === 'toAIStream' ) { // Check if there's an object argument const args = path.node.arguments; if (args.length > 0 && args[0].type === 'ObjectExpression') { const objectArg = args[0]; // Look for onCompletion property and rename it to onFinal objectArg.properties.forEach(prop => { if ( (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier' && prop.key.name === 'onCompletion' ) { context.hasChanges = true; prop.key.name = 'onFinal'; } else if ( prop.type === 'ObjectMethod' && prop.key.type === 'Identifier' && prop.key.name === 'onCompletion' ) { context.hasChanges = true; prop.key.name = 'onFinal'; } }); } } }); }); --- File: /ai/packages/codemod/src/codemods/v5/replace-provider-metadata-with-provider-options.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Replace object property keys from providerMetadata to providerOptions root .find(j.ObjectProperty) .filter(path => { const key = path.node.key; return ( (key.type === 'Identifier' && key.name === 'providerMetadata') || (key.type === 'StringLiteral' && key.value === 'providerMetadata') ); }) .forEach(path => { const key = path.node.key; if (key.type === 'Identifier') { key.name = 'providerOptions'; } else if (key.type === 'StringLiteral') { key.value = 'providerOptions'; } context.hasChanges = true; }); // Replace object method keys from providerMetadata to providerOptions root .find(j.ObjectMethod) .filter(path => { const key = path.node.key; return ( (key.type === 'Identifier' && key.name === 'providerMetadata') || (key.type === 'StringLiteral' && key.value === 'providerMetadata') ); }) .forEach(path => { const key = path.node.key; if (key.type === 'Identifier') { key.name = 'providerOptions'; } else if (key.type === 'StringLiteral') { key.value = 'providerOptions'; } context.hasChanges = true; }); // Replace member expressions (e.g., params.providerMetadata) root .find(j.MemberExpression) .filter(path => { const property = path.node.property; return ( (property.type === 'Identifier' && property.name === 'providerMetadata') || (property.type === 'StringLiteral' && property.value === 'providerMetadata') ); }) .forEach(path => { const property = path.node.property; if (property.type === 'Identifier') { property.name = 'providerOptions'; } else if (property.type === 'StringLiteral') { property.value = 'providerOptions'; } context.hasChanges = true; }); // Replace identifier references to providerMetadata variables root .find(j.Identifier) .filter(path => { // Only replace identifiers that are not part of object keys or member expressions // and are not in destructuring patterns (which are handled separately) const parent = path.parent; return ( path.node.name === 'providerMetadata' && parent.node.type !== 'ObjectProperty' && parent.node.type !== 'ObjectMethod' && parent.node.type !== 'MemberExpression' && parent.node.type !== 'TSPropertySignature' && !(parent.node.type === 'Property' && parent.node.key === path.node) ); }) .forEach(path => { path.node.name = 'providerOptions'; context.hasChanges = true; }); // Replace destructuring patterns in function parameters and variable declarations root.find(j.ObjectPattern).forEach(path => { path.node.properties.forEach(prop => { if (prop.type === 'ObjectProperty') { const key = prop.key; if ( (key.type === 'Identifier' && key.name === 'providerMetadata') || (key.type === 'StringLiteral' && key.value === 'providerMetadata') ) { if (key.type === 'Identifier') { key.name = 'providerOptions'; } else if (key.type === 'StringLiteral') { key.value = 'providerOptions'; } context.hasChanges = true; } } else if (prop.type === 'Property') { const key = prop.key; if ( (key.type === 'Identifier' && key.name === 'providerMetadata') || (key.type === 'StringLiteral' && key.value === 'providerMetadata') ) { if (key.type === 'Identifier') { key.name = 'providerOptions'; } else if (key.type === 'StringLiteral') { key.value = 'providerOptions'; } context.hasChanges = true; } } }); }); // Replace type annotations and interface properties root .find(j.TSPropertySignature) .filter(path => { const key = path.node.key; return ( (key.type === 'Identifier' && key.name === 'providerMetadata') || (key.type === 'StringLiteral' && key.value === 'providerMetadata') ); }) .forEach(path => { const key = path.node.key; if (key.type === 'Identifier') { key.name = 'providerOptions'; } else if (key.type === 'StringLiteral') { key.value = 'providerOptions'; } context.hasChanges = true; }); }); --- File: /ai/packages/codemod/src/codemods/v5/replace-rawresponse-with-response.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Methods that return objects with rawResponse const aiMethods = [ 'embed', 'embedMany', 'generateText', 'generateObject', 'streamObject', 'streamText', ]; // Track variable names that store results from AI methods const aiResultVariables = new Set<string>(); // Track destructured rawResponse variable names and their new names const destructuredMapping = new Map<string, string>(); // Find variable declarations from AI method calls root.find(j.VariableDeclarator).forEach(path => { const { id, init } = path.node; // Handle await AI method calls if ( init && init.type === 'AwaitExpression' && init.argument && init.argument.type === 'CallExpression' && init.argument.callee.type === 'Identifier' && aiMethods.includes(init.argument.callee.name) ) { if (id.type === 'Identifier') { aiResultVariables.add(id.name); } else if (id.type === 'ObjectPattern') { // Check if both response and rawResponse are present const hasResponse = id.properties.some( prop => (prop.type === 'Property' || prop.type === 'ObjectProperty') && prop.key.type === 'Identifier' && prop.key.name === 'response', ); let rawResponseVarName: string | null = null; // First pass: find rawResponse and track its variable name id.properties.forEach(prop => { if ( (prop.type === 'Property' || prop.type === 'ObjectProperty') && prop.key.type === 'Identifier' && prop.key.name === 'rawResponse' && prop.value.type === 'Identifier' ) { rawResponseVarName = prop.value.name; } }); // Second pass: handle the transformation if (hasResponse && rawResponseVarName) { // If response already exists, remove rawResponse and map its variable to 'response' id.properties = id.properties.filter( prop => !( (prop.type === 'Property' || prop.type === 'ObjectProperty') && prop.key.type === 'Identifier' && prop.key.name === 'rawResponse' ), ); // Find the response variable name to map rawResponse variable to it const responseProp = id.properties.find( prop => (prop.type === 'Property' || prop.type === 'ObjectProperty') && prop.key.type === 'Identifier' && prop.key.name === 'response', ) as any; if ( responseProp && responseProp.value && responseProp.value.type === 'Identifier' ) { destructuredMapping.set( rawResponseVarName, responseProp.value.name, ); } context.hasChanges = true; } else { // If response doesn't exist, rename rawResponse to response id.properties.forEach(prop => { if ( (prop.type === 'Property' || prop.type === 'ObjectProperty') && prop.key.type === 'Identifier' && prop.key.name === 'rawResponse' && prop.value.type === 'Identifier' ) { context.hasChanges = true; prop.key.name = 'response'; // Track the destructured variable name mapping destructuredMapping.set(prop.value.name, 'response'); } }); } } } // Handle non-await streaming method calls if ( init && init.type === 'CallExpression' && init.callee.type === 'Identifier' && ['streamObject', 'streamText'].includes(init.callee.name) ) { if (id.type === 'Identifier') { aiResultVariables.add(id.name); } else if (id.type === 'ObjectPattern') { // Similar logic for streaming methods const hasResponse = id.properties.some( prop => (prop.type === 'Property' || prop.type === 'ObjectProperty') && prop.key.type === 'Identifier' && prop.key.name === 'response', ); let rawResponseVarName: string | null = null; id.properties.forEach(prop => { if ( (prop.type === 'Property' || prop.type === 'ObjectProperty') && prop.key.type === 'Identifier' && prop.key.name === 'rawResponse' && prop.value.type === 'Identifier' ) { rawResponseVarName = prop.value.name; } }); if (hasResponse && rawResponseVarName) { id.properties = id.properties.filter( prop => !( (prop.type === 'Property' || prop.type === 'ObjectProperty') && prop.key.type === 'Identifier' && prop.key.name === 'rawResponse' ), ); const responseProp = id.properties.find( prop => (prop.type === 'Property' || prop.type === 'ObjectProperty') && prop.key.type === 'Identifier' && prop.key.name === 'response', ) as any; if ( responseProp && responseProp.value && responseProp.value.type === 'Identifier' ) { destructuredMapping.set( rawResponseVarName, responseProp.value.name, ); } context.hasChanges = true; } else { id.properties.forEach(prop => { if ( (prop.type === 'Property' || prop.type === 'ObjectProperty') && prop.key.type === 'Identifier' && prop.key.name === 'rawResponse' && prop.value.type === 'Identifier' ) { context.hasChanges = true; prop.key.name = 'response'; destructuredMapping.set(prop.value.name, 'response'); } }); } } } }); // Find assignment expressions from AI method calls root.find(j.AssignmentExpression).forEach(path => { const { left, right } = path.node; // Handle await AI method calls if ( right.type === 'AwaitExpression' && right.argument && right.argument.type === 'CallExpression' && right.argument.callee.type === 'Identifier' && aiMethods.includes(right.argument.callee.name) ) { if (left.type === 'Identifier') { aiResultVariables.add(left.name); } } // Handle non-await streaming method calls if ( right.type === 'CallExpression' && right.callee.type === 'Identifier' && ['streamObject', 'streamText'].includes(right.callee.name) ) { if (left.type === 'Identifier') { aiResultVariables.add(left.name); } } }); // Transform member expressions accessing .rawResponse on AI result variables root.find(j.MemberExpression).forEach(path => { const { object, property } = path.node; if (property.type === 'Identifier' && property.name === 'rawResponse') { // Direct access: variable.rawResponse if (object.type === 'Identifier' && aiResultVariables.has(object.name)) { context.hasChanges = true; property.name = 'response'; } // Nested access: something.rawResponse where something might be an AI result let currentObj = object; while (currentObj.type === 'MemberExpression') { currentObj = currentObj.object; } if ( currentObj.type === 'Identifier' && aiResultVariables.has(currentObj.name) ) { context.hasChanges = true; property.name = 'response'; } } }); // Transform identifiers that were destructured as rawResponse root.find(j.Identifier).forEach(path => { const varName = path.node.name; if (varName && destructuredMapping.has(varName)) { // Check if this identifier should be transformed const parent = path.parent; // Don't transform if it's a property key in an object if ( (parent.value.type === 'Property' || parent.value.type === 'ObjectProperty') && parent.value.key === path.node ) { return; } // Don't transform if it's in a variable declarator pattern (already handled) if ( parent.value.type === 'VariableDeclarator' && parent.value.id === path.node ) { return; } // Transform the identifier context.hasChanges = true; path.node.name = destructuredMapping.get(varName)!; } }); }); --- File: /ai/packages/codemod/src/codemods/v5/replace-redacted-reasoning-type.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; const foundUsages: Array<{ line: number; context: string; }> = []; // Find string literals that match 'redacted-reasoning' root.find(j.Literal).forEach(path => { if (path.node.value === 'redacted-reasoning') { const lineNumber = path.node.loc?.start?.line || 0; // Try to determine the context (e.g., if condition, switch case, etc.) let context = 'unknown context'; // Check if this is in a comparison (e.g., part.type === 'redacted-reasoning') const parent = path.parent.node; if ( parent && parent.type === 'BinaryExpression' && parent.operator === '===' ) { const left = parent.left; if ( left.type === 'MemberExpression' && left.property.type === 'Identifier' && left.property.name === 'type' ) { context = 'type comparison'; } } // Check if this is in a switch case let currentPath = path.parent; while (currentPath && currentPath.node) { if (currentPath.node.type === 'SwitchCase') { context = 'switch case'; break; } currentPath = currentPath.parent; } foundUsages.push({ line: lineNumber, context: context, }); } }); // Also find template literals that might contain 'redacted-reasoning' root.find(j.TemplateLiteral).forEach(path => { path.node.quasis.forEach(quasi => { if (quasi.value.raw.includes('redacted-reasoning')) { const lineNumber = path.node.loc?.start?.line || 0; foundUsages.push({ line: lineNumber, context: 'template literal', }); } }); }); // Generate helpful messages for found usages if (foundUsages.length > 0) { context.messages.push( `Found ${foundUsages.length} usage(s) of 'redacted-reasoning' part type that need migration:`, ); foundUsages.forEach(usage => { context.messages.push(` Line ${usage.line}: ${usage.context}`); }); context.messages.push(''); context.messages.push('Migration required:'); context.messages.push( ' The redacted-reasoning part type has been removed.', ); context.messages.push(' Use provider-specific metadata instead:'); context.messages.push(''); context.messages.push(' Before:'); context.messages.push(' if (part.type === "redacted-reasoning") {'); context.messages.push(' console.log("<redacted>");'); context.messages.push(' }'); context.messages.push(''); context.messages.push(' After:'); context.messages.push( ' if (part.providerMetadata?.anthropic?.redactedData != null) {', ); context.messages.push(' console.log("<redacted>");'); context.messages.push(' }'); context.messages.push(''); context.messages.push( ' Note: The exact metadata path depends on your provider.', ); context.messages.push( ' Check your provider documentation for the correct path.', ); } }); --- File: /ai/packages/codemod/src/codemods/v5/replace-simulate-streaming.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Track if we need to add imports let needsSimulateStreamingMiddleware = false; let needsWrapLanguageModel = false; // Find provider calls with simulateStreaming: true option root.find(j.CallExpression).forEach(path => { const { callee, arguments: args } = path.node; // Check if this looks like a provider call (function call with 1-2 arguments) if (callee.type === 'Identifier' && args.length >= 1 && args.length <= 2) { const secondArg = args[1]; // Check if second argument is an object with simulateStreaming: true if (secondArg && secondArg.type === 'ObjectExpression') { // Find the simulateStreaming property const simulateStreamingProp = secondArg.properties.find(prop => { return ( (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier' && prop.key.name === 'simulateStreaming' && // jscodeshift parses true/false as Literal // the typescript parser parses true/false as BooleanLiteral (prop.value.type === 'Literal' || prop.value.type === 'BooleanLiteral') && prop.value.value === true ); }); if (simulateStreamingProp) { context.hasChanges = true; needsSimulateStreamingMiddleware = true; needsWrapLanguageModel = true; // Remove simulateStreaming property from the options object const filteredProperties = secondArg.properties.filter( prop => !( (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier' && prop.key.name === 'simulateStreaming' ), ); // Create the new wrapped model call let modelCall; if (filteredProperties.length > 0) { // Keep remaining properties modelCall = j.callExpression(callee, [ args[0], j.objectExpression(filteredProperties), ]); } else { // No other properties, just use the first argument modelCall = j.callExpression(callee, [args[0]]); } // Replace with wrapLanguageModel call const wrappedCall = j.callExpression( j.identifier('wrapLanguageModel'), [ j.objectExpression([ j.property('init', j.identifier('model'), modelCall), j.property( 'init', j.identifier('middleware'), j.callExpression( j.identifier('simulateStreamingMiddleware'), [], ), ), ]), ], ); // Replace the original call path.replace(wrappedCall); } } } }); // Add necessary imports to 'ai' import declaration if (needsSimulateStreamingMiddleware || needsWrapLanguageModel) { root .find(j.ImportDeclaration, { source: { value: 'ai' }, }) .forEach(path => { const existingSpecifiers = path.node.specifiers || []; const importNames = new Set( existingSpecifiers .filter(spec => spec.type === 'ImportSpecifier') .map(spec => spec.imported.name), ); const newImports = []; if ( needsSimulateStreamingMiddleware && !importNames.has('simulateStreamingMiddleware') ) { newImports.push( j.importSpecifier(j.identifier('simulateStreamingMiddleware')), ); } if (needsWrapLanguageModel && !importNames.has('wrapLanguageModel')) { newImports.push(j.importSpecifier(j.identifier('wrapLanguageModel'))); } if (newImports.length > 0) { path.node.specifiers = [...existingSpecifiers, ...newImports]; } }); } }); --- File: /ai/packages/codemod/src/codemods/v5/replace-textdelta-with-text.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Find member expressions that match: delta.textDelta root .find(j.MemberExpression) .filter(path => { const node = path.node; // Must be accessing a property called 'textDelta' if ( !j.Identifier.check(node.property) || node.property.name !== 'textDelta' ) { return false; } // The object must be an identifier called 'delta' if (!j.Identifier.check(node.object) || node.object.name !== 'delta') { return false; } return true; }) .forEach(path => { // Replace delta.textDelta with delta.text const newMemberExpression = j.memberExpression( path.node.object, // delta j.identifier('text'), // text ); path.replace(newMemberExpression); context.hasChanges = true; }); // Replace the case 'text-delta' with case 'text' root .find(j.SwitchCase) .filter(path => { const node = path.node; // Check if the test is a string literal with value 'text-delta' if (j.Literal.check(node.test) && node.test.value === 'text-delta') { return true; } return false; }) .forEach(path => { // Replace 'text-delta' with 'text' path.node.test = j.literal('text'); context.hasChanges = true; }); // Replace string literal 'text-delta' with 'text' in direct comparisons root .find(j.Literal) .filter(path => { const node = path.node; return node.value === 'text-delta'; }) .forEach(path => { // Replace 'text-delta' with 'text' path.node.value = 'text'; context.hasChanges = true; }); }); --- File: /ai/packages/codemod/src/codemods/v5/replace-usage-token-properties.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Replace member expressions: usage.promptTokens -> usage.inputTokens root .find(j.MemberExpression, { property: { name: 'promptTokens' }, }) .forEach(path => { context.hasChanges = true; path.node.property = j.identifier('inputTokens'); }); // Replace member expressions: usage.completionTokens -> usage.outputTokens root .find(j.MemberExpression, { property: { name: 'completionTokens' }, }) .forEach(path => { context.hasChanges = true; path.node.property = j.identifier('outputTokens'); }); // Replace object properties in object literals: { promptTokens: ... } root .find(j.ObjectProperty, { key: { name: 'promptTokens' }, }) .forEach(path => { context.hasChanges = true; path.node.key = j.identifier('inputTokens'); }); // Replace object properties in object literals: { completionTokens: ... } root .find(j.ObjectProperty, { key: { name: 'completionTokens' }, }) .forEach(path => { context.hasChanges = true; path.node.key = j.identifier('outputTokens'); }); // Replace Property nodes in object literals (alternative representation): { promptTokens: ... } root .find(j.Property, { key: { name: 'promptTokens' }, shorthand: false, }) .forEach(path => { context.hasChanges = true; path.node.key = j.identifier('inputTokens'); }); // Replace Property nodes in object literals (alternative representation): { completionTokens: ... } root .find(j.Property, { key: { name: 'completionTokens' }, shorthand: false, }) .forEach(path => { context.hasChanges = true; path.node.key = j.identifier('outputTokens'); }); // Replace shorthand object properties: { promptTokens } -> { inputTokens } root .find(j.Property, { key: { name: 'promptTokens' }, shorthand: true, }) .forEach(path => { context.hasChanges = true; path.node.key = j.identifier('inputTokens'); path.node.value = j.identifier('inputTokens'); }); // Replace shorthand object properties: { completionTokens } -> { outputTokens } root .find(j.Property, { key: { name: 'completionTokens' }, shorthand: true, }) .forEach(path => { context.hasChanges = true; path.node.key = j.identifier('outputTokens'); path.node.value = j.identifier('outputTokens'); }); // Replace destructuring patterns: { promptTokens } = obj root.find(j.ObjectPattern).forEach(path => { let hasPatternChanges = false; path.node.properties.forEach(prop => { // Handle both ObjectProperty and Property nodes in destructuring if ( (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier' ) { if (prop.key.name === 'promptTokens') { prop.key = j.identifier('inputTokens'); // If it's shorthand, update the value as well if (prop.shorthand && prop.value.type === 'Identifier') { prop.value = j.identifier('inputTokens'); } hasPatternChanges = true; } else if (prop.key.name === 'completionTokens') { prop.key = j.identifier('outputTokens'); // If it's shorthand, update the value as well if (prop.shorthand && prop.value.type === 'Identifier') { prop.value = j.identifier('outputTokens'); } hasPatternChanges = true; } } }); if (hasPatternChanges) { context.hasChanges = true; } }); }); --- File: /ai/packages/codemod/src/codemods/v5/require-createIdGenerator-size-argument.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Map to track generator variable names and their createIdGenerator call sites const generatorVariables = new Map<string, any>(); // Map to track generator variable names and the sizes used in calls const generatorSizes = new Map<string, number>(); // First pass: Find all createIdGenerator variable declarations root .find(j.VariableDeclarator) .filter(path => { return Boolean( path.node.init && j.CallExpression.check(path.node.init) && j.Identifier.check(path.node.init.callee) && path.node.init.callee.name === 'createIdGenerator', ); }) .forEach(path => { if (j.Identifier.check(path.node.id)) { generatorVariables.set(path.node.id.name, path); } }); // Also find createIdGenerator assignments (not just declarations) root .find(j.AssignmentExpression) .filter(path => { return ( j.CallExpression.check(path.node.right) && j.Identifier.check(path.node.right.callee) && path.node.right.callee.name === 'createIdGenerator' && j.Identifier.check(path.node.left) ); }) .forEach(path => { if (j.Identifier.check(path.node.left)) { generatorVariables.set(path.node.left.name, path); } }); // Second pass: Find calls to generator variables to extract size arguments root .find(j.CallExpression) .filter(path => { return ( j.Identifier.check(path.node.callee) && generatorVariables.has(path.node.callee.name) ); }) .forEach(path => { const generatorName = (path.node.callee as any).name; const args = path.node.arguments; if ( args.length > 0 && j.Literal.check(args[0]) && typeof args[0].value === 'number' ) { const size = args[0].value as number; // Store the size for this generator (assuming all calls use same size) if (!generatorSizes.has(generatorName)) { generatorSizes.set(generatorName, size); } } }); // Third pass: Update createIdGenerator calls to include size in options generatorVariables.forEach((path, generatorName) => { const size = generatorSizes.get(generatorName); if (size !== undefined) { // Handle both variable declarations and assignments const callExpression = j.VariableDeclarator.check(path.node) ? (path.node.init as any) : (path.node.right as any); // AssignmentExpression const args = callExpression.arguments; if (args.length === 0) { // createIdGenerator() -> createIdGenerator({ size: X }) callExpression.arguments = [ j.objectExpression([ j.objectProperty(j.identifier('size'), j.literal(size)), ]), ]; } else if (args.length === 1 && j.ObjectExpression.check(args[0])) { // createIdGenerator({ prefix: 'msg' }) -> createIdGenerator({ prefix: 'msg', size: X }) const existingProps = args[0].properties; const sizeProperty = j.objectProperty( j.identifier('size'), j.literal(size), ); args[0].properties = [...existingProps, sizeProperty]; } context.hasChanges = true; } }); // Fourth pass: Remove size arguments from generator function calls root .find(j.CallExpression) .filter(path => { return ( j.Identifier.check(path.node.callee) && generatorVariables.has(path.node.callee.name) ); }) .forEach(path => { if (path.node.arguments.length > 0) { path.node.arguments = []; context.hasChanges = true; } }); }); --- File: /ai/packages/codemod/src/codemods/v5/restructure-file-stream-parts.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Track identifiers that are file stream parts in various contexts const fileStreamPartIdentifiers = new Set<string>(); // Track variables that hold streamText results const streamTextResultVariables = new Set<string>(); // Track iterator variables from result.fullStream const fullStreamIteratorVariables = new Set<string>(); // Find streamText imports and track result variables root .find(j.ImportDeclaration) .filter(path => { return ( path.node.source.type === 'StringLiteral' && path.node.source.value === 'ai' ); }) .forEach(path => { if (path.node.specifiers) { path.node.specifiers.forEach(specifier => { if ( specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name === 'streamText' ) { // Find streamText calls and track their result variables root .find(j.CallExpression) .filter(callPath => { return ( callPath.node.callee.type === 'Identifier' && callPath.node.callee.name === 'streamText' ); }) .forEach(callPath => { // Look for variable declarations that store the result const parent = callPath.parent; if ( parent && parent.value && parent.value.type === 'VariableDeclarator' && parent.value.id.type === 'Identifier' ) { streamTextResultVariables.add(parent.value.id.name); } }); } }); } }); // Find for-await-of loops over result.fullStream root.find(j.ForAwaitStatement).forEach(path => { const right = path.node.right; if ( right && right.type === 'MemberExpression' && right.object.type === 'Identifier' && streamTextResultVariables.has(right.object.name) && right.property.type === 'Identifier' && right.property.name === 'fullStream' ) { // Track the iterator variable const left = path.node.left; if (left.type === 'VariableDeclaration' && left.declarations[0]) { const declaration = left.declarations[0]; if ( declaration.type === 'VariableDeclarator' && declaration.id.type === 'Identifier' ) { fullStreamIteratorVariables.add(declaration.id.name); } } } }); // Transform object literals that represent file stream parts root .find(j.ObjectExpression) .filter(path => { // Look for objects with type: 'file' property return path.node.properties.some(prop => { if ( (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier' && prop.key.name === 'type' && prop.value.type === 'StringLiteral' && prop.value.value === 'file' ) { return true; } return false; }); }) .forEach(path => { const properties = path.node.properties; // Find file-related properties (everything except 'type') const fileProperties = properties.filter(prop => { if ( (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier' ) { return prop.key.name !== 'type'; } return false; }); // Only transform if we have file properties to move if (fileProperties.length > 0) { // Create new file object with the file properties const fileObject = j.objectExpression(fileProperties); // Create new properties array with just type and file const typeProperty = properties.find(prop => { return ( (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier' && prop.key.name === 'type' ); }); if (typeProperty) { const newProperties = [ typeProperty, j.objectProperty(j.identifier('file'), fileObject), ]; path.node.properties = newProperties; context.hasChanges = true; } } }); // Find file stream part identifiers from switch cases root .find(j.SwitchCase) .filter(path => { return !!( path.node.test && path.node.test.type === 'StringLiteral' && path.node.test.value === 'file' ); }) .forEach(path => { // Look for the switch expression to find the identifier const switchStatement = path.parent; if ( switchStatement && switchStatement.value && switchStatement.value.type === 'SwitchStatement' && switchStatement.value.discriminant.type === 'MemberExpression' && switchStatement.value.discriminant.property.type === 'Identifier' && switchStatement.value.discriminant.property.name === 'type' && switchStatement.value.discriminant.object.type === 'Identifier' ) { const identifierName = switchStatement.value.discriminant.object.name; fileStreamPartIdentifiers.add(identifierName); // Also check if this identifier is a fullStream iterator if (fullStreamIteratorVariables.has(identifierName)) { fileStreamPartIdentifiers.add(identifierName); } } }); // Find file stream part identifiers from if statements root .find(j.IfStatement) .filter(path => { const test = path.node.test; return ( test && test.type === 'BinaryExpression' && test.operator === '===' && test.left.type === 'MemberExpression' && test.left.property.type === 'Identifier' && test.left.property.name === 'type' && test.right.type === 'StringLiteral' && test.right.value === 'file' ); }) .forEach(path => { const test = path.node.test; if ( test && test.type === 'BinaryExpression' && test.left.type === 'MemberExpression' && test.left.object.type === 'Identifier' ) { fileStreamPartIdentifiers.add(test.left.object.name); } }); // Add fullStream iterator variables to fileStreamPartIdentifiers fullStreamIteratorVariables.forEach(variable => { fileStreamPartIdentifiers.add(variable); }); // Transform property access on identified file stream part variables root .find(j.MemberExpression) .filter(path => { return ( path.node.object.type === 'Identifier' && fileStreamPartIdentifiers.has(path.node.object.name) && path.node.property.type === 'Identifier' && (path.node.property.name === 'mediaType' || path.node.property.name === 'mimeType' || path.node.property.name === 'data' || path.node.property.name === 'base64' || path.node.property.name === 'uint8Array') ); }) .forEach(path => { // Transform part.mediaType to part.file.mediaType path.node.object = j.memberExpression( path.node.object, j.identifier('file'), ); context.hasChanges = true; }); // Transform direct identifier references that should become part.file // This handles cases like presentImages([part]) in file contexts root .find(j.Identifier) .filter(path => { return ( fileStreamPartIdentifiers.has(path.node.name) && // Make sure this is not a property key or already part of a member expression path.parent.value.type !== 'MemberExpression' && path.parent.value.type !== 'Property' && path.parent.value.type !== 'ObjectProperty' ); }) .forEach(path => { // Check if this identifier is in a context where we're dealing with the file object // We need to look for the switch case or if statement context let isInFileContext = false; let currentParent = path.parent; while (currentParent && !isInFileContext) { if ( currentParent.value && currentParent.value.type === 'SwitchCase' && currentParent.value.test && currentParent.value.test.type === 'StringLiteral' && currentParent.value.test.value === 'file' ) { isInFileContext = true; break; } currentParent = currentParent.parent; } // Also check for if statement context if (!isInFileContext) { currentParent = path.parent; while (currentParent && !isInFileContext) { if ( currentParent.value && currentParent.value.type === 'IfStatement' && currentParent.value.test && currentParent.value.test.type === 'BinaryExpression' && currentParent.value.test.operator === '===' && currentParent.value.test.right && currentParent.value.test.right.type === 'StringLiteral' && currentParent.value.test.right.value === 'file' ) { isInFileContext = true; break; } currentParent = currentParent.parent; } } if (isInFileContext) { // Transform part to part.file j(path).replaceWith( j.memberExpression( j.identifier(path.node.name), j.identifier('file'), ), ); context.hasChanges = true; } }); }); --- File: /ai/packages/codemod/src/codemods/v5/restructure-source-stream-parts.ts --- import { createTransformer } from '../lib/create-transformer'; export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; // Track identifiers that are source stream parts in various contexts const sourceStreamPartIdentifiers = new Set<string>(); // Track variables that hold streamText results const streamTextResultVariables = new Set<string>(); // Track iterator variables from result.fullStream const fullStreamIteratorVariables = new Set<string>(); // Find streamText imports and track result variables root .find(j.ImportDeclaration) .filter(path => { return ( path.node.source.type === 'StringLiteral' && path.node.source.value === 'ai' ); }) .forEach(path => { if (path.node.specifiers) { path.node.specifiers.forEach(specifier => { if ( specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name === 'streamText' ) { // Find streamText calls and track their result variables root .find(j.CallExpression) .filter(callPath => { return ( callPath.node.callee.type === 'Identifier' && callPath.node.callee.name === 'streamText' ); }) .forEach(callPath => { // Look for variable declarations that store the result const parent = callPath.parent; if ( parent && parent.value && parent.value.type === 'VariableDeclarator' && parent.value.id.type === 'Identifier' ) { streamTextResultVariables.add(parent.value.id.name); } }); } }); } }); // Find for-await-of loops over result.fullStream root.find(j.ForAwaitStatement).forEach(path => { const right = path.node.right; if ( right && right.type === 'MemberExpression' && right.object.type === 'Identifier' && streamTextResultVariables.has(right.object.name) && right.property.type === 'Identifier' && right.property.name === 'fullStream' ) { // Track the iterator variable const left = path.node.left; if (left.type === 'VariableDeclaration' && left.declarations[0]) { const declaration = left.declarations[0]; if ( declaration.type === 'VariableDeclarator' && declaration.id.type === 'Identifier' ) { fullStreamIteratorVariables.add(declaration.id.name); } } } }); // Transform object literals that have nested source structure root .find(j.ObjectExpression) .filter(path => { // Look for objects with type: 'source' property and a 'source' property containing the nested data return ( path.node.properties.some(prop => { return ( (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier' && prop.key.name === 'type' && prop.value.type === 'StringLiteral' && prop.value.value === 'source' ); }) && path.node.properties.some(prop => { return ( (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier' && prop.key.name === 'source' && prop.value.type === 'ObjectExpression' ); }) ); }) .forEach(path => { const properties = path.node.properties; // Find the type property const typeProperty = properties.find(prop => { return ( (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier' && prop.key.name === 'type' ); }); // Find the source property with nested data const sourceProperty = properties.find(prop => { return ( (prop.type === 'ObjectProperty' || prop.type === 'Property') && prop.key.type === 'Identifier' && prop.key.name === 'source' && prop.value.type === 'ObjectExpression' ); }); if ( typeProperty && sourceProperty && (sourceProperty.type === 'ObjectProperty' || sourceProperty.type === 'Property') && sourceProperty.value.type === 'ObjectExpression' ) { // Extract all properties from the nested source object const nestedSourceProperties = sourceProperty.value.properties; // Create new flat properties array: type + all nested source properties const newProperties = [typeProperty, ...nestedSourceProperties]; path.node.properties = newProperties; context.hasChanges = true; } }); // Find source stream part identifiers from switch cases root .find(j.SwitchCase) .filter(path => { return !!( path.node.test && path.node.test.type === 'StringLiteral' && path.node.test.value === 'source' ); }) .forEach(path => { // Look for the switch expression to find the identifier const switchStatement = path.parent; if ( switchStatement && switchStatement.value && switchStatement.value.type === 'SwitchStatement' && switchStatement.value.discriminant.type === 'MemberExpression' && switchStatement.value.discriminant.property.type === 'Identifier' && switchStatement.value.discriminant.property.name === 'type' && switchStatement.value.discriminant.object.type === 'Identifier' ) { const identifierName = switchStatement.value.discriminant.object.name; sourceStreamPartIdentifiers.add(identifierName); } }); // Find source stream part identifiers from if statements root .find(j.IfStatement) .filter(path => { const test = path.node.test; return ( test && test.type === 'BinaryExpression' && test.operator === '===' && test.left.type === 'MemberExpression' && test.left.property.type === 'Identifier' && test.left.property.name === 'type' && test.right.type === 'StringLiteral' && test.right.value === 'source' ); }) .forEach(path => { const test = path.node.test; if ( test && test.type === 'BinaryExpression' && test.left.type === 'MemberExpression' && test.left.object.type === 'Identifier' ) { sourceStreamPartIdentifiers.add(test.left.object.name); } }); // Add fullStream iterator variables to sourceStreamPartIdentifiers fullStreamIteratorVariables.forEach(variable => { sourceStreamPartIdentifiers.add(variable); }); // Find map callbacks where the parameter represents source stream parts // Look for patterns like: .filter(item => item.type === 'source').map(source => ...) root .find(j.CallExpression) .filter(path => { const callee = path.node.callee; if (callee.type !== 'MemberExpression') return false; if ( callee.property.type !== 'Identifier' || callee.property.name !== 'map' ) return false; if (callee.object.type !== 'CallExpression') return false; const filterCall = callee.object; if (!filterCall.callee || filterCall.callee.type !== 'MemberExpression') return false; if ( filterCall.callee.property.type !== 'Identifier' || filterCall.callee.property.name !== 'filter' ) return false; return true; }) .forEach(path => { const callee = path.node.callee; if ( callee.type === 'MemberExpression' && callee.object.type === 'CallExpression' ) { const filterCall = callee.object; if (filterCall.arguments && filterCall.arguments[0]) { const filterCallback = filterCall.arguments[0]; if (filterCallback.type === 'ArrowFunctionExpression') { const body = filterCallback.body; if ( body.type === 'BinaryExpression' && body.operator === '===' && body.left.type === 'MemberExpression' && body.left.property.type === 'Identifier' && body.left.property.name === 'type' && body.right.type === 'StringLiteral' && body.right.value === 'source' ) { // Found filter for source types, now track the map callback parameter if (path.node.arguments && path.node.arguments[0]) { const mapCallback = path.node.arguments[0]; if ( mapCallback.type === 'ArrowFunctionExpression' && mapCallback.params.length > 0 && mapCallback.params[0].type === 'Identifier' ) { sourceStreamPartIdentifiers.add(mapCallback.params[0].name); } } } } } } }); // Transform nested property access: part.source.sourceType -> part.sourceType root .find(j.MemberExpression) .filter(path => { return ( path.node.object.type === 'MemberExpression' && path.node.object.object.type === 'Identifier' && sourceStreamPartIdentifiers.has(path.node.object.object.name) && path.node.object.property.type === 'Identifier' && path.node.object.property.name === 'source' && path.node.property.type === 'Identifier' && (path.node.property.name === 'sourceType' || path.node.property.name === 'id' || path.node.property.name === 'url' || path.node.property.name === 'title' || path.node.property.name === 'mediaType' || path.node.property.name === 'filename' || path.node.property.name === 'providerMetadata') ); }) .forEach(path => { // Transform part.source.sourceType to part.sourceType if (path.node.object.type === 'MemberExpression') { path.node.object = path.node.object.object; context.hasChanges = true; } }); }); --- File: /ai/packages/codemod/src/codemods/v5/rsc-package.ts --- import { createTransformer } from '../lib/create-transformer'; /* The `ai/rsc` export has been extracted to a separate package `@ai-sdk/rsc` Before: ```jsx import { createStreamableValue } from 'ai/rsc'; ``` After: ```bash pnpm add @ai-sdk/rsc ``` ```jsx import { createStreamableValue } from '@ai-sdk/rsc'; ``` Commit: https://github.com/vercel/ai/pull/5542 */ export default createTransformer((fileInfo, api, options, context) => { const { j, root } = context; root .find(j.ImportDeclaration) .filter(path => path.node.source.value === 'ai/rsc') .forEach(path => { path.node.source.value = '@ai-sdk/rsc'; context.hasChanges = true; }); }); --- File: /ai/packages/codemod/src/lib/transform-options.ts --- export interface TransformOptions { dry?: boolean; print?: boolean; verbose?: boolean; jscodeshift?: string; } --- File: /ai/packages/codemod/src/lib/transform.ts --- import { execSync } from 'child_process'; import debug from 'debug'; import fs from 'fs'; import path from 'path'; import { TransformOptions } from './transform-options'; const log = debug('codemod:transform'); const error = debug('codemod:transform:error'); function getJscodeshift(): string { const localJscodeshift = path.resolve( __dirname, '../../node_modules/.bin/jscodeshift', ); return fs.existsSync(localJscodeshift) ? localJscodeshift : 'jscodeshift'; } function buildCommand( codemodPath: string, targetPath: string, jscodeshift: string, options: TransformOptions, ): string { // Ignoring everything under `.*/` covers `.next/` along with any other // framework build related or otherwise intended-to-be-hidden directories. let command = `${jscodeshift} -t ${codemodPath} ${targetPath} \ --parser tsx \ --ignore-pattern="**/node_modules/**" \ --ignore-pattern="**/.*/**" \ --ignore-pattern="**/dist/**" \ --ignore-pattern="**/build/**" \ --ignore-pattern="**/*.min.js" \ --ignore-pattern="**/*.bundle.js"`; if (options.dry) { command += ' --dry'; } if (options.print) { command += ' --print'; } if (options.verbose) { command += ' --verbose'; } if (options.jscodeshift) { command += ` ${options.jscodeshift}`; } return command; } export type TransformErrors = { transform: string; filename: string; summary: string; }[]; function parseErrors(transform: string, output: string): TransformErrors { const errors: TransformErrors = []; const errorRegex = /ERR (.+) Transformation error/g; const syntaxErrorRegex = /SyntaxError: .+/g; let match; while ((match = errorRegex.exec(output)) !== null) { const filename = match[1]; const syntaxErrorMatch = syntaxErrorRegex.exec(output); if (syntaxErrorMatch) { const summary = syntaxErrorMatch[0]; errors.push({ transform, filename, summary }); } } return errors; } export function transform( codemod: string, source: string, transformOptions: TransformOptions, options: { logStatus: boolean } = { logStatus: true }, ): TransformErrors { if (options.logStatus) { log(`Applying codemod '${codemod}': ${source}`); } const codemodPath = path.resolve(__dirname, `../codemods/${codemod}.js`); const targetPath = path.resolve(source); const jscodeshift = getJscodeshift(); const command = buildCommand( codemodPath, targetPath, jscodeshift, transformOptions, ); const stdout = execSync(command, { encoding: 'utf8', stdio: 'pipe' }); const errors = parseErrors(codemod, stdout); if (options.logStatus && errors.length > 0) { errors.forEach(({ transform, filename, summary }) => { error( `Error applying codemod [codemod=${transform}, path=${filename}, summary=${summary}]`, ); }); } return errors; } --- File: /ai/packages/codemod/src/lib/upgrade.ts --- import debug from 'debug'; import { transform, TransformErrors } from './transform'; import { TransformOptions } from './transform-options'; import { SingleBar, Presets } from 'cli-progress'; const bundle = [ 'v4/remove-ai-stream-methods-from-stream-text-result', 'v4/remove-anthropic-facade', 'v4/remove-await-streamobject', 'v4/remove-await-streamtext', 'v4/remove-deprecated-provider-registry-exports', 'v4/remove-experimental-ai-fn-exports', 'v4/remove-experimental-message-types', 'v4/remove-experimental-streamdata', 'v4/remove-experimental-tool', 'v4/remove-experimental-useassistant', 'v4/remove-google-facade', 'v4/remove-isxxxerror', 'v4/remove-metadata-with-headers', 'v4/remove-mistral-facade', 'v4/remove-openai-facade', 'v4/rename-format-stream-part', 'v4/rename-parse-stream-part', 'v4/replace-baseurl', 'v4/replace-continuation-steps', 'v4/replace-langchain-toaistream', 'v4/replace-nanoid', 'v4/replace-roundtrips-with-maxsteps', 'v4/replace-token-usage-types', 'v4/rewrite-framework-imports', 'v5/flatten-streamtext-file-properties', 'v5/import-LanguageModelV2-from-provider-package', 'v5/migrate-to-data-stream-protocol-v2', 'v5/move-image-model-maxImagesPerCall', 'v5/move-langchain-adapter', 'v5/move-provider-options', 'v5/move-react-to-ai-sdk', 'v5/move-ui-utils-to-ai', 'v5/remove-experimental-wrap-language-model', 'v5/remove-get-ui-text', 'v5/remove-openai-compatibility', 'v5/remove-sendExtraMessageFields', 'v5/rename-converttocoremessages-to-converttomodelmessages', 'v5/rename-core-message-to-model-message', 'v5/rename-datastream-transform-stream', 'v5/rename-IDGenerator-to-IdGenerator', 'v5/rename-languagemodelv1providermetadata', 'v5/rename-max-tokens-to-max-output-tokens', 'v5/rename-message-to-ui-message', 'v5/rename-mime-type-to-media-type', 'v5/rename-pipedatastreamtoresponse-to-pipeuimessagestreamtoresponse', 'v5/rename-reasoning-properties', 'v5/rename-reasoning-to-reasoningText', 'v5/rename-request-options', 'v5/rename-todatastreamresponse-to-touimessagestreamresponse', 'v5/rename-tool-parameters-to-inputschema', 'v5/replace-bedrock-snake-case', 'v5/replace-content-with-parts', 'v5/replace-experimental-provider-metadata', 'v5/replace-generatetext-text-property', 'v5/replace-image-type-with-file-type', 'v5/replace-llamaindex-adapter', 'v5/replace-oncompletion-with-onfinal', 'v5/replace-provider-metadata-with-provider-options', 'v5/replace-rawresponse-with-response', 'v5/replace-redacted-reasoning-type', 'v5/replace-simulate-streaming', 'v5/replace-textdelta-with-text', 'v5/replace-usage-token-properties', 'v5/require-createIdGenerator-size-argument', 'v5/restructure-file-stream-parts', 'v5/restructure-source-stream-parts', 'v5/rsc-package', ]; const log = debug('codemod:upgrade'); const error = debug('codemod:upgrade:error'); // Extract v4 and v5 codemods from the bundle const v4Bundle = bundle.filter(codemod => codemod.startsWith('v4/')); const v5Bundle = bundle.filter(codemod => codemod.startsWith('v5/')); function runCodemods( codemods: string[], options: TransformOptions, versionLabel: string, ) { const cwd = process.cwd(); log(`Starting ${versionLabel} codemods...`); const modCount = codemods.length; const bar = new SingleBar( { format: 'Progress |{bar}| {percentage}% | ETA: {eta}s || {codemod}', hideCursor: true, }, Presets.shades_classic, ); bar.start(modCount, 0, { codemod: 'Starting...' }); const allErrors: TransformErrors = []; for (const [index, codemod] of codemods.entries()) { const errors = transform(codemod, cwd, options, { logStatus: false }); allErrors.push(...errors); bar.increment(1, { codemod }); } bar.stop(); if (allErrors.length > 0) { log( `Some ${versionLabel} codemods did not apply successfully to all files. Details:`, ); allErrors.forEach(({ transform, filename, summary }) => { error(`codemod=${transform}, path=${filename}, summary=${summary}`); }); } log(`${versionLabel} codemods complete.`); } export function upgradeV4(options: TransformOptions) { runCodemods(v4Bundle, options, 'v4'); } export function upgradeV5(options: TransformOptions) { runCodemods(v5Bundle, options, 'v5'); } export function upgrade(options: TransformOptions) { const cwd = process.cwd(); log('Starting upgrade...'); const modCount = bundle.length; const bar = new SingleBar( { format: 'Progress |{bar}| {percentage}% | ETA: {eta}s || {codemod}', hideCursor: true, }, Presets.shades_classic, ); bar.start(modCount, 0, { codemod: 'Starting...' }); const allErrors: TransformErrors = []; for (const [index, codemod] of bundle.entries()) { const errors = transform(codemod, cwd, options, { logStatus: false }); allErrors.push(...errors); bar.increment(1, { codemod }); } bar.stop(); if (allErrors.length > 0) { log('Some codemods did not apply successfully to all files. Details:'); allErrors.forEach(({ transform, filename, summary }) => { error(`codemod=${transform}, path=${filename}, summary=${summary}`); }); } log('Upgrade complete.'); } --- File: /ai/packages/codemod/src/test/__testfixtures__/flatten-streamtext-file-properties.input.ts --- // Test various usages of delta.file.mediaType and delta.file.data import { streamText } from 'ai'; // Mock function for testing function processFile(mediaType: string, data: any) { console.log('Processing file', mediaType, data); } function handleStreamDelta(delta: any) { // Should be transformed: delta.file.mediaType -> delta.mediaType if (delta.file.mediaType === 'application/pdf') { console.log('PDF file detected'); } // Should be transformed: delta.file.data -> delta.data const fileData = delta.file.data; // Should be transformed in expressions const mediaTypeCheck = delta.file.mediaType || 'unknown'; const hasData = !!delta.file.data; // Should be transformed in function calls processFile(delta.file.mediaType, delta.file.data); // Should be transformed in object properties const fileInfo = { type: delta.file.mediaType, content: delta.file.data, size: delta.file.data?.length }; // Should be transformed in conditional expressions const result = delta.file.mediaType ? delta.file.data : null; // Should NOT be transformed - different object name const response = { file: { mediaType: 'test' } }; if (response.file.mediaType) { console.log('This should not change'); } // Should NOT be transformed - different property names if (delta.file.filename) { console.log('This should not change'); } // Should NOT be transformed - not the right pattern if (delta.metadata) { console.log('This should not change'); } } // Additional patterns that should be transformed function anotherHandler(delta: any) { return { mediaType: delta.file.mediaType, data: delta.file.data }; } --- File: /ai/packages/codemod/src/test/__testfixtures__/flatten-streamtext-file-properties.output.ts --- // Test various usages of delta.file.mediaType and delta.file.data import { streamText } from 'ai'; // Mock function for testing function processFile(mediaType: string, data: any) { console.log('Processing file', mediaType, data); } function handleStreamDelta(delta: any) { // Should be transformed: delta.file.mediaType -> delta.mediaType if (delta.mediaType === 'application/pdf') { console.log('PDF file detected'); } // Should be transformed: delta.file.data -> delta.data const fileData = delta.data; // Should be transformed in expressions const mediaTypeCheck = delta.mediaType || 'unknown'; const hasData = !!delta.data; // Should be transformed in function calls processFile(delta.mediaType, delta.data); // Should be transformed in object properties const fileInfo = { type: delta.mediaType, content: delta.data, size: delta.data?.length }; // Should be transformed in conditional expressions const result = delta.mediaType ? delta.data : null; // Should NOT be transformed - different object name const response = { file: { mediaType: 'test' } }; if (response.file.mediaType) { console.log('This should not change'); } // Should NOT be transformed - different property names if (delta.file.filename) { console.log('This should not change'); } // Should NOT be transformed - not the right pattern if (delta.metadata) { console.log('This should not change'); } } // Additional patterns that should be transformed function anotherHandler(delta: any) { return { mediaType: delta.mediaType, data: delta.data }; } --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-ai-stream-methods-from-stream-text-result.input.ts --- // @ts-nocheck import { streamText } from 'ai'; async function handler(req, res) { const stream = streamText({ model: 'gpt-4', prompt: 'Hello' }); const /* WARNING: toAIStream has been removed from streamText. See migration guide at https://ai-sdk.dev/docs/migration-guides */ aiStream = stream.toAIStream(); /* WARNING: pipeAIStreamToResponse has been removed from streamText. See migration guide at https://ai-sdk.dev/docs/migration-guides */ stream.pipeAIStreamToResponse(res); /* WARNING: toAIStreamResponse has been removed from streamText. See migration guide at https://ai-sdk.dev/docs/migration-guides */ return stream.toAIStreamResponse(); } --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-ai-stream-methods-from-stream-text-result.output.ts --- // @ts-nocheck import { streamText } from 'ai'; async function handler(req, res) { const stream = streamText({ model: 'gpt-4', prompt: 'Hello' }); const /* WARNING: toAIStream has been removed from streamText. See migration guide at https://ai-sdk.dev/docs/migration-guides */ /* WARNING: toAIStream has been removed from streamText. See migration guide at https://ai-sdk.dev/docs/migration-guides */ aiStream = stream.toAIStream(); /* WARNING: pipeAIStreamToResponse has been removed from streamText. See migration guide at https://ai-sdk.dev/docs/migration-guides */ /* WARNING: pipeAIStreamToResponse has been removed from streamText. See migration guide at https://ai-sdk.dev/docs/migration-guides */ stream.pipeAIStreamToResponse(res); /* WARNING: toAIStreamResponse has been removed from streamText. See migration guide at https://ai-sdk.dev/docs/migration-guides */ /* WARNING: toAIStreamResponse has been removed from streamText. See migration guide at https://ai-sdk.dev/docs/migration-guides */ return stream.toAIStreamResponse(); } --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-anthropic-facade.input.ts --- // @ts-nocheck import { createAnthropic } from '@ai-sdk/anthropic'; const anthropic = createAnthropic({ apiKey: 'key', baseURL: 'url', headers: { 'custom': 'header' } }); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-anthropic-facade.output.ts --- // @ts-nocheck import { createAnthropic } from '@ai-sdk/anthropic'; const anthropic = createAnthropic({ apiKey: 'key', baseURL: 'url', headers: { 'custom': 'header' } }); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-await-fn-alias.input.ts --- // @ts-nocheck import { streamText as myStreamText } from 'ai'; async function main() { const result = myStreamText({ model: 'gpt-3.5-turbo', prompt: 'Hello, world!', }); console.log(result); } main(); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-await-fn-alias.output.ts --- // @ts-nocheck import { streamText as myStreamText } from 'ai'; async function main() { const result = myStreamText({ model: 'gpt-3.5-turbo', prompt: 'Hello, world!', }); console.log(result); } main(); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-await-fn-other-fn.input.ts --- // @ts-nocheck import { streamText } from 'ai'; async function main() { const result = await otherFunction({ data: 'test', }); const streamResult = streamText({ model: 'gpt-3.5-turbo', prompt: 'Hello again!', }); console.log(result, streamResult); } main(); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-await-fn-other-fn.output.ts --- // @ts-nocheck import { streamText } from 'ai'; async function main() { const result = await otherFunction({ data: 'test', }); const streamResult = streamText({ model: 'gpt-3.5-turbo', prompt: 'Hello again!', }); console.log(result, streamResult); } main(); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-await-fn-other.input.ts --- // @ts-nocheck import { streamText } from 'other-module'; async function main() { const result = await streamText({ model: 'gpt-3.5-turbo', prompt: 'Hello, world!', }); console.log(result); } main(); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-await-fn-other.output.ts --- // @ts-nocheck import { streamText } from 'other-module'; async function main() { const result = await streamText({ model: 'gpt-3.5-turbo', prompt: 'Hello, world!', }); console.log(result); } main(); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-await-fn.input.ts --- // @ts-nocheck import { streamText } from 'ai'; async function main() { const result = streamText({ model: 'gpt-3.5-turbo', prompt: 'Hello, world!', }); console.log(result); } main(); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-await-fn.output.ts --- // @ts-nocheck import { streamText } from 'ai'; async function main() { const result = streamText({ model: 'gpt-3.5-turbo', prompt: 'Hello, world!', }); console.log(result); } main(); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-deprecated-provider-registry-exports.input.ts --- // @ts-nocheck import { Provider, experimental_createProviderRegistry } from 'ai'; function createProvider(): Provider { return { languageModel: () => null, textEmbeddingModel: () => null }; } function createRegistry(): Provider { return experimental_createProviderRegistry({ test: createProvider() }); } const registry: Provider = createRegistry(); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-deprecated-provider-registry-exports.output.ts --- // @ts-nocheck import { Provider, experimental_createProviderRegistry } from 'ai'; function createProvider(): Provider { return { languageModel: () => null, textEmbeddingModel: () => null }; } function createRegistry(): Provider { return experimental_createProviderRegistry({ test: createProvider() }); } const registry: Provider = createRegistry(); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-experimental-ai-fn-exports.input.ts --- // @ts-nocheck import { generateText, streamText, generateObject, streamObject } from 'ai'; async function main() { const result = await generateText({ model: provider('model-name'), prompt: 'Hello', }); const stream = await streamText({ model: provider('model-name'), prompt: 'Hello', }); const obj = await generateObject({ model: provider('model-name'), prompt: 'Hello', }); const objStream = await streamObject({ model: provider('model-name'), prompt: 'Hello', }); } --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-experimental-ai-fn-exports.output.ts --- // @ts-nocheck import { generateText, streamText, generateObject, streamObject } from 'ai'; async function main() { const result = await generateText({ model: provider('model-name'), prompt: 'Hello', }); const stream = await streamText({ model: provider('model-name'), prompt: 'Hello', }); const obj = await generateObject({ model: provider('model-name'), prompt: 'Hello', }); const objStream = await streamObject({ model: provider('model-name'), prompt: 'Hello', }); } --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-experimental-message-types.input.ts --- // @ts-nocheck import { CoreMessage, CoreUserMessage, CoreAssistantMessage, CoreToolMessage } from 'ai'; function processMessage(message: CoreMessage) { console.log(message); } function handleUser(msg: CoreUserMessage) { console.log(msg); } const assistant: CoreAssistantMessage = { role: 'assistant', content: 'Hello' }; type ToolHandler = (msg: CoreToolMessage) => void; --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-experimental-message-types.output.ts --- // @ts-nocheck import { CoreMessage, CoreUserMessage, CoreAssistantMessage, CoreToolMessage } from 'ai'; function processMessage(message: CoreMessage) { console.log(message); } function handleUser(msg: CoreUserMessage) { console.log(msg); } const assistant: CoreAssistantMessage = { role: 'assistant', content: 'Hello' }; type ToolHandler = (msg: CoreToolMessage) => void; --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-experimental-streamdata.input.ts --- // @ts-nocheck import { StreamData } from 'ai'; import { experimental_StreamData as StreamDataLegacy } from 'other-pkg'; // Should rename - class extension class CustomStream extends StreamData { // Custom implementation } // Should rename - type usage const createStream = (): StreamData => { return new StreamData(); }; // Should rename - instance check const isStreamData = (obj: unknown): obj is StreamData => { return obj instanceof StreamData; }; // Should NOT rename - different package class OtherStream extends StreamDataLegacy { // Custom implementation } --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-experimental-streamdata.output.ts --- // @ts-nocheck import { StreamData } from 'ai'; import { experimental_StreamData as StreamDataLegacy } from 'other-pkg'; // Should rename - class extension class CustomStream extends StreamData { // Custom implementation } // Should rename - type usage const createStream = (): StreamData => { return new StreamData(); }; // Should rename - instance check const isStreamData = (obj: unknown): obj is StreamData => { return obj instanceof StreamData; }; // Should NOT rename - different package class OtherStream extends StreamDataLegacy { // Custom implementation } --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-experimental-tool-not-ai.input.ts --- // @ts-nocheck import { ExperimentalTool } from 'not-ai'; interface Config { tool: ExperimentalTool; } const myTool: ExperimentalTool = { description: 'test', parameters: {} }; --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-experimental-tool-not-ai.output.ts --- // @ts-nocheck import { ExperimentalTool } from 'not-ai'; interface Config { tool: ExperimentalTool; } const myTool: ExperimentalTool = { description: 'test', parameters: {} }; --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-experimental-tool.input.ts --- // @ts-nocheck import { CoreTool } from 'ai'; interface Config { tool: CoreTool; } const myTool: CoreTool = { description: 'test', parameters: {} }; --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-experimental-tool.output.ts --- // @ts-nocheck import { CoreTool } from 'ai'; interface Config { tool: CoreTool; } const myTool: CoreTool = { description: 'test', parameters: {} }; --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-experimental-useassistant.input.tsx --- // @ts-nocheck import { useAssistant, Message } from 'ai/react'; export default function Page() { const { status, messages, input, submitMessage, handleInputChange } = useAssistant({ api: '/api/assistant' }); return ( <div className="flex flex-col gap-2"> <div className="p-2">status: {status}</div> <div className="flex flex-col gap-2 p-2"> {messages.map((message: Message) => ( <div key={message.id} className="flex flex-row gap-2"> <div className="w-24 text-zinc-500">{`${message.role}: `}</div> <div className="w-full">{message.content}</div> </div> ))} </div> <form onSubmit={submitMessage} className="fixed bottom-0 w-full p-2"> <input className="w-full p-2 bg-zinc-100" placeholder="Send message..." value={input} onChange={handleInputChange} disabled={status !== 'awaiting_message'} /> </form> </div> ); } --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-experimental-useassistant.output.tsx --- // @ts-nocheck import { useAssistant, Message } from 'ai/react'; export default function Page() { const { status, messages, input, submitMessage, handleInputChange } = useAssistant({ api: '/api/assistant' }); return ( <div className="flex flex-col gap-2"> <div className="p-2">status: {status}</div> <div className="flex flex-col gap-2 p-2"> {messages.map((message: Message) => ( <div key={message.id} className="flex flex-row gap-2"> <div className="w-24 text-zinc-500">{`${message.role}: `}</div> <div className="w-full">{message.content}</div> </div> ))} </div> <form onSubmit={submitMessage} className="fixed bottom-0 w-full p-2"> <input className="w-full p-2 bg-zinc-100" placeholder="Send message..." value={input} onChange={handleInputChange} disabled={status !== 'awaiting_message'} /> </form> </div> ); } --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-google-facade.input.ts --- // @ts-nocheck import { createGoogleGenerativeAI } from '@ai-sdk/google'; const google = createGoogleGenerativeAI({ apiKey: 'key', baseURL: 'url', headers: { 'custom': 'header' } }); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-google-facade.output.ts --- // @ts-nocheck import { createGoogleGenerativeAI } from '@ai-sdk/google'; const google = createGoogleGenerativeAI({ apiKey: 'key', baseURL: 'url', headers: { 'custom': 'header' } }); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-isxxxerror.input.ts --- // @ts-nocheck import { APICallError, TypeValidationError } from 'ai'; import { NoSuchModelError } from '@ai-sdk/provider'; import { CustomError } from 'other-pkg'; if (APICallError.isInstance(error)) { console.log('API Call Error'); } if (TypeValidationError.isInstance(error)) { console.log('Type Validation Error'); } if (NoSuchModelError.isInstance(error)) { console.log('No Such Model Error'); } // Should not transform if (CustomError.isCustomError(error)) { console.log('Custom Error'); } --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-isxxxerror.output.ts --- // @ts-nocheck import { APICallError, TypeValidationError } from 'ai'; import { NoSuchModelError } from '@ai-sdk/provider'; import { CustomError } from 'other-pkg'; if (APICallError.isInstance(error)) { console.log('API Call Error'); } if (TypeValidationError.isInstance(error)) { console.log('Type Validation Error'); } if (NoSuchModelError.isInstance(error)) { console.log('No Such Model Error'); } // Should not transform if (CustomError.isCustomError(error)) { console.log('Custom Error'); } --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-metadata-with-headers.input.ts --- // @ts-nocheck import { generateObject, LanguageModelResponseMetadata } from 'ai'; import { LanguageModelResponseMetadataWithHeaders as MetadataWithHeaders } from 'other-pkg'; // Direct type usage interface Config { metadata: LanguageModelResponseMetadata; } // Usage with generateObject result async function processResult() { const result = await generateObject({ model, schema: schema, prompt: 'test' }); // Save response metadata to variable const metadata: LanguageModelResponseMetadata = result.response; // Destructured access const { headers, timestamp }: LanguageModelResponseMetadata = result.response; // Direct property access const responseData: LanguageModelResponseMetadata = { id: result.response.id, timestamp: result.response.timestamp, headers: result.response.headers }; return { metadata, headers, responseData }; } // Should NOT rename - different package type OtherMetadata = MetadataWithHeaders; // Should rename const data: LanguageModelResponseMetadata = { id: 'test', timestamp: new Date(), headers: {} }; --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-metadata-with-headers.output.ts --- // @ts-nocheck import { generateObject, LanguageModelResponseMetadata } from 'ai'; import { LanguageModelResponseMetadataWithHeaders as MetadataWithHeaders } from 'other-pkg'; // Direct type usage interface Config { metadata: LanguageModelResponseMetadata; } // Usage with generateObject result async function processResult() { const result = await generateObject({ model, schema: schema, prompt: 'test' }); // Save response metadata to variable const metadata: LanguageModelResponseMetadata = result.response; // Destructured access const { headers, timestamp }: LanguageModelResponseMetadata = result.response; // Direct property access const responseData: LanguageModelResponseMetadata = { id: result.response.id, timestamp: result.response.timestamp, headers: result.response.headers }; return { metadata, headers, responseData }; } // Should NOT rename - different package type OtherMetadata = MetadataWithHeaders; // Should rename const data: LanguageModelResponseMetadata = { id: 'test', timestamp: new Date(), headers: {} }; --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-mistral-facade.input.ts --- // @ts-nocheck import { createMistral } from '@ai-sdk/mistral'; const mistral = createMistral({ apiKey: 'key', baseURL: 'url', headers: { 'custom': 'header' } }); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-mistral-facade.output.ts --- // @ts-nocheck import { createMistral } from '@ai-sdk/mistral'; const mistral = createMistral({ apiKey: 'key', baseURL: 'url', headers: { 'custom': 'header' } }); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-openai-facade-as.input.ts --- // @ts-nocheck import OpenAI from 'openai'; import { createOpenAI } from '@ai-sdk/openai'; const client1 = new OpenAI(); const client2 = createOpenAI(); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-openai-facade-as.output.ts --- // @ts-nocheck import OpenAI from 'openai'; import { createOpenAI } from '@ai-sdk/openai'; const client1 = new OpenAI(); const client2 = createOpenAI(); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-openai-facade-corp.input.ts --- // @ts-nocheck import OpenAI from 'openai'; const openaiClient = new OpenAI({ apiKey: 'key2' }); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-openai-facade-corp.output.ts --- // @ts-nocheck import OpenAI from 'openai'; const openaiClient = new OpenAI({ apiKey: 'key2' }); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-openai-facade.input.ts --- // @ts-nocheck import { createOpenAI } from '@ai-sdk/openai'; const openai = createOpenAI({ apiKey: 'key', baseURL: 'url', headers: { 'custom': 'header' } }); --- File: /ai/packages/codemod/src/test/__testfixtures__/remove-openai-facade.output.ts --- // @ts-nocheck import { createOpenAI } from '@ai-sdk/openai'; const openai = createOpenAI({ apiKey: 'key', baseURL: 'url', headers: { 'custom': 'header' } }); --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-converttocoremessages-to-converttomodelmessages.input.ts --- // @ts-nocheck import { convertToCoreMessages, streamText } from 'ai'; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToCoreMessages(messages), }); return result.toDataStreamResponse(); } // Also test function call const coreMessages = convertToCoreMessages(uiMessages); console.log(coreMessages); --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-converttocoremessages-to-converttomodelmessages.output.ts --- // @ts-nocheck import { convertToModelMessages, streamText } from 'ai'; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), }); return result.toDataStreamResponse(); } // Also test function call const coreMessages = convertToModelMessages(uiMessages); console.log(coreMessages); --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-format-stream-part-not-ai.input.ts --- // @ts-nocheck import { formatStreamPart } from 'not-ai'; const response = new Response(formatStreamPart('text', cached)); --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-format-stream-part-not-ai.output.ts --- // @ts-nocheck import { formatStreamPart } from 'not-ai'; const response = new Response(formatStreamPart('text', cached)); --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-format-stream-part.input.ts --- // @ts-nocheck import { formatDataStreamPart } from 'ai'; const response = new Response(formatDataStreamPart('text', cached)); --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-format-stream-part.output.ts --- // @ts-nocheck import { formatDataStreamPart } from 'ai'; const response = new Response(formatDataStreamPart('text', cached)); --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-IDGenerator-to-IdGenerator.input.ts --- // @ts-nocheck import { IDGenerator } from 'ai'; import { type IDGenerator as GeneratorType, someFunction, otherFunction } from 'ai'; // Variable declarations with type annotations const generator1: IDGenerator = createGenerator(); let generator2: IDGenerator; var generator3: IDGenerator = null; // Function declarations with IDGenerator parameters function processGenerator(gen: IDGenerator): void { console.log(gen); } // Arrow functions with IDGenerator parameters const handleGenerator = (gen: IDGenerator): IDGenerator => { return gen; }; // Function return types function createCustomGenerator(): IDGenerator { return {} as IDGenerator; } // Type aliases and interfaces type MyGenerator = IDGenerator; interface GeneratorConfig { generator: IDGenerator; } // Class properties class GeneratorService { private generator: IDGenerator; constructor(gen: IDGenerator) { this.generator = gen; } getGenerator(): IDGenerator { return this.generator; } } // Generic types type GeneratorArray = Array<IDGenerator>; type GeneratorMap = Map<string, IDGenerator>; // Object type annotations const config: { primary: IDGenerator; secondary?: IDGenerator; } = { primary: generator1, secondary: generator2 }; // Should NOT be transformed - different package import { IDGenerator as OtherGenerator } from 'other-package'; const otherGen: OtherGenerator = null; --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-IDGenerator-to-IdGenerator.output.ts --- // @ts-nocheck import { IdGenerator } from 'ai'; import { type IdGenerator as GeneratorType, someFunction, otherFunction } from 'ai'; // Variable declarations with type annotations const generator1: IdGenerator = createGenerator(); let generator2: IdGenerator; var generator3: IdGenerator = null; // Function declarations with IDGenerator parameters function processGenerator(gen: IdGenerator): void { console.log(gen); } // Arrow functions with IDGenerator parameters const handleGenerator = (gen: IdGenerator): IdGenerator => { return gen; }; // Function return types function createCustomGenerator(): IdGenerator { return {} as IdGenerator; } // Type aliases and interfaces type MyGenerator = IdGenerator; interface GeneratorConfig { generator: IdGenerator; } // Class properties class GeneratorService { private generator: IdGenerator; constructor(gen: IdGenerator) { this.generator = gen; } getGenerator(): IdGenerator { return this.generator; } } // Generic types type GeneratorArray = Array<IdGenerator>; type GeneratorMap = Map<string, IdGenerator>; // Object type annotations const config: { primary: IdGenerator; secondary?: IdGenerator; } = { primary: generator1, secondary: generator2 }; // Should NOT be transformed - different package import { IDGenerator as OtherGenerator } from 'other-package'; const otherGen: OtherGenerator = null; --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-message-to-ui-message.input.ts --- // @ts-nocheck import { Message, CreateMessage, generateText } from 'ai'; // Basic usage with type annotations export function handleMessage(message: Message): void { console.log(message.content); } export function createNewMessage(): CreateMessage { return { role: 'user', content: 'Hello world', }; } // Array types export function handleMessages(messages: Message[]): CreateMessage[] { return messages.map(msg => ({ role: msg.role, content: msg.content, })); } // Function parameters and return types export async function processChat( messages: Message[], factory: () => CreateMessage ): Promise<Message> { const newMessage = factory(); const result = await generateText({ model: 'gpt-4', messages: [...messages, newMessage], }); return { role: 'assistant', content: result.text, } as Message; } // Interface extending interface CustomMessage extends Message { timestamp: number; } interface MessageFactory { create(): CreateMessage; } // Type aliases type MessageList = Message[]; type MessageCreator = () => CreateMessage; // Generic types export class MessageHandler<T extends Message> { handle(message: T): CreateMessage { return { role: message.role, content: message.content, }; } } // Union types type MessageOrCreator = Message | CreateMessage; export function genericTest() { const [message, setMessage] = generic<Message | CreateMessage>(null); } --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-message-to-ui-message.output.ts --- // @ts-nocheck import { UIMessage, CreateUIMessage, generateText } from 'ai'; // Basic usage with type annotations export function handleMessage(message: UIMessage): void { console.log(message.content); } export function createNewMessage(): CreateUIMessage { return { role: 'user', content: 'Hello world', }; } // Array types export function handleMessages(messages: UIMessage[]): CreateUIMessage[] { return messages.map(msg => ({ role: msg.role, content: msg.content, })); } // Function parameters and return types export async function processChat( messages: UIMessage[], factory: () => CreateUIMessage ): Promise<UIMessage> { const newMessage = factory(); const result = await generateText({ model: 'gpt-4', messages: [...messages, newMessage], }); return { role: 'assistant', content: result.text, } as UIMessage; } // Interface extending interface CustomMessage extends UIMessage { timestamp: number; } interface MessageFactory { create(): CreateUIMessage; } // Type aliases type MessageList = UIMessage[]; type MessageCreator = () => CreateUIMessage; // Generic types export class MessageHandler<T extends UIMessage> { handle(message: T): CreateUIMessage { return { role: message.role, content: message.content, }; } } // Union types type MessageOrCreator = UIMessage | CreateUIMessage; export function genericTest() { const [message, setMessage] = generic<UIMessage | CreateUIMessage>(null); } --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-parse-stream-part-not-ai.input.ts --- // @ts-nocheck import { parseStreamPart } from 'not-ai'; const result = parseStreamPart(data); --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-parse-stream-part-not-ai.output.ts --- // @ts-nocheck import { parseStreamPart } from 'not-ai'; const result = parseStreamPart(data); --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-parse-stream-part.input.ts --- // @ts-nocheck import { parseDataStreamPart } from 'ai'; const result = parseDataStreamPart(data); --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-parse-stream-part.output.ts --- // @ts-nocheck import { parseDataStreamPart } from 'ai'; const result = parseDataStreamPart(data); --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-pipedatastreamtoresponse-to-pipeuimessagestreamtoresponse.input.ts --- // @ts-nocheck import { streamText, pipeDataStreamToResponse } from 'ai'; export async function handler(req: Request, res: Response) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages, }); // Method call result.pipeDataStreamToResponse(res); // Also test standalone function import pipeDataStreamToResponse(result.toDataStream(), res); } --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-pipedatastreamtoresponse-to-pipeuimessagestreamtoresponse.output.ts --- // @ts-nocheck import { streamText, pipeUIMessageStreamToResponse } from 'ai'; export async function handler(req: Request, res: Response) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages, }); // Method call result.pipeUIMessageStreamToResponse(res); // Also test standalone function import pipeUIMessageStreamToResponse(result.toDataStream(), res); } --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-todatastreamresponse-to-touimessagestreamresponse.input.ts --- // @ts-nocheck import { streamText } from 'ai'; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages, }); return result.toDataStreamResponse(); } // Another example const stream = streamText({ model, prompt }); const response = stream.toDataStreamResponse({ status: 200, headers: { 'custom': 'header' } }); --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-todatastreamresponse-to-touimessagestreamresponse.output.ts --- // @ts-nocheck import { streamText } from 'ai'; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages, }); return result.toUIMessageStreamResponse(); } // Another example const stream = streamText({ model, prompt }); const response = stream.toUIMessageStreamResponse({ status: 200, headers: { 'custom': 'header' } }); --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-tool-parameters-to-inputschema.input.ts --- // @ts-nocheck import { tool } from 'ai'; import { z } from 'zod'; // Using tool() function const weatherTool = tool({ description: 'Get weather for a location', parameters: z.object({ location: z.string(), }), execute: async ({ location }) => { return `Weather in ${location}`; }, }); // In tools object const tools = { weather: { description: 'Get weather information', parameters: z.object({ city: z.string(), }), execute: async ({ city }) => { return `Weather in ${city}`; }, }, search: { description: 'Search the web', parameters: z.object({ query: z.string(), }), execute: async ({ query }) => { return `Search results for ${query}`; }, }, }; --- File: /ai/packages/codemod/src/test/__testfixtures__/rename-tool-parameters-to-inputschema.output.ts --- // @ts-nocheck import { tool } from 'ai'; import { z } from 'zod'; // Using tool() function const weatherTool = tool({ description: 'Get weather for a location', inputSchema: z.object({ location: z.string(), }), execute: async ({ location }) => { return `Weather in ${location}`; }, }); // In tools object const tools = { weather: { description: 'Get weather information', inputSchema: z.object({ city: z.string(), }), execute: async ({ city }) => { return `Weather in ${city}`; }, }, search: { description: 'Search the web', inputSchema: z.object({ query: z.string(), }), execute: async ({ query }) => { return `Search results for ${query}`; }, }, }; --- File: /ai/packages/codemod/src/test/__testfixtures__/replace-baseurl.input.ts --- // @ts-nocheck import { createAnthropic } from '@ai-sdk/anthropic'; import { createOpenAI } from '@ai-sdk/openai'; import { createMistral } from '@ai-sdk/mistral'; const anthropic = createAnthropic({ baseUrl: 'https://api.anthropic.com' }); const openai = createOpenAI({ baseUrl: 'https://api.openai.com' }); const mistral = createMistral({ baseUrl: 'https://api.mistral.ai' }); // Should NOT rename - not in provider creation const config = { baseUrl: 'https://example.com' }; // Should NOT rename - not a provider function someOtherFunction({ baseUrl }) { return baseUrl; } --- File: /ai/packages/codemod/src/test/__testfixtures__/replace-baseurl.output.ts --- // @ts-nocheck import { createAnthropic } from '@ai-sdk/anthropic'; import { createOpenAI } from '@ai-sdk/openai'; import { createMistral } from '@ai-sdk/mistral'; const anthropic = createAnthropic({ baseURL: 'https://api.anthropic.com' }); const openai = createOpenAI({ baseURL: 'https://api.openai.com' }); const mistral = createMistral({ baseURL: 'https://api.mistral.ai' }); // Should NOT rename - not in provider creation const config = { baseUrl: 'https://example.com' }; // Should NOT rename - not a provider function someOtherFunction({ baseUrl }) { return baseUrl; } --- File: /ai/packages/codemod/src/test/__testfixtures__/replace-continuation-steps.input.ts --- // @ts-nocheck import { generateText } from 'ai'; import { generateText as genText } from 'ai'; import { generateText as otherGen } from 'other-pkg'; // Should rename - direct import const result = await generateText({ model, prompt: 'Hello', experimental_continuationSteps: true }); // Should rename - aliased import await genText({ experimental_continuationSteps: false }); // Should NOT rename - different package await otherGen({ experimental_continuationSteps: true }); // Should NOT rename - not in generateText call const config = { experimental_continuationSteps: true }; --- File: /ai/packages/codemod/src/test/__testfixtures__/replace-continuation-steps.output.ts --- // @ts-nocheck import { generateText } from 'ai'; import { generateText as genText } from 'ai'; import { generateText as otherGen } from 'other-pkg'; // Should rename - direct import const result = await generateText({ model, prompt: 'Hello', experimental_continueSteps: true }); // Should rename - aliased import await genText({ experimental_continueSteps: false }); // Should NOT rename - different package await otherGen({ experimental_continuationSteps: true }); // Should NOT rename - not in generateText call const config = { experimental_continuationSteps: true }; --- File: /ai/packages/codemod/src/test/__testfixtures__/replace-langchain-toaistream.input.ts --- // @ts-nocheck import { LangChainAdapter } from 'ai'; import { model } from 'langchain'; const /* WARNING: toAIStream has been removed from streamText. See migration guide at https://ai-sdk.dev/docs/migration-guides */ stream = LangChainAdapter.toAIStream(model.stream(), { onToken: token => console.log(token) }); const response = new Response(stream); --- File: /ai/packages/codemod/src/test/__testfixtures__/replace-langchain-toaistream.output.ts --- // @ts-nocheck import { LangChainAdapter } from 'ai'; import { model } from 'langchain'; const /* WARNING: toAIStream has been removed from streamText. See migration guide at https://ai-sdk.dev/docs/migration-guides */ stream = LangChainAdapter.toDataStream(model.stream(), { onToken: token => console.log(token) }); const response = new Response(stream); --- File: /ai/packages/codemod/src/test/__testfixtures__/replace-nanoid-not-ai.input.ts --- // @ts-nocheck import { generateText } from 'ai'; import { createCohere } from '@ai-sdk/cohere'; import { nanoid } from 'nanoid'; import 'dotenv/config'; async function main() { const cohereProvider = createCohere({ generateId: nanoid, }); const { text } = await generateText({ model: cohereProvider('command-r-plus'), prompt: 'Write a short story about red pandas.', }); console.log(text); } main().catch(console.error); --- File: /ai/packages/codemod/src/test/__testfixtures__/replace-nanoid-not-ai.output.ts --- // @ts-nocheck import { generateText } from 'ai'; import { createCohere } from '@ai-sdk/cohere'; import { nanoid } from 'nanoid'; import 'dotenv/config'; async function main() { const cohereProvider = createCohere({ generateId: nanoid, }); const { text } = await generateText({ model: cohereProvider('command-r-plus'), prompt: 'Write a short story about red pandas.', }); console.log(text); } main().catch(console.error); --- File: /ai/packages/codemod/src/test/__testfixtures__/replace-nanoid.input.ts --- // @ts-nocheck import { generateText } from 'ai'; import { createCohere } from '@ai-sdk/cohere'; import { nanoid } from 'ai'; import 'dotenv/config'; async function main() { const cohereProvider = createCohere({ generateId: nanoid, }); const { text } = await generateText({ model: cohereProvider('command-r-plus'), prompt: 'Write a short story about red pandas.', }); console.log(text); } main().catch(console.error); --- File: /ai/packages/codemod/src/test/__testfixtures__/replace-nanoid.output.ts --- // @ts-nocheck import { generateText } from 'ai'; import { createCohere } from '@ai-sdk/cohere'; import { generateId } from 'ai'; import 'dotenv/config'; async function main() { const cohereProvider = createCohere({ generateId, }); const { text } = await generateText({ model: cohereProvider('command-r-plus'), prompt: 'Write a short story about red pandas.', }); console.log(text); } main().catch(console.error); --- File: /ai/packages/codemod/src/test/__testfixtures__/replace-roundtrips-with-maxsteps.input.ts --- // @ts-nocheck import { generateText, streamText } from 'ai'; // Test generateText with both roundtrip types await generateText({ model, maxToolRoundtrips: 3, maxAutomaticRoundtrips: 2 }); // Test streamText with just maxToolRoundtrips streamText({ model, maxToolRoundtrips: 5 }); // Test streamText with subsequent maxToolRoundtrips streamText({ model, maxToolRoundtrips: 67 }); // Test streamText with no roundtrips streamText({ model }); // Test generateText with just maxAutomaticRoundtrips await generateText({ model, maxAutomaticRoundtrips: 4 }); // Test generateText with subsequent maxToolRoundtrips await generateText({ model, maxToolRoundtrips: 42 }); // Test generateText with no roundtrips await generateText({ model }); // Test property access const result = await generateText({ model }); console.log(result.roundtrips.length); --- File: /ai/packages/codemod/src/test/__testfixtures__/replace-roundtrips-with-maxsteps.output.ts --- // @ts-nocheck import { generateText, streamText } from 'ai'; // Test generateText with both roundtrip types await generateText({ model, maxSteps: 3 }); // Test streamText with just maxToolRoundtrips streamText({ model, maxSteps: 6 }); // Test streamText with subsequent maxToolRoundtrips streamText({ model, maxSteps: 68 }); // Test streamText with no roundtrips streamText({ model }); // Test generateText with just maxAutomaticRoundtrips await generateText({ model, maxSteps: 5 }); // Test generateText with subsequent maxToolRoundtrips await generateText({ model, maxSteps: 43 }); // Test generateText with no roundtrips await generateText({ model }); // Test property access const result = await generateText({ model }); console.log(result.steps.length); --- File: /ai/packages/codemod/src/test/__testfixtures__/replace-token-usage-types.input.ts --- // @ts-nocheck import { TokenUsage, CompletionTokenUsage, EmbeddingTokenUsage } from 'ai'; function recordUsage(usage: TokenUsage) { console.log(usage); } function processEmbedding(usage: EmbeddingTokenUsage) { console.log(usage); } const handler = (data: CompletionTokenUsage) => { console.log(data); }; --- File: /ai/packages/codemod/src/test/__testfixtures__/replace-token-usage-types.output.ts --- // @ts-nocheck import { LanguageModelUsage, EmbeddingModelUsage } from 'ai'; function recordUsage(usage: LanguageModelUsage) { console.log(usage); } function processEmbedding(usage: EmbeddingModelUsage) { console.log(usage); } const handler = (data: LanguageModelUsage) => { console.log(data); }; --- File: /ai/packages/codemod/src/test/__testfixtures__/require-createIdGenerator-size-argument.input.ts --- // @ts-nocheck import { createIdGenerator } from 'ai'; // Case 1: createIdGenerator() with size passed to generator call const generator = createIdGenerator({ prefix: 'msg' }); const id2 = generator(16); // Case 2: createIdGenerator() without options, size passed to generator call const generator2 = createIdGenerator(); const id3 = generator2(32); // Case 3: Multiple calls with same size const generator3 = createIdGenerator({ prefix: 'user' }); const id4 = generator3(8); const id5 = generator3(8); // Case 4: Generator without size argument (should remain unchanged) const generator4 = createIdGenerator({ size: 24 }); const id6 = generator4(); // Case 5: Multiple generators const msgGenerator = createIdGenerator({ prefix: 'msg' }); const userGenerator = createIdGenerator({ prefix: 'user' }); const msgId = msgGenerator(16); const userId = userGenerator(12); // Case 6: Generator assigned later let laterGenerator; laterGenerator = createIdGenerator(); const laterId = laterGenerator(20); --- File: /ai/packages/codemod/src/test/__testfixtures__/require-createIdGenerator-size-argument.output.ts --- // @ts-nocheck import { createIdGenerator } from 'ai'; // Case 1: createIdGenerator() with size passed to generator call const generator = createIdGenerator({ prefix: 'msg', size: 16 }); const id2 = generator(); // Case 2: createIdGenerator() without options, size passed to generator call const generator2 = createIdGenerator({ size: 32 }); const id3 = generator2(); // Case 3: Multiple calls with same size const generator3 = createIdGenerator({ prefix: 'user', size: 8 }); const id4 = generator3(); const id5 = generator3(); // Case 4: Generator without size argument (should remain unchanged) const generator4 = createIdGenerator({ size: 24 }); const id6 = generator4(); // Case 5: Multiple generators const msgGenerator = createIdGenerator({ prefix: 'msg', size: 16 }); const userGenerator = createIdGenerator({ prefix: 'user', size: 12 }); const msgId = msgGenerator(); const userId = userGenerator(); // Case 6: Generator assigned later let laterGenerator; laterGenerator = createIdGenerator({ size: 20 }); const laterId = laterGenerator(); --- File: /ai/packages/codemod/src/test/__testfixtures__/rewrite-framework-imports-solid.input.ts --- // @ts-nocheck import { useChat } from 'ai/solid'; --- File: /ai/packages/codemod/src/test/__testfixtures__/rewrite-framework-imports-solid.output.ts --- // @ts-nocheck import { useChat } from '@ai-sdk/solid'; --- File: /ai/packages/codemod/src/test/__testfixtures__/rewrite-framework-imports-svelte.input.ts --- // @ts-nocheck import { useChat } from 'ai/svelte'; --- File: /ai/packages/codemod/src/test/__testfixtures__/rewrite-framework-imports-svelte.output.ts --- // @ts-nocheck import { useChat } from '@ai-sdk/svelte'; --- File: /ai/packages/codemod/src/test/__testfixtures__/rewrite-framework-imports-vue.input.ts --- // @ts-nocheck import { useChat } from 'ai/vue'; --- File: /ai/packages/codemod/src/test/__testfixtures__/rewrite-framework-imports-vue.output.ts --- // @ts-nocheck import { useChat } from '@ai-sdk/vue'; --- File: /ai/packages/codemod/src/test/create-transformer.test.ts --- import { createTransformer, TransformContext, } from '../codemods/lib/create-transformer'; import { FileInfo, API, JSCodeshift } from 'jscodeshift'; import { describe, test, expect, beforeEach, vi } from 'vitest'; describe('createTransformer', () => { let mockApi: API; let mockFileInfo: FileInfo; let mockOptions: any; let mockReport: ReturnType<typeof vi.fn>; let jscodeshift: JSCodeshift; beforeEach(() => { // Mock the api object mockReport = vi.fn(); jscodeshift = require('jscodeshift'); mockApi = { jscodeshift, j: jscodeshift, stats: () => {}, report: mockReport, } as unknown as API; // Mock the fileInfo object mockFileInfo = { path: 'test-file.js', source: ` const a = 1; console.log(a); `, }; // Mock options if needed mockOptions = {}; }); test('should return transformed code when changes are made', () => { // Create a transformer function that makes changes const transformFn = vi.fn( (fileInfo, api, options, context: TransformContext) => { const { j, root } = context; // Replace all console.log statements with console.error root .find(j.CallExpression, { callee: { object: { name: 'console' }, property: { name: 'log' }, }, }) .forEach(path => { context.hasChanges = true; j(path).replaceWith( j.callExpression( j.memberExpression( j.identifier('console'), j.identifier('error'), ), path.node.arguments, ), ); }); // Add a message to report context.messages.push('Replaced console.log with console.error'); }, ); const transformer = createTransformer(transformFn); const result = transformer(mockFileInfo, mockApi, mockOptions); expect(transformFn).toHaveBeenCalled(); // The result should be a string containing the transformed code expect(typeof result).toBe('string'); expect(result).toContain('console.error(a);'); // The report method should have been called with the message expect(mockReport).toHaveBeenCalledWith( 'Replaced console.log with console.error', ); }); test('should return null when no changes are made', () => { // Create a transformer function that makes no changes const transformFn = vi.fn((fileInfo, api, options, context) => { // Intentionally do nothing }); const transformer = createTransformer(transformFn); const result = transformer(mockFileInfo, mockApi, mockOptions); expect(transformFn).toHaveBeenCalled(); // The result should be null since no changes were made expect(result).toBeNull(); // The report method should not have been called expect(mockReport).not.toHaveBeenCalled(); }); test('should correctly initialize context with j and root', () => { let contextJ: JSCodeshift | undefined; let contextRoot: any; const transformFn = vi.fn((fileInfo, api, options, context) => { contextJ = context.j; contextRoot = context.root; }); const transformer = createTransformer(transformFn); transformer(mockFileInfo, mockApi, mockOptions); expect(transformFn).toHaveBeenCalled(); // Ensure that context.j and context.root are initialized expect(contextJ).toBe(jscodeshift); expect(contextRoot).toBeDefined(); }); test('should pass fileInfo, api, options, and context to the transform function', () => { const transformFn = vi.fn(); const transformer = createTransformer(transformFn); transformer(mockFileInfo, mockApi, mockOptions); expect(transformFn).toHaveBeenCalledWith( mockFileInfo, mockApi, mockOptions, expect.objectContaining({ j: jscodeshift, root: expect.anything(), hasChanges: false, messages: [], }), ); }); test('should handle multiple messages', () => { const transformFn = vi.fn((fileInfo, api, options, context) => { context.messages.push('First message'); context.messages.push('Second message'); }); const transformer = createTransformer(transformFn); transformer(mockFileInfo, mockApi, mockOptions); // The report method should have been called with both messages expect(mockReport).toHaveBeenCalledTimes(2); expect(mockReport).toHaveBeenCalledWith('First message'); expect(mockReport).toHaveBeenCalledWith('Second message'); }); }); --- File: /ai/packages/codemod/src/test/flatten-streamtext-file-properties.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v5/flatten-streamtext-file-properties'; import { testTransform } from './test-utils'; describe('flatten-streamtext-file-properties', () => { it('transforms correctly', () => { testTransform(transformer, 'flatten-streamtext-file-properties'); }); }); --- File: /ai/packages/codemod/src/test/jscodeshift-testUtils.d.ts --- declare module 'jscodeshift/dist/testUtils' { export function defineTest( dirName: string, transformName: string, options?: any, testFilePrefix?: string, testOptions?: any, ): void; } --- File: /ai/packages/codemod/src/test/remove-ai-stream-methods-from-stream-text-result.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/remove-ai-stream-methods-from-stream-text-result'; import { testTransform } from './test-utils'; describe('remove-ai-stream-methods-from-stream-text-result', () => { it('transforms correctly', () => { testTransform( transformer, 'remove-ai-stream-methods-from-stream-text-result', ); }); }); --- File: /ai/packages/codemod/src/test/remove-anthropic-facade.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/remove-anthropic-facade'; import { testTransform } from './test-utils'; describe('remove-anthropic-facade', () => { it('transforms correctly', () => { testTransform(transformer, 'remove-anthropic-facade'); }); }); --- File: /ai/packages/codemod/src/test/remove-await-fn.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/remove-await-streamtext'; import { testTransform } from './test-utils'; describe('remove-await-fn', () => { it('transforms correctly', () => { testTransform(transformer, 'remove-await-fn'); }); it('transforms correctly with aliasing', () => { testTransform(transformer, 'remove-await-fn-alias'); }); it('does not transform when imported from other package', () => { testTransform(transformer, 'remove-await-fn-other'); }); it('does not transform on other function', () => { testTransform(transformer, 'remove-await-fn-other-fn'); }); }); --- File: /ai/packages/codemod/src/test/remove-deprecated-provider-registry-exports.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/remove-deprecated-provider-registry-exports'; import { testTransform } from './test-utils'; describe('remove-deprecated-provider-registry-exports', () => { it('transforms correctly', () => { testTransform(transformer, 'remove-deprecated-provider-registry-exports'); }); }); --- File: /ai/packages/codemod/src/test/remove-experimental-ai-fn-exports.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/remove-experimental-ai-fn-exports'; import { testTransform } from './test-utils'; describe('remove-experimental-ai-fn-exports', () => { it('transforms correctly', () => { testTransform(transformer, 'remove-experimental-ai-fn-exports'); }); }); --- File: /ai/packages/codemod/src/test/remove-experimental-message-types.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/remove-experimental-message-types'; import { testTransform } from './test-utils'; describe('remove-experimental-message-types', () => { it('transforms correctly', () => { testTransform(transformer, 'remove-experimental-message-types'); }); }); --- File: /ai/packages/codemod/src/test/remove-experimental-streamdata.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/remove-experimental-streamdata'; import { testTransform } from './test-utils'; describe('remove-experimental-streamdata', () => { it('transforms correctly', () => { testTransform(transformer, 'remove-experimental-streamdata'); }); }); --- File: /ai/packages/codemod/src/test/remove-experimental-tool.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/remove-experimental-tool'; import { testTransform } from './test-utils'; describe('remove-experimental-tool', () => { it('transforms correctly', () => { testTransform(transformer, 'remove-experimental-tool'); }); it('does not transform from other packages', () => { testTransform(transformer, 'remove-experimental-tool-not-ai'); }); }); --- File: /ai/packages/codemod/src/test/remove-experimental-useassistant.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/remove-experimental-useassistant'; import { testTransform } from './test-utils'; describe('remove-experimental-useassistant', () => { it('transforms correctly', () => { testTransform(transformer, 'remove-experimental-useassistant'); }); }); --- File: /ai/packages/codemod/src/test/remove-google-facade.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/remove-google-facade'; import { testTransform } from './test-utils'; describe('remove-google-facade', () => { it('transforms correctly', () => { testTransform(transformer, 'remove-google-facade'); }); }); --- File: /ai/packages/codemod/src/test/remove-isxxxerror.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/remove-isxxxerror'; import { testTransform } from './test-utils'; describe('remove-isxxxerror', () => { it('transforms correctly', () => { testTransform(transformer, 'remove-isxxxerror'); }); }); --- File: /ai/packages/codemod/src/test/remove-metadata-with-headers.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/remove-metadata-with-headers'; import { testTransform } from './test-utils'; describe('remove-metadata-with-headers', () => { it('transforms correctly', () => { testTransform(transformer, 'remove-metadata-with-headers'); }); }); --- File: /ai/packages/codemod/src/test/remove-mistral-facade.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/remove-mistral-facade'; import { testTransform } from './test-utils'; describe('remove-mistral-facade', () => { it('transforms correctly', () => { testTransform(transformer, 'remove-mistral-facade'); }); }); --- File: /ai/packages/codemod/src/test/remove-openai-facade.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/remove-openai-facade'; import { testTransform } from './test-utils'; describe('remove-openai-facade', () => { it('transforms correctly', () => { testTransform(transformer, 'remove-openai-facade'); }); it('does not transform openai corporate', () => { testTransform(transformer, 'remove-openai-facade-corp'); }); it('does transform openai import with as', () => { testTransform(transformer, 'remove-openai-facade-as'); }); }); --- File: /ai/packages/codemod/src/test/rename-converttocoremessages-to-converttomodelmessages.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v5/rename-converttocoremessages-to-converttomodelmessages'; import { testTransform } from './test-utils'; describe('rename-converttocoremessages-to-converttomodelmessages', () => { it('transforms correctly', () => { testTransform( transformer, 'rename-converttocoremessages-to-converttomodelmessages', ); }); }); --- File: /ai/packages/codemod/src/test/rename-format-stream-part.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/rename-format-stream-part'; import { testTransform } from './test-utils'; describe('rename-format-stream-part', () => { it('transforms correctly', () => { testTransform(transformer, 'rename-format-stream-part'); }); it('does not transform from other packages', () => { testTransform(transformer, 'rename-format-stream-part-not-ai'); }); }); --- File: /ai/packages/codemod/src/test/rename-IDGenerator-to-IdGenerator.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v5/rename-IDGenerator-to-IdGenerator'; import { testTransform } from './test-utils'; describe('rename-IDGenerator-to-IdGenerator', () => { it('transforms correctly', () => { testTransform(transformer, 'rename-IDGenerator-to-IdGenerator'); }); }); --- File: /ai/packages/codemod/src/test/rename-message-to-ui-message.test.ts --- import { join } from 'path'; import { readFileSync } from 'fs'; import jscodeshift from 'jscodeshift'; import transform from '../codemods/v5/rename-message-to-ui-message'; function trim(str: string) { return str.replace(/^\s+|\s+$/, ''); } describe('rename-message-to-ui-message', () => { it('transforms Message and CreateMessage to UIMessage and CreateUIMessage', () => { const input = readFileSync( join(__dirname, '__testfixtures__/rename-message-to-ui-message.input.ts'), 'utf8', ); const expected = readFileSync( join( __dirname, '__testfixtures__/rename-message-to-ui-message.output.ts', ), 'utf8', ); const j = jscodeshift.withParser('tsx'); const result = transform( { source: input, path: 'test.ts' }, { jscodeshift: j, j: j, stats: () => {}, report: () => {}, }, {}, ); expect(trim(result || '')).toEqual(trim(expected)); }); }); --- File: /ai/packages/codemod/src/test/rename-parse-stream-part.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/rename-parse-stream-part'; import { testTransform } from './test-utils'; describe('rename-parse-stream-part', () => { it('transforms correctly', () => { testTransform(transformer, 'rename-parse-stream-part'); }); it('does not transform from other packages', () => { testTransform(transformer, 'rename-parse-stream-part-not-ai'); }); }); --- File: /ai/packages/codemod/src/test/rename-pipedatastreamtoresponse-to-pipeuimessagestreamtoresponse.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v5/rename-pipedatastreamtoresponse-to-pipeuimessagestreamtoresponse'; import { testTransform } from './test-utils'; describe('rename-pipedatastreamtoresponse-to-pipeuimessagestreamtoresponse', () => { it('transforms correctly', () => { testTransform( transformer, 'rename-pipedatastreamtoresponse-to-pipeuimessagestreamtoresponse', ); }); }); --- File: /ai/packages/codemod/src/test/rename-todatastreamresponse-to-touimessagestreamresponse.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v5/rename-todatastreamresponse-to-touimessagestreamresponse'; import { testTransform } from './test-utils'; describe('rename-todatastreamresponse-to-touimessagestreamresponse', () => { it('transforms correctly', () => { testTransform( transformer, 'rename-todatastreamresponse-to-touimessagestreamresponse', ); }); }); --- File: /ai/packages/codemod/src/test/rename-tool-parameters-to-inputschema.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v5/rename-tool-parameters-to-inputschema'; import { testTransform } from './test-utils'; describe('rename-tool-parameters-to-inputschema', () => { it('transforms correctly', () => { testTransform(transformer, 'rename-tool-parameters-to-inputschema'); }); }); --- File: /ai/packages/codemod/src/test/replace-baseurl.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/replace-baseurl'; import { testTransform } from './test-utils'; describe('replace-baseurl', () => { it('transforms correctly', () => { testTransform(transformer, 'replace-baseurl'); }); }); --- File: /ai/packages/codemod/src/test/replace-continuation-steps.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/replace-continuation-steps'; import { testTransform } from './test-utils'; describe('replace-continuation-steps', () => { it('transforms correctly', () => { testTransform(transformer, 'replace-continuation-steps'); }); }); --- File: /ai/packages/codemod/src/test/replace-langchain-toaistream.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/replace-langchain-toaistream'; import { testTransform } from './test-utils'; describe('replace-langchain-toaistream', () => { it('transforms correctly', () => { testTransform(transformer, 'replace-langchain-toaistream'); }); }); --- File: /ai/packages/codemod/src/test/replace-nanoid.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/replace-nanoid'; import { testTransform } from './test-utils'; describe('replace-nanoid', () => { it('transforms correctly', () => { testTransform(transformer, 'replace-nanoid'); }); it('does not transform when imported from other packages', () => { testTransform(transformer, 'replace-nanoid-not-ai'); }); }); --- File: /ai/packages/codemod/src/test/replace-roundtrips-with-maxsteps.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/replace-roundtrips-with-maxsteps'; import { testTransform } from './test-utils'; describe('replace-roundtrips-with-maxsteps', () => { it('transforms correctly', () => { testTransform(transformer, 'replace-roundtrips-with-maxsteps'); }); }); --- File: /ai/packages/codemod/src/test/replace-token-usage-types.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/replace-token-usage-types'; import { testTransform } from './test-utils'; describe('replace-token-usage-types', () => { it('transforms correctly', () => { testTransform(transformer, 'replace-token-usage-types'); }); }); --- File: /ai/packages/codemod/src/test/require-createIdGenerator-size-argument.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v5/require-createIdGenerator-size-argument'; import { testTransform } from './test-utils'; describe('require-createIdGenerator-size-argument', () => { it('transforms correctly', () => { testTransform(transformer, 'require-createIdGenerator-size-argument'); }); }); --- File: /ai/packages/codemod/src/test/rewrite-framework-imports.test.ts --- import { describe, it } from 'vitest'; import transformer from '../codemods/v4/rewrite-framework-imports'; import { testTransform } from './test-utils'; describe('rewrite-framework-imports', () => { for (const framework of ['solid', 'vue', 'svelte'] as const) { it(`transforms ${framework} correctly`, () => { testTransform(transformer, `rewrite-framework-imports-${framework}`); }); } }); --- File: /ai/packages/codemod/src/test/test-utils.test.ts --- import { describe, it, expect, beforeEach, vi } from 'vitest'; import * as testUtils from './test-utils'; import { existsSync, readFileSync } from 'fs'; import { join } from 'path'; vi.mock('fs', () => ({ existsSync: vi.fn(), readFileSync: vi.fn(), })); vi.mock('path', () => ({ join: vi.fn((...args) => args.join('/')), })); describe('test-utils', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('applyTransform', () => { it('should apply transform and return modified source when transform returns string', () => { const mockTransform = vi.fn().mockReturnValue('modified source'); const input = 'original source'; const result = testUtils.applyTransform(mockTransform, input); expect(result).toBe('modified source'); expect(mockTransform).toHaveBeenCalledWith( expect.objectContaining({ path: 'test.tsx', source: input, }), expect.objectContaining({ j: expect.any(Function), jscodeshift: expect.any(Function), stats: expect.any(Function), report: console.log, }), expect.any(Object), // Expecting the 'options' argument ); }); it('should return original source when transform returns null', () => { const mockTransform = vi.fn().mockReturnValue(null); const input = 'original source'; const result = testUtils.applyTransform(mockTransform, input); expect(result).toBe(input); expect(mockTransform).toHaveBeenCalledWith( expect.objectContaining({ path: 'test.tsx', source: input, }), expect.objectContaining({ j: expect.any(Function), jscodeshift: expect.any(Function), stats: expect.any(Function), report: console.log, }), expect.any(Object), // Expecting the 'options' argument ); }); it('should pass additional options to transform', () => { const mockTransform = vi.fn().mockReturnValue('modified'); const options = { dry: true }; testUtils.applyTransform(mockTransform, 'input', options); expect(mockTransform).toHaveBeenCalledWith( expect.objectContaining({ path: 'test.tsx', source: 'input', }), expect.objectContaining({ j: expect.any(Function), jscodeshift: expect.any(Function), stats: expect.any(Function), report: console.log, }), expect.objectContaining(options), // Verifying the 'options' argument ); }); }); describe('readFixture', () => { const mockExistsSync = existsSync as unknown as ReturnType<typeof vi.fn>; const mockReadFileSync = readFileSync as unknown as ReturnType< typeof vi.fn >; const mockJoin = join as unknown as ReturnType<typeof vi.fn>; beforeEach(() => { mockJoin.mockImplementation((...parts) => parts.join('/')); }); it('should read .ts fixture when it exists', () => { mockExistsSync.mockImplementation((path: string) => path.endsWith('.ts')); mockReadFileSync.mockReturnValue('ts content'); const result = testUtils.readFixture('test', 'input'); expect(result).toEqual({ content: 'ts content', extension: '.ts', }); expect(mockReadFileSync).toHaveBeenCalledWith( expect.stringContaining('test.input.ts'), 'utf8', ); }); it('should read .tsx fixture when .ts does not exist', () => { mockExistsSync.mockImplementation((path: string) => path.endsWith('.tsx'), ); mockReadFileSync.mockReturnValue('tsx content'); const result = testUtils.readFixture('test', 'input'); expect(result).toEqual({ content: 'tsx content', extension: '.tsx', }); expect(mockReadFileSync).toHaveBeenCalledWith( expect.stringContaining('test.input.tsx'), 'utf8', ); }); it('should throw error when no fixture exists', () => { mockExistsSync.mockReturnValue(false); expect(() => testUtils.readFixture('test', 'input')).toThrow( 'Fixture not found: test.input', ); }); }); describe('validateSyntax', () => { it('should validate typescript syntax', () => { const tsCode = ` interface User { name: string; age: number; } const user: User = { name: 'John', age: 30 }; `; expect(() => testUtils.validateSyntax(tsCode, '.ts')).not.toThrow(); }); it('should validate tsx syntax', () => { const tsxCode = ` interface Props { name: string; } const Component = ({ name }: Props) => <div>{name}</div>; `; expect(() => testUtils.validateSyntax(tsxCode, '.tsx')).not.toThrow(); }); it('should validate javascript syntax', () => { const jsCode = ` const user = { name: 'John', age: 30 }; console.log(user); `; expect(() => testUtils.validateSyntax(jsCode, '.js')).not.toThrow(); }); it('should validate jsx syntax', () => { const jsxCode = ` const Component = ({ name }) => <div>{name}</div>; export default Component; `; expect(() => testUtils.validateSyntax(jsxCode, '.jsx')).not.toThrow(); }); it('should catch syntax errors', () => { const invalidCode = ` const x = { foo: 'bar' bar: 'baz' // missing comma }; `; expect(() => testUtils.validateSyntax(invalidCode, '.js')).toThrow( /Syntax error/, ); }); it('should catch typescript type errors', () => { const invalidTsCode = ` const x: number = "string"; // Type mismatch `; expect(() => testUtils.validateSyntax(invalidTsCode, '.ts')).toThrow( /Type.*string.*not assignable to type.*number/, ); }); }); describe('testTransform', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should compare transform output with fixture and validate syntax', () => { const mockTransform = vi.fn().mockReturnValue('const x: number = 42;'); (existsSync as any).mockImplementation((path: string) => path.endsWith('.ts'), ); (readFileSync as any) .mockReturnValueOnce('const x: number = 1;') // Valid TS input .mockReturnValueOnce('const x: number = 42;'); // Valid TS output testUtils.testTransform(mockTransform, 'test'); expect(mockTransform).toHaveBeenCalled(); expect(readFileSync).toHaveBeenCalledTimes(2); }); it('should throw when transform output does not match fixture', () => { const mockTransform = vi.fn().mockReturnValue('wrong output'); (existsSync as any).mockImplementation((path: string) => path.endsWith('.ts'), ); (readFileSync as any) .mockReturnValueOnce('input') .mockReturnValueOnce('expected output'); expect(() => testUtils.testTransform(mockTransform, 'test')).toThrow(); }); }); }); --- File: /ai/packages/codemod/src/test/test-utils.ts --- import { API, FileInfo } from 'jscodeshift'; import jscodeshift from 'jscodeshift'; import { join } from 'path'; import { existsSync, readFileSync } from 'fs'; import ts from 'typescript'; /** * Applies a codemod transform to the input code. * * @param transform - The codemod transform function. * @param input - The input source code. * @param options - Optional transform options. * @returns The transformed code or the original input if no changes were made. */ export function applyTransform( transform: (fileInfo: FileInfo, api: API, options: any) => string | null, input: string, options = {}, ): string { const fileInfo = { path: 'test.tsx', // Use .tsx to support both .ts and .tsx source: input, }; const j = jscodeshift.withParser('tsx'); const api: API = { j, jscodeshift: j, stats: () => {}, report: console.log, }; // A null result indicates no changes were made. const result = transform(fileInfo, api, options); return result === null ? input : result; } /** * Reads a fixture file from the __testfixtures__ directory. * * @param name - The base name of the fixture. * @param type - The type of fixture ('input' or 'output'). * @returns An object containing the fixture's content and its file extension. * @throws If the fixture file is not found. */ export function readFixture( name: string, type: 'input' | 'output', ): { content: string; extension: string } { const basePath = join(__dirname, '__testfixtures__', `${name}.${type}`); const extensions = ['.ts', '.tsx', '.js', '.jsx']; for (const ext of extensions) { const fullPath = `${basePath}${ext}`; if (existsSync(fullPath)) { return { content: readFileSync(fullPath, 'utf8'), extension: ext }; } } throw new Error( `Fixture not found: ${name}.${type} with extensions ${extensions.join( ', ', )}`, ); } /** * Validates the syntax of the provided code using TypeScript's compiler. * * @param code - The source code to validate. * @param extension - The file extension to determine ScriptKind. * @throws If the code contains syntax errors. */ export function validateSyntax(code: string, extension: string): void { // Add JSX namespace definition only for tsx files const jsxTypes = ` declare namespace JSX { interface IntrinsicElements { [elemName: string]: any; } } `; // Add JSX types only for tsx files const codeWithTypes = extension === '.tsx' ? jsxTypes + code : code; // Determine the appropriate script kind based on file extension let scriptKind: ts.ScriptKind; switch (extension) { case '.tsx': scriptKind = ts.ScriptKind.TSX; break; case '.jsx': scriptKind = ts.ScriptKind.JSX; break; case '.ts': scriptKind = ts.ScriptKind.TS; break; case '.js': default: scriptKind = ts.ScriptKind.JS; } const fileName = `test${extension}`; // Create a source file const sourceFile = ts.createSourceFile( fileName, codeWithTypes, ts.ScriptTarget.Latest, true, scriptKind, ); // Create compiler options const compilerOptions: ts.CompilerOptions = { allowJs: true, noEmit: true, jsx: ts.JsxEmit.Preserve, target: ts.ScriptTarget.Latest, module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.NodeNext, esModuleInterop: true, strict: true, noImplicitAny: false, skipLibCheck: true, jsxFactory: 'React.createElement', jsxFragmentFactory: 'React.Fragment', baseUrl: '.', paths: { '*': ['*'], }, // Disable type checking for JS/JSX files checkJs: extension !== '.js' && extension !== '.jsx', allowSyntheticDefaultImports: true, // Ignore missing libraries noResolve: true, }; // Create a program with the source file const host = ts.createCompilerHost(compilerOptions); const originalGetSourceFile = host.getSourceFile; host.getSourceFile = (name: string, ...args) => { if (name === fileName) { return sourceFile; } return originalGetSourceFile.call(host, name, ...args); }; // Override module resolution host.resolveModuleNameLiterals = (moduleLiterals, containingFile) => { return moduleLiterals.map(moduleLiteral => ({ resolvedModule: { resolvedFileName: `${moduleLiteral.text}.d.ts`, extension: '.d.ts', isExternalLibraryImport: true, packageId: { name: moduleLiteral.text, subModuleName: '', version: '1.0.0', }, }, })); }; const program = ts.createProgram([fileName], compilerOptions, host); // Get only syntactic diagnostics for JS/JSX files const diagnostics = extension === '.js' || extension === '.jsx' ? program.getSyntacticDiagnostics(sourceFile) : [ ...program.getSyntacticDiagnostics(sourceFile), ...program.getSemanticDiagnostics(sourceFile), ]; // Filter out module resolution errors const relevantDiagnostics = diagnostics.filter(diagnostic => { // Ignore "Cannot find module" errors if (diagnostic.code === 2307) { // TypeScript error code for module not found return false; } return true; }); // If there are any errors, throw with details if (relevantDiagnostics.length > 0) { const errors = relevantDiagnostics .map(diagnostic => { if (diagnostic.file) { const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!); return `${line + 1}:${ character + 1 } - ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`; } return ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); }) .join('\n'); throw new Error( `Syntax error in code with extension ${extension}:\n${errors}`, ); } } /** * Tests a codemod transform by applying it to input fixtures and comparing the output to expected fixtures. * Additionally, validates that both input and output fixtures have valid syntax. * * @param transformer - The codemod transformer function. * @param fixtureName - The base name of the fixture to test. */ export function testTransform( transformer: (fileInfo: FileInfo, api: API, options: any) => string | null, fixtureName: string, ) { // Read input and output fixtures along with their extensions const { content: input, extension: inputExt } = readFixture( fixtureName, 'input', ); const { content: expectedOutput, extension: outputExt } = readFixture( fixtureName, 'output', ); // Validate that input code is syntactically correct validateSyntax(input, inputExt); // Validate that expected output is syntactically correct validateSyntax(expectedOutput, outputExt); // Apply the transformer to the input code const actualOutput = applyTransform(transformer, input); // Validate that output code is syntactically correct validateSyntax(actualOutput, outputExt); // Compare actual output to expected output expect(actualOutput).toBe(expectedOutput); } --- File: /ai/packages/codemod/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/bin/codemod.ts'], outDir: 'dist/bin', format: ['cjs'], dts: false, sourcemap: true, }, { entry: ['src/codemods/**/*.ts'], outDir: 'dist/codemods', format: ['cjs'], dts: false, sourcemap: true, }, ]); --- File: /ai/packages/codemod/vitest.config.ts --- import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, environment: 'node', }, }); --- File: /ai/packages/cohere/src/cohere-chat-language-model.test.ts --- import { LanguageModelV2Prompt } from '@ai-sdk/provider'; import { convertReadableStreamToArray, createTestServer, isNodeVersion, } from '@ai-sdk/provider-utils/test'; import { createCohere } from './cohere-provider'; const TEST_PROMPT: LanguageModelV2Prompt = [ { role: 'system', content: 'you are a friendly bot!', }, { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; const provider = createCohere({ apiKey: 'test-api-key', }); const model = provider('command-r-plus'); const server = createTestServer({ 'https://api.cohere.com/v2/chat': {}, }); describe('doGenerate', () => { function prepareJsonResponse({ text = '', tool_calls, finish_reason = 'COMPLETE', tokens = { input_tokens: 4, output_tokens: 30, }, generation_id = 'dad0c7cd-7982-42a7-acfb-706ccf598291', headers, }: { text?: string; tool_calls?: any; finish_reason?: string; tokens?: { input_tokens: number; output_tokens: number; }; generation_id?: string; headers?: Record<string, string>; }) { server.urls['https://api.cohere.com/v2/chat'].response = { type: 'json-value', headers, body: { response_id: '0cf61ae0-1f60-4c18-9802-be7be809e712', generation_id, message: { role: 'assistant', content: [{ type: 'text', text }], ...(tool_calls ? { tool_calls } : {}), }, finish_reason, usage: { billed_units: { input_tokens: 9, output_tokens: 415 }, tokens, }, }, }; } it('should extract text response', async () => { prepareJsonResponse({ text: 'Hello, World!' }); const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "text": "Hello, World!", "type": "text", }, ] `); }); it('should extract tool calls', async () => { prepareJsonResponse({ text: 'Hello, World!', tool_calls: [ { id: 'test-id-1', type: 'function', function: { name: 'test-tool', arguments: '{"value":"example value"}', }, }, ], }); const { content, finishReason } = await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "text": "Hello, World!", "type": "text", }, { "input": "{"value":"example value"}", "toolCallId": "test-id-1", "toolName": "test-tool", "type": "tool-call", }, ] `); expect(finishReason).toStrictEqual('stop'); }); it('should extract usage', async () => { prepareJsonResponse({ tokens: { input_tokens: 20, output_tokens: 5 }, }); const { usage } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(usage).toMatchInlineSnapshot(` { "inputTokens": 20, "outputTokens": 5, "totalTokens": 25, } `); }); it('should send additional response information', async () => { prepareJsonResponse({ generation_id: 'test-id', }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response).toMatchInlineSnapshot(` { "body": { "finish_reason": "COMPLETE", "generation_id": "test-id", "message": { "content": [ { "text": "", "type": "text", }, ], "role": "assistant", }, "response_id": "0cf61ae0-1f60-4c18-9802-be7be809e712", "usage": { "billed_units": { "input_tokens": 9, "output_tokens": 415, }, "tokens": { "input_tokens": 4, "output_tokens": 30, }, }, }, "headers": { "content-length": "287", "content-type": "application/json", }, "id": "test-id", } `); }); it('should extract finish reason', async () => { prepareJsonResponse({ finish_reason: 'MAX_TOKENS', }); const { finishReason } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(finishReason).toStrictEqual('length'); }); it('should expose the raw response headers', async () => { prepareJsonResponse({ headers: { 'test-header': 'test-value' }, }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-length': '316', 'content-type': 'application/json', // custom header 'test-header': 'test-value', }); }); it('should pass model and messages', async () => { prepareJsonResponse({}); await model.doGenerate({ prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'command-r-plus', messages: [ { role: 'system', content: 'you are a friendly bot!' }, { role: 'user', content: 'Hello' }, ], }); }); describe('should pass tools', async () => { it('should support "none" tool choice', async () => { prepareJsonResponse({}); await model.doGenerate({ toolChoice: { type: 'none' }, tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' }, }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'command-r-plus', messages: [ { role: 'system', content: 'you are a friendly bot!', }, { role: 'user', content: 'Hello' }, ], tool_choice: 'NONE', tools: [ { type: 'function', function: { name: 'test-tool', parameters: { type: 'object', properties: { value: { type: 'string' }, }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }, ], }); }); }); it('should pass headers', async () => { prepareJsonResponse({}); const provider = createCohere({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider('command-r-plus').doGenerate({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should pass response format', async () => { prepareJsonResponse({}); await model.doGenerate({ prompt: TEST_PROMPT, responseFormat: { type: 'json', schema: { type: 'object', properties: { text: { type: 'string' }, }, required: ['text'], }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'command-r-plus', messages: [ { role: 'system', content: 'you are a friendly bot!' }, { role: 'user', content: 'Hello' }, ], response_format: { type: 'json_object', json_schema: { type: 'object', properties: { text: { type: 'string' }, }, required: ['text'], }, }, }); }); it('should send request body', async () => { prepareJsonResponse({ text: '' }); const { request } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(request).toMatchInlineSnapshot(` { "body": { "frequency_penalty": undefined, "k": undefined, "max_tokens": undefined, "messages": [ { "content": "you are a friendly bot!", "role": "system", }, { "content": "Hello", "role": "user", }, ], "model": "command-r-plus", "p": undefined, "presence_penalty": undefined, "response_format": undefined, "seed": undefined, "stop_sequences": undefined, "temperature": undefined, "tool_choice": undefined, "tools": undefined, }, } `); }); it('should handle string "null" tool call arguments', async () => { prepareJsonResponse({ tool_calls: [ { id: 'test-id-1', type: 'function', function: { name: 'currentTime', arguments: 'null', }, }, ], }); const { content } = await model.doGenerate({ tools: [ { type: 'function', name: 'currentTime', inputSchema: { type: 'object', properties: {}, required: [], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: [ { role: 'user', content: [{ type: 'text', text: 'What is the current time?' }], }, ], }); expect(content).toMatchInlineSnapshot(` [ { "input": "{}", "toolCallId": "test-id-1", "toolName": "currentTime", "type": "tool-call", }, ] `); }); describe('citations', () => { it('should extract text documents and send to API', async () => { prepareJsonResponse({ text: 'Hello, World!' }); await model.doGenerate({ prompt: [ { role: 'user', content: [ { type: 'text', text: 'What does this say?' }, { type: 'file', data: 'This is a test document.', mediaType: 'text/plain', filename: 'test.txt', }, ], }, ], }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "documents": [ { "data": { "text": "This is a test document.", "title": "test.txt", }, }, ], "messages": [ { "content": "What does this say?", "role": "user", }, ], "model": "command-r-plus", } `); }); it('should extract multiple text documents', async () => { prepareJsonResponse({ text: 'Hello, World!' }); await model.doGenerate({ prompt: [ { role: 'user', content: [ { type: 'text', text: 'What do these documents say?' }, { type: 'file', data: Buffer.from('First document content'), mediaType: 'text/plain', filename: 'doc1.txt', }, { type: 'file', data: Buffer.from('Second document content'), mediaType: 'text/plain', filename: 'doc2.txt', }, ], }, ], }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "documents": [ { "data": { "text": "First document content", "title": "doc1.txt", }, }, { "data": { "text": "Second document content", "title": "doc2.txt", }, }, ], "messages": [ { "content": "What do these documents say?", "role": "user", }, ], "model": "command-r-plus", } `); }); it('should support JSON files', async () => { prepareJsonResponse({ text: 'Hello, World!' }); await model.doGenerate({ prompt: [ { role: 'user', content: [ { type: 'text', text: 'What is in this JSON?' }, { type: 'file', data: Buffer.from('{"key": "value"}'), mediaType: 'application/json', filename: 'data.json', }, ], }, ], }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "documents": [ { "data": { "text": "{"key": "value"}", "title": "data.json", }, }, ], "messages": [ { "content": "What is in this JSON?", "role": "user", }, ], "model": "command-r-plus", } `); }); it('should throw error for unsupported file types', async () => { prepareJsonResponse({ text: 'Hello, World!' }); await expect( model.doGenerate({ prompt: [ { role: 'user', content: [ { type: 'text', text: 'What is this?' }, { type: 'file', data: Buffer.from('PDF binary data'), mediaType: 'application/pdf', filename: 'document.pdf', }, ], }, ], }), ).rejects.toThrow( "Media type 'application/pdf' is not supported. Supported media types are: text/* and application/json.", ); }); it('should successfully process supported text media types', async () => { prepareJsonResponse({ text: 'Hello, World!' }); await model.doGenerate({ prompt: [ { role: 'user', content: [ { type: 'text', text: 'What is this?' }, { type: 'file', data: Buffer.from('This is plain text content'), mediaType: 'text/plain', filename: 'text.txt', }, { type: 'file', data: Buffer.from('# Markdown Header\nContent'), mediaType: 'text/markdown', filename: 'doc.md', }, ], }, ], }); expect(await server.calls[0].requestBodyJson).toMatchObject({ documents: [ { data: { text: 'This is plain text content', title: 'text.txt', }, }, { data: { text: '# Markdown Header\nContent', title: 'doc.md', }, }, ], }); }); it('should extract citations from response', async () => { const mockGenerateId = vi.fn().mockReturnValue('test-citation-id'); const testProvider = createCohere({ apiKey: 'test-api-key', generateId: mockGenerateId, }); const testModel = testProvider('command-r-plus'); server.urls['https://api.cohere.com/v2/chat'].response = { type: 'json-value', body: { response_id: '0cf61ae0-1f60-4c18-9802-be7be809e712', generation_id: 'dad0c7cd-7982-42a7-acfb-706ccf598291', message: { role: 'assistant', content: [ { type: 'text', text: 'AI has many benefits including automation.', }, ], citations: [ { start: 31, end: 41, text: 'automation', type: 'TEXT_CONTENT', sources: [ { type: 'document', id: 'doc:0', document: { id: 'doc:0', text: 'AI provides automation and efficiency.', title: 'ai-benefits.txt', }, }, ], }, ], }, finish_reason: 'COMPLETE', usage: { billed_units: { input_tokens: 9, output_tokens: 415 }, tokens: { input_tokens: 4, output_tokens: 30 }, }, }, }; const { content } = await testModel.doGenerate({ prompt: [ { role: 'user', content: [ { type: 'text', text: 'What are AI benefits?' }, { type: 'file', data: 'AI provides automation and efficiency.', mediaType: 'text/plain', filename: 'ai-benefits.txt', }, ], }, ], }); expect(content).toMatchInlineSnapshot(` [ { "text": "AI has many benefits including automation.", "type": "text", }, { "id": "test-citation-id", "mediaType": "text/plain", "providerMetadata": { "cohere": { "citationType": "TEXT_CONTENT", "end": 41, "sources": [ { "document": { "id": "doc:0", "text": "AI provides automation and efficiency.", "title": "ai-benefits.txt", }, "id": "doc:0", "type": "document", }, ], "start": 31, "text": "automation", }, }, "sourceType": "document", "title": "ai-benefits.txt", "type": "source", }, ] `); }); it('should not include documents parameter when no files present', async () => { prepareJsonResponse({ text: 'Hello, World!' }); await model.doGenerate({ prompt: TEST_PROMPT, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.documents).toBeUndefined(); }); }); }); describe('doStream', () => { function prepareStreamResponse({ content, usage = { input_tokens: 17, output_tokens: 244, }, finish_reason = 'COMPLETE', headers, }: { content: string[]; usage?: { input_tokens: number; output_tokens: number; }; finish_reason?: string; headers?: Record<string, string>; }) { server.urls['https://api.cohere.com/v2/chat'].response = { type: 'stream-chunks', headers, chunks: [ `event: message-start\ndata: {"type":"message-start","id":"586ac33f-9c64-452c-8f8d-e5890e73b6fb","delta":{"message":{"role":"assistant","content":[],"tool_plan":"","tool_calls":[],"citations":[]}}}\n\n`, `event: content-start\ndata: {"type":"content-start","index":0,"delta":{"message":{"content":{"type":"text","text":""}}}}\n\n`, ...content.map( text => `event: content-delta\ndata: {"type":"content-delta","index":0,"delta":{"message":{"content":{"text":"${text}"}}}}\n\n`, ), `event: content-end\ndata: {"type":"content-end","index":0}\n\n`, `event: message-end\ndata: {"type":"message-end","delta":` + `{"finish_reason":"${finish_reason}",` + `"usage":{"tokens":{"input_tokens":${usage.input_tokens},"output_tokens":${usage.output_tokens}}}}}\n\n`, `data: [DONE]\n\n`, ], }; } it('should stream text deltas', async () => { prepareStreamResponse({ content: ['Hello', ', ', 'World!'], finish_reason: 'COMPLETE', usage: { input_tokens: 34, output_tokens: 12, }, }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "586ac33f-9c64-452c-8f8d-e5890e73b6fb", "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "delta": ", ", "id": "0", "type": "text-delta", }, { "delta": "World!", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 34, "outputTokens": 12, "totalTokens": 46, }, }, ] `); }); it('should stream tool deltas', async () => { server.urls['https://api.cohere.com/v2/chat'].response = { type: 'stream-chunks', chunks: [ `event: message-start\ndata: {"type":"message-start","id":"29f14a5a-11de-4cae-9800-25e4747408ea","delta":{"message":{"role":"assistant","content":[],"tool_plan":"","tool_calls":[],"citations":[]}}}\n\n`, `event: tool-call-start\ndata: {"type":"tool-call-start","delta":{"message":{"tool_calls":{"id":"test-id-1","type":"function","function":{"name":"test-tool","arguments":""}}}}}\n\n`, `event: tool-call-delta\ndata: {"type":"tool-call-delta","delta":{"message":{"tool_calls":{"function":{"arguments":"{\\n \\""}}}}}\n\n`, `event: tool-call-delta\ndata: {"type":"tool-call-delta","delta":{"message":{"tool_calls":{"function":{"arguments":"ticker"}}}}}\n\n`, `event: tool-call-delta\ndata: {"type":"tool-call-delta","delta":{"message":{"tool_calls":{"function":{"arguments":"_"}}}}}\n\n`, `event: tool-call-delta\ndata: {"type":"tool-call-delta","delta":{"message":{"tool_calls":{"function":{"arguments":"symbol"}}}}}\n\n`, `event: tool-call-delta\ndata: {"type":"tool-call-delta","delta":{"message":{"tool_calls":{"function":{"arguments":"\\":"}}}}}\n\n`, `event: tool-call-delta\ndata: {"type":"tool-call-delta","delta":{"message":{"tool_calls":{"function":{"arguments":" \\""}}}}}\n\n`, `event: tool-call-delta\ndata: {"type":"tool-call-delta","delta":{"message":{"tool_calls":{"function":{"arguments":"AAPL"}}}}}\n\n`, `event: tool-call-delta\ndata: {"type":"tool-call-delta","delta":{"message":{"tool_calls":{"function":{"arguments":"\\""}}}}}\n\n`, `event: tool-call-delta\ndata: {"type":"tool-call-delta","delta":{"message":{"tool_calls":{"function":{"arguments":"\\n"}}}}}\n\n`, `event: tool-call-delta\ndata: {"type":"tool-call-delta","delta":{"message":{"tool_calls":{"function":{"arguments":"}"}}}}}\n\n`, `event: tool-call-end\ndata: {"type":"tool-call-end"}\n\n`, `event: message-end\ndata: {"type":"message-end","delta":{"finish_reason":"COMPLETE","usage":{"tokens":{"input_tokens":893,"output_tokens":62}}}}\n\n`, `data: [DONE]\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], includeRawChunks: false, }); const responseArray = await convertReadableStreamToArray(stream); expect(responseArray).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "29f14a5a-11de-4cae-9800-25e4747408ea", "type": "response-metadata", }, { "id": "test-id-1", "toolName": "test-tool", "type": "tool-input-start", }, { "delta": "{ "", "id": "test-id-1", "type": "tool-input-delta", }, { "delta": "ticker", "id": "test-id-1", "type": "tool-input-delta", }, { "delta": "_", "id": "test-id-1", "type": "tool-input-delta", }, { "delta": "symbol", "id": "test-id-1", "type": "tool-input-delta", }, { "delta": "":", "id": "test-id-1", "type": "tool-input-delta", }, { "delta": " "", "id": "test-id-1", "type": "tool-input-delta", }, { "delta": "AAPL", "id": "test-id-1", "type": "tool-input-delta", }, { "delta": """, "id": "test-id-1", "type": "tool-input-delta", }, { "delta": " ", "id": "test-id-1", "type": "tool-input-delta", }, { "delta": "}", "id": "test-id-1", "type": "tool-input-delta", }, { "id": "test-id-1", "type": "tool-input-end", }, { "input": "{"ticker_symbol":"AAPL"}", "toolCallId": "test-id-1", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 893, "outputTokens": 62, "totalTokens": 955, }, }, ] `); // Check if the tool call ID is the same in the tool call delta and the tool call const toolCallIds = responseArray .filter( chunk => chunk.type === 'tool-input-delta' || chunk.type === 'tool-call', ) .map(chunk => (chunk.type === 'tool-call' ? chunk.toolCallId : chunk.id)); expect(new Set(toolCallIds)).toStrictEqual(new Set(['test-id-1'])); }); it.skipIf(isNodeVersion(20))( 'should handle unparsable stream parts', async () => { server.urls['https://api.cohere.com/v2/chat'].response = { type: 'stream-chunks', chunks: [`event: foo-message\ndata: {unparsable}\n\n`], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "error": [AI_JSONParseError: JSON parsing failed: Text: {unparsable}. Error message: Expected property name or '}' in JSON at position 1 (line 1 column 2)], "type": "error", }, { "finishReason": "error", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }, ); it('should expose the raw response headers', async () => { prepareStreamResponse({ content: [], headers: { 'test-header': 'test-value' }, }); const { response } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', // custom header 'test-header': 'test-value', }); }); it('should pass the messages and the model', async () => { prepareStreamResponse({ content: [] }); await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ stream: true, model: 'command-r-plus', messages: [ { role: 'system', content: 'you are a friendly bot!', }, { role: 'user', content: 'Hello', }, ], }); }); it('should pass headers', async () => { prepareStreamResponse({ content: [] }); const provider = createCohere({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider('command-r-plus').doStream({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, includeRawChunks: false, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should send request body', async () => { prepareStreamResponse({ content: [] }); const { request } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(request).toMatchInlineSnapshot(` { "body": { "frequency_penalty": undefined, "k": undefined, "max_tokens": undefined, "messages": [ { "content": "you are a friendly bot!", "role": "system", }, { "content": "Hello", "role": "user", }, ], "model": "command-r-plus", "p": undefined, "presence_penalty": undefined, "response_format": undefined, "seed": undefined, "stop_sequences": undefined, "stream": true, "temperature": undefined, "tool_choice": undefined, "tools": undefined, }, } `); }); it('should handle empty tool call arguments', async () => { server.urls['https://api.cohere.com/v2/chat'].response = { type: 'stream-chunks', chunks: [ `event: message-start\ndata: {"type":"message-start","id":"test-id","delta":{"message":{"role":"assistant","content":[],"tool_plan":"","tool_calls":[],"citations":[]}}}\n\n`, `event: tool-call-start\ndata: {"type":"tool-call-start","delta":{"message":{"tool_calls":{"id":"test-id-1","type":"function","function":{"name":"test-tool","arguments":""}}}}}\n\n`, `event: tool-call-end\ndata: {"type":"tool-call-end"}\n\n`, `event: message-end\ndata: {"type":"message-end","delta":{"finish_reason":"COMPLETE","usage":{"tokens":{"input_tokens":10,"output_tokens":5}}}}\n\n`, `data: [DONE]\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: {}, required: [], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "test-id", "type": "response-metadata", }, { "id": "test-id-1", "toolName": "test-tool", "type": "tool-input-start", }, { "id": "test-id-1", "type": "tool-input-end", }, { "input": "{}", "toolCallId": "test-id-1", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 10, "outputTokens": 5, "totalTokens": 15, }, }, ] `); }); it('should include raw chunks when includeRawChunks is enabled', async () => { prepareStreamResponse({ content: ['Hello', ' World!'], }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: true, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks.filter(chunk => chunk.type === 'raw')).toMatchInlineSnapshot(` [ { "rawValue": { "delta": { "message": { "citations": [], "content": [], "role": "assistant", "tool_calls": [], "tool_plan": "", }, }, "id": "586ac33f-9c64-452c-8f8d-e5890e73b6fb", "type": "message-start", }, "type": "raw", }, { "rawValue": { "delta": { "message": { "content": { "text": "", "type": "text", }, }, }, "index": 0, "type": "content-start", }, "type": "raw", }, { "rawValue": { "delta": { "message": { "content": { "text": "Hello", }, }, }, "index": 0, "type": "content-delta", }, "type": "raw", }, { "rawValue": { "delta": { "message": { "content": { "text": " World!", }, }, }, "index": 0, "type": "content-delta", }, "type": "raw", }, { "rawValue": { "index": 0, "type": "content-end", }, "type": "raw", }, { "rawValue": { "delta": { "finish_reason": "COMPLETE", "usage": { "tokens": { "input_tokens": 17, "output_tokens": 244, }, }, }, "type": "message-end", }, "type": "raw", }, ] `); }); it('should not include raw chunks when includeRawChunks is false', async () => { prepareStreamResponse({ content: ['Hello', ' World!'], }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks.filter(chunk => chunk.type === 'raw')).toHaveLength(0); }); }); --- File: /ai/packages/cohere/src/cohere-chat-language-model.ts --- import { LanguageModelV2, LanguageModelV2CallWarning, LanguageModelV2Content, LanguageModelV2FinishReason, LanguageModelV2Prompt, LanguageModelV2StreamPart, LanguageModelV2Usage, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { FetchFunction, ParseResult, combineHeaders, createEventSourceResponseHandler, createJsonResponseHandler, generateId, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { CohereChatModelId } from './cohere-chat-options'; import { cohereFailedResponseHandler } from './cohere-error'; import { convertToCohereChatPrompt } from './convert-to-cohere-chat-prompt'; import { mapCohereFinishReason } from './map-cohere-finish-reason'; import { prepareTools } from './cohere-prepare-tools'; type CohereChatConfig = { provider: string; baseURL: string; headers: () => Record<string, string | undefined>; fetch?: FetchFunction; generateId: () => string; }; export class CohereChatLanguageModel implements LanguageModelV2 { readonly specificationVersion = 'v2'; readonly modelId: CohereChatModelId; readonly supportedUrls = { // No URLs are supported. }; private readonly config: CohereChatConfig; constructor(modelId: CohereChatModelId, config: CohereChatConfig) { this.modelId = modelId; this.config = config; } get provider(): string { return this.config.provider; } private getArgs({ prompt, maxOutputTokens, temperature, topP, topK, frequencyPenalty, presencePenalty, stopSequences, responseFormat, seed, tools, toolChoice, }: Parameters<LanguageModelV2['doGenerate']>[0]) { const { messages: chatPrompt, documents: cohereDocuments, warnings: promptWarnings, } = convertToCohereChatPrompt(prompt); const { tools: cohereTools, toolChoice: cohereToolChoice, toolWarnings, } = prepareTools({ tools, toolChoice }); return { args: { // model id: model: this.modelId, // standardized settings: frequency_penalty: frequencyPenalty, presence_penalty: presencePenalty, max_tokens: maxOutputTokens, temperature, p: topP, k: topK, seed, stop_sequences: stopSequences, // response format: response_format: responseFormat?.type === 'json' ? { type: 'json_object', json_schema: responseFormat.schema } : undefined, // messages: messages: chatPrompt, // tools: tools: cohereTools, tool_choice: cohereToolChoice, // documents for RAG: ...(cohereDocuments.length > 0 && { documents: cohereDocuments }), }, warnings: [...toolWarnings, ...promptWarnings], }; } async doGenerate( options: Parameters<LanguageModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>> { const { args, warnings } = this.getArgs(options); const { responseHeaders, value: response, rawValue: rawResponse, } = await postJsonToApi({ url: `${this.config.baseURL}/chat`, headers: combineHeaders(this.config.headers(), options.headers), body: args, failedResponseHandler: cohereFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( cohereChatResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const content: Array<LanguageModelV2Content> = []; // text content: if ( response.message.content?.[0]?.text != null && response.message.content?.[0]?.text.length > 0 ) { content.push({ type: 'text', text: response.message.content[0].text }); } // citations: for (const citation of response.message.citations ?? []) { content.push({ type: 'source', sourceType: 'document', id: this.config.generateId(), mediaType: 'text/plain', title: citation.sources[0]?.document?.title || 'Document', providerMetadata: { cohere: { start: citation.start, end: citation.end, text: citation.text, sources: citation.sources, ...(citation.type && { citationType: citation.type }), }, }, }); } // tool calls: for (const toolCall of response.message.tool_calls ?? []) { content.push({ type: 'tool-call' as const, toolCallId: toolCall.id, toolName: toolCall.function.name, // Cohere sometimes returns `null` for tool call arguments for tools // defined as having no arguments. input: toolCall.function.arguments.replace(/^null$/, '{}'), }); } return { content, finishReason: mapCohereFinishReason(response.finish_reason), usage: { inputTokens: response.usage.tokens.input_tokens, outputTokens: response.usage.tokens.output_tokens, totalTokens: response.usage.tokens.input_tokens + response.usage.tokens.output_tokens, }, request: { body: args }, response: { // TODO timestamp, model id id: response.generation_id ?? undefined, headers: responseHeaders, body: rawResponse, }, warnings, }; } async doStream( options: Parameters<LanguageModelV2['doStream']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>> { const { args, warnings } = this.getArgs(options); const { responseHeaders, value: response } = await postJsonToApi({ url: `${this.config.baseURL}/chat`, headers: combineHeaders(this.config.headers(), options.headers), body: { ...args, stream: true }, failedResponseHandler: cohereFailedResponseHandler, successfulResponseHandler: createEventSourceResponseHandler( cohereChatChunkSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); let finishReason: LanguageModelV2FinishReason = 'unknown'; const usage: LanguageModelV2Usage = { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, }; let pendingToolCall: { id: string; name: string; arguments: string; hasFinished: boolean; } | null = null; return { stream: response.pipeThrough( new TransformStream< ParseResult<z.infer<typeof cohereChatChunkSchema>>, LanguageModelV2StreamPart >({ start(controller) { controller.enqueue({ type: 'stream-start', warnings }); }, transform(chunk, controller) { if (options.includeRawChunks) { controller.enqueue({ type: 'raw', rawValue: chunk.rawValue }); } // handle failed chunk parsing / validation: if (!chunk.success) { finishReason = 'error'; controller.enqueue({ type: 'error', error: chunk.error }); return; } const value = chunk.value; const type = value.type; switch (type) { case 'content-start': { controller.enqueue({ type: 'text-start', id: String(value.index), }); return; } case 'content-delta': { controller.enqueue({ type: 'text-delta', id: String(value.index), delta: value.delta.message.content.text, }); return; } case 'content-end': { controller.enqueue({ type: 'text-end', id: String(value.index), }); return; } case 'tool-call-start': { const toolId = value.delta.message.tool_calls.id; const toolName = value.delta.message.tool_calls.function.name; const initialArgs = value.delta.message.tool_calls.function.arguments; pendingToolCall = { id: toolId, name: toolName, arguments: initialArgs, hasFinished: false, }; controller.enqueue({ type: 'tool-input-start', id: toolId, toolName, }); if (initialArgs.length > 0) { controller.enqueue({ type: 'tool-input-delta', id: toolId, delta: initialArgs, }); } return; } case 'tool-call-delta': { if (pendingToolCall && !pendingToolCall.hasFinished) { const argsDelta = value.delta.message.tool_calls.function.arguments; pendingToolCall.arguments += argsDelta; controller.enqueue({ type: 'tool-input-delta', id: pendingToolCall.id, delta: argsDelta, }); } return; } case 'tool-call-end': { if (pendingToolCall && !pendingToolCall.hasFinished) { controller.enqueue({ type: 'tool-input-end', id: pendingToolCall.id, }); controller.enqueue({ type: 'tool-call', toolCallId: pendingToolCall.id, toolName: pendingToolCall.name, input: JSON.stringify( JSON.parse(pendingToolCall.arguments?.trim() || '{}'), ), }); pendingToolCall.hasFinished = true; pendingToolCall = null; } return; } case 'message-start': { controller.enqueue({ type: 'response-metadata', id: value.id ?? undefined, }); return; } case 'message-end': { finishReason = mapCohereFinishReason(value.delta.finish_reason); const tokens = value.delta.usage.tokens; usage.inputTokens = tokens.input_tokens; usage.outputTokens = tokens.output_tokens; usage.totalTokens = tokens.input_tokens + tokens.output_tokens; return; } default: { return; } } }, flush(controller) { controller.enqueue({ type: 'finish', finishReason, usage, }); }, }), ), request: { body: { ...args, stream: true } }, response: { headers: responseHeaders }, }; } } const cohereChatResponseSchema = z.object({ generation_id: z.string().nullish(), message: z.object({ role: z.string(), content: z .array( z.object({ type: z.string(), text: z.string(), }), ) .nullish(), tool_plan: z.string().nullish(), tool_calls: z .array( z.object({ id: z.string(), type: z.literal('function'), function: z.object({ name: z.string(), arguments: z.string(), }), }), ) .nullish(), citations: z .array( z.object({ start: z.number(), end: z.number(), text: z.string(), sources: z.array( z.object({ type: z.string().optional(), id: z.string().optional(), document: z.object({ id: z.string().optional(), text: z.string(), title: z.string(), }), }), ), type: z.string().optional(), }), ) .nullish(), }), finish_reason: z.string(), usage: z.object({ billed_units: z.object({ input_tokens: z.number(), output_tokens: z.number(), }), tokens: z.object({ input_tokens: z.number(), output_tokens: z.number(), }), }), }); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const cohereChatChunkSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('citation-start'), }), z.object({ type: z.literal('citation-end'), }), z.object({ type: z.literal('content-start'), index: z.number(), }), z.object({ type: z.literal('content-delta'), index: z.number(), delta: z.object({ message: z.object({ content: z.object({ text: z.string(), }), }), }), }), z.object({ type: z.literal('content-end'), index: z.number(), }), z.object({ type: z.literal('message-start'), id: z.string().nullish(), }), z.object({ type: z.literal('message-end'), delta: z.object({ finish_reason: z.string(), usage: z.object({ tokens: z.object({ input_tokens: z.number(), output_tokens: z.number(), }), }), }), }), // https://docs.cohere.com/v2/docs/streaming#tool-use-stream-events-for-tool-calling z.object({ type: z.literal('tool-plan-delta'), delta: z.object({ message: z.object({ tool_plan: z.string(), }), }), }), z.object({ type: z.literal('tool-call-start'), delta: z.object({ message: z.object({ tool_calls: z.object({ id: z.string(), type: z.literal('function'), function: z.object({ name: z.string(), arguments: z.string(), }), }), }), }), }), // A single tool call's `arguments` stream in chunks and must be accumulated // in a string and so the full tool object info can only be parsed once we see // `tool-call-end`. z.object({ type: z.literal('tool-call-delta'), delta: z.object({ message: z.object({ tool_calls: z.object({ function: z.object({ arguments: z.string(), }), }), }), }), }), z.object({ type: z.literal('tool-call-end'), }), ]); --- File: /ai/packages/cohere/src/cohere-chat-options.ts --- // https://docs.cohere.com/docs/models export type CohereChatModelId = | 'command-a-03-2025' | 'command-r7b-12-2024' | 'command-r-plus-04-2024' | 'command-r-plus' | 'command-r-08-2024' | 'command-r-03-2024' | 'command-r' | 'command' | 'command-nightly' | 'command-light' | 'command-light-nightly' | (string & {}); --- File: /ai/packages/cohere/src/cohere-chat-prompt.ts --- export type CohereChatPrompt = Array<CohereChatMessage>; export type CohereChatMessage = | CohereSystemMessage | CohereUserMessage | CohereAssistantMessage | CohereToolMessage; export interface CohereSystemMessage { role: 'system'; content: string; } export interface CohereUserMessage { role: 'user'; content: string; } export interface CohereAssistantMessage { role: 'assistant'; content: string | undefined; tool_plan: string | undefined; tool_calls: | Array<{ id: string; type: 'function'; function: { name: string; arguments: string; }; }> | undefined; } export interface CohereToolMessage { role: 'tool'; content: string; tool_call_id: string; } export type CohereToolChoice = 'NONE' | 'REQUIRED' | undefined; --- File: /ai/packages/cohere/src/cohere-embedding-model.test.ts --- import { EmbeddingModelV2Embedding } from '@ai-sdk/provider'; import { createTestServer } from '@ai-sdk/provider-utils/test'; import { createCohere } from './cohere-provider'; const dummyEmbeddings = [ [0.1, 0.2, 0.3, 0.4, 0.5], [0.6, 0.7, 0.8, 0.9, 1.0], ]; const testValues = ['sunny day at the beach', 'rainy day in the city']; const provider = createCohere({ apiKey: 'test-api-key' }); const model = provider.textEmbeddingModel('embed-english-v3.0'); const server = createTestServer({ 'https://api.cohere.com/v2/embed': {}, }); describe('doEmbed', () => { function prepareJsonResponse({ embeddings = dummyEmbeddings, meta = { billed_units: { input_tokens: 8 } }, headers, }: { embeddings?: EmbeddingModelV2Embedding[]; meta?: { billed_units: { input_tokens: number } }; headers?: Record<string, string>; } = {}) { server.urls['https://api.cohere.com/v2/embed'].response = { type: 'json-value', headers, body: { id: 'test-id', texts: testValues, embeddings: { float: embeddings }, meta, }, }; } it('should extract embedding', async () => { prepareJsonResponse(); const { embeddings } = await model.doEmbed({ values: testValues }); expect(embeddings).toStrictEqual(dummyEmbeddings); }); it('should expose the raw response', async () => { prepareJsonResponse({ headers: { 'test-header': 'test-value' }, }); const { response } = await model.doEmbed({ values: testValues }); expect(response?.headers).toStrictEqual({ // default headers: 'content-length': '185', 'content-type': 'application/json', // custom header 'test-header': 'test-value', }); expect(response).toMatchSnapshot(); }); it('should extract usage', async () => { prepareJsonResponse({ meta: { billed_units: { input_tokens: 20 } }, }); const { usage } = await model.doEmbed({ values: testValues }); expect(usage).toStrictEqual({ tokens: 20 }); }); it('should pass the model and the values', async () => { prepareJsonResponse(); await model.doEmbed({ values: testValues }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'embed-english-v3.0', embedding_types: ['float'], texts: testValues, input_type: 'search_query', }); }); it('should pass the input_type setting', async () => { prepareJsonResponse(); await provider.textEmbeddingModel('embed-english-v3.0').doEmbed({ values: testValues, providerOptions: { cohere: { inputType: 'search_document', }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'embed-english-v3.0', embedding_types: ['float'], texts: testValues, input_type: 'search_document', }); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createCohere({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.textEmbeddingModel('embed-english-v3.0').doEmbed({ values: testValues, headers: { 'Custom-Request-Header': 'request-header-value', }, }); const requestHeaders = server.calls[0].requestHeaders; expect(requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); }); --- File: /ai/packages/cohere/src/cohere-embedding-model.ts --- import { EmbeddingModelV2, TooManyEmbeddingValuesForCallError, } from '@ai-sdk/provider'; import { combineHeaders, createJsonResponseHandler, FetchFunction, parseProviderOptions, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { CohereEmbeddingModelId, cohereEmbeddingOptions, } from './cohere-embedding-options'; import { cohereFailedResponseHandler } from './cohere-error'; type CohereEmbeddingConfig = { provider: string; baseURL: string; headers: () => Record<string, string | undefined>; fetch?: FetchFunction; }; export class CohereEmbeddingModel implements EmbeddingModelV2<string> { readonly specificationVersion = 'v2'; readonly modelId: CohereEmbeddingModelId; readonly maxEmbeddingsPerCall = 96; readonly supportsParallelCalls = true; private readonly config: CohereEmbeddingConfig; constructor(modelId: CohereEmbeddingModelId, config: CohereEmbeddingConfig) { this.modelId = modelId; this.config = config; } get provider(): string { return this.config.provider; } async doEmbed({ values, headers, abortSignal, providerOptions, }: Parameters<EmbeddingModelV2<string>['doEmbed']>[0]): Promise< Awaited<ReturnType<EmbeddingModelV2<string>['doEmbed']>> > { const embeddingOptions = await parseProviderOptions({ provider: 'cohere', providerOptions, schema: cohereEmbeddingOptions, }); if (values.length > this.maxEmbeddingsPerCall) { throw new TooManyEmbeddingValuesForCallError({ provider: this.provider, modelId: this.modelId, maxEmbeddingsPerCall: this.maxEmbeddingsPerCall, values, }); } const { responseHeaders, value: response, rawValue, } = await postJsonToApi({ url: `${this.config.baseURL}/embed`, headers: combineHeaders(this.config.headers(), headers), body: { model: this.modelId, // The AI SDK only supports 'float' embeddings which are also the only ones // the Cohere API docs state are supported for all models. // https://docs.cohere.com/v2/reference/embed#request.body.embedding_types embedding_types: ['float'], texts: values, input_type: embeddingOptions?.inputType ?? 'search_query', truncate: embeddingOptions?.truncate, }, failedResponseHandler: cohereFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( cohereTextEmbeddingResponseSchema, ), abortSignal, fetch: this.config.fetch, }); return { embeddings: response.embeddings.float, usage: { tokens: response.meta.billed_units.input_tokens }, response: { headers: responseHeaders, body: rawValue }, }; } } // minimal version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const cohereTextEmbeddingResponseSchema = z.object({ embeddings: z.object({ float: z.array(z.array(z.number())), }), meta: z.object({ billed_units: z.object({ input_tokens: z.number(), }), }), }); --- File: /ai/packages/cohere/src/cohere-embedding-options.ts --- import { z } from 'zod/v4'; export type CohereEmbeddingModelId = | 'embed-english-v3.0' | 'embed-multilingual-v3.0' | 'embed-english-light-v3.0' | 'embed-multilingual-light-v3.0' | 'embed-english-v2.0' | 'embed-english-light-v2.0' | 'embed-multilingual-v2.0' | (string & {}); export const cohereEmbeddingOptions = z.object({ /** * Specifies the type of input passed to the model. Default is `search_query`. * * - "search_document": Used for embeddings stored in a vector database for search use-cases. * - "search_query": Used for embeddings of search queries run against a vector DB to find relevant documents. * - "classification": Used for embeddings passed through a text classifier. * - "clustering": Used for embeddings run through a clustering algorithm. */ inputType: z .enum(['search_document', 'search_query', 'classification', 'clustering']) .optional(), /** * Specifies how the API will handle inputs longer than the maximum token length. * Default is `END`. * * - "NONE": If selected, when the input exceeds the maximum input token length will return an error. * - "START": Will discard the start of the input until the remaining input is exactly the maximum input token length for the model. * - "END": Will discard the end of the input until the remaining input is exactly the maximum input token length for the model. */ truncate: z.enum(['NONE', 'START', 'END']).optional(), }); export type CohereEmbeddingOptions = z.infer<typeof cohereEmbeddingOptions>; --- File: /ai/packages/cohere/src/cohere-error.ts --- import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; const cohereErrorDataSchema = z.object({ message: z.string(), }); export type CohereErrorData = z.infer<typeof cohereErrorDataSchema>; export const cohereFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: cohereErrorDataSchema, errorToMessage: data => data.message, }); --- File: /ai/packages/cohere/src/cohere-prepare-tools.test.ts --- import { prepareTools } from './cohere-prepare-tools'; it('should return undefined tools when no tools are provided', () => { const result = prepareTools({ tools: [], }); expect(result).toStrictEqual({ tools: undefined, toolChoice: undefined, toolWarnings: [], }); }); it('should process function tools correctly', () => { const functionTool = { type: 'function' as const, name: 'testFunction', description: 'test description', inputSchema: { type: 'object' as const, properties: {} }, }; const result = prepareTools({ tools: [functionTool], }); expect(result).toStrictEqual({ tools: [ { type: 'function', function: { name: 'testFunction', description: 'test description', parameters: { type: 'object' as const, properties: {} }, }, }, ], toolChoice: undefined, toolWarnings: [], }); }); it('should add warnings for provider-defined tools', () => { const result = prepareTools({ tools: [ { type: 'provider-defined' as const, id: 'provider.tool', name: 'tool', args: {}, }, ], }); expect(result).toStrictEqual({ tools: [], toolChoice: undefined, toolWarnings: [ { type: 'unsupported-tool', tool: { type: 'provider-defined' as const, id: 'provider.tool', name: 'tool', args: {}, }, }, ], }); }); describe('tool choice handling', () => { const basicTool = { type: 'function' as const, name: 'testFunction', description: 'test description', inputSchema: { type: 'object' as const, properties: {} }, }; it('should handle auto tool choice', () => { const result = prepareTools({ tools: [basicTool], toolChoice: { type: 'auto' }, }); expect(result.toolChoice).toBe(undefined); }); it('should handle none tool choice', () => { const result = prepareTools({ tools: [basicTool], toolChoice: { type: 'none' }, }); expect(result).toStrictEqual({ tools: [ { type: 'function', function: { name: 'testFunction', description: 'test description', parameters: { type: 'object', properties: {} }, }, }, ], toolChoice: 'NONE', toolWarnings: [], }); }); it('should handle required tool choice', () => { const result = prepareTools({ tools: [basicTool], toolChoice: { type: 'required' }, }); expect(result).toStrictEqual({ tools: [ { type: 'function', function: { name: 'testFunction', description: 'test description', parameters: { type: 'object', properties: {} }, }, }, ], toolChoice: 'REQUIRED', toolWarnings: [], }); }); it('should handle tool type tool choice by filtering tools', () => { const result = prepareTools({ tools: [basicTool], toolChoice: { type: 'tool', toolName: 'testFunction' }, }); expect(result).toStrictEqual({ tools: [ { type: 'function', function: { name: 'testFunction', description: 'test description', parameters: { type: 'object', properties: {} }, }, }, ], toolChoice: 'REQUIRED', toolWarnings: [], }); }); }); --- File: /ai/packages/cohere/src/cohere-prepare-tools.ts --- import { LanguageModelV2CallOptions, LanguageModelV2CallWarning, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { CohereToolChoice } from './cohere-chat-prompt'; export function prepareTools({ tools, toolChoice, }: { tools: LanguageModelV2CallOptions['tools']; toolChoice?: LanguageModelV2CallOptions['toolChoice']; }): { tools: | Array<{ type: 'function'; function: { name: string | undefined; description: string | undefined; parameters: unknown; }; }> | undefined; toolChoice: CohereToolChoice; toolWarnings: LanguageModelV2CallWarning[]; } { // when the tools array is empty, change it to undefined to prevent errors: tools = tools?.length ? tools : undefined; const toolWarnings: LanguageModelV2CallWarning[] = []; if (tools == null) { return { tools: undefined, toolChoice: undefined, toolWarnings }; } const cohereTools: Array<{ type: 'function'; function: { name: string; description: string | undefined; parameters: unknown; }; }> = []; for (const tool of tools) { if (tool.type === 'provider-defined') { toolWarnings.push({ type: 'unsupported-tool', tool }); } else { cohereTools.push({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.inputSchema, }, }); } } if (toolChoice == null) { return { tools: cohereTools, toolChoice: undefined, toolWarnings }; } const type = toolChoice.type; switch (type) { case 'auto': return { tools: cohereTools, toolChoice: undefined, toolWarnings }; case 'none': return { tools: cohereTools, toolChoice: 'NONE', toolWarnings }; case 'required': return { tools: cohereTools, toolChoice: 'REQUIRED', toolWarnings }; case 'tool': return { tools: cohereTools.filter( tool => tool.function.name === toolChoice.toolName, ), toolChoice: 'REQUIRED', toolWarnings, }; default: { const _exhaustiveCheck: never = type; throw new UnsupportedFunctionalityError({ functionality: `tool choice type: ${_exhaustiveCheck}`, }); } } } --- File: /ai/packages/cohere/src/cohere-provider.ts --- import { EmbeddingModelV2, LanguageModelV2, NoSuchModelError, ProviderV2, } from '@ai-sdk/provider'; import { FetchFunction, generateId, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { CohereChatLanguageModel } from './cohere-chat-language-model'; import { CohereChatModelId } from './cohere-chat-options'; import { CohereEmbeddingModel } from './cohere-embedding-model'; import { CohereEmbeddingModelId } from './cohere-embedding-options'; export interface CohereProvider extends ProviderV2 { (modelId: CohereChatModelId): LanguageModelV2; /** Creates a model for text generation. */ languageModel(modelId: CohereChatModelId): LanguageModelV2; embedding(modelId: CohereEmbeddingModelId): EmbeddingModelV2<string>; textEmbeddingModel(modelId: CohereEmbeddingModelId): EmbeddingModelV2<string>; } export interface CohereProviderSettings { /** Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.cohere.com/v2`. */ baseURL?: string; /** API key that is being send using the `Authorization` header. It defaults to the `COHERE_API_KEY` environment variable. */ apiKey?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; /** Optional function to generate a unique ID for each request. */ generateId?: () => string; } /** Create a Cohere AI provider instance. */ export function createCohere( options: CohereProviderSettings = {}, ): CohereProvider { const baseURL = withoutTrailingSlash(options.baseURL) ?? 'https://api.cohere.com/v2'; const getHeaders = () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'COHERE_API_KEY', description: 'Cohere', })}`, ...options.headers, }); const createChatModel = (modelId: CohereChatModelId) => new CohereChatLanguageModel(modelId, { provider: 'cohere.chat', baseURL, headers: getHeaders, fetch: options.fetch, generateId: options.generateId ?? generateId, }); const createTextEmbeddingModel = (modelId: CohereEmbeddingModelId) => new CohereEmbeddingModel(modelId, { provider: 'cohere.textEmbedding', baseURL, headers: getHeaders, fetch: options.fetch, }); const provider = function (modelId: CohereChatModelId) { if (new.target) { throw new Error( 'The Cohere model function cannot be called with the new keyword.', ); } return createChatModel(modelId); }; provider.languageModel = createChatModel; provider.embedding = createTextEmbeddingModel; provider.textEmbeddingModel = createTextEmbeddingModel; provider.imageModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'imageModel' }); }; return provider; } /** Default Cohere provider instance. */ export const cohere = createCohere(); --- File: /ai/packages/cohere/src/convert-to-cohere-chat-prompt.test.ts --- import { convertToCohereChatPrompt } from './convert-to-cohere-chat-prompt'; describe('convert to cohere chat prompt', () => { describe('file processing', () => { it('should extract documents from file parts', () => { const result = convertToCohereChatPrompt([ { role: 'user', content: [ { type: 'text', text: 'Analyze this file: ' }, { type: 'file', data: Buffer.from('This is file content'), mediaType: 'text/plain', filename: 'test.txt', }, ], }, ]); expect(result).toEqual({ messages: [ { role: 'user', content: 'Analyze this file: ', }, ], documents: [ { data: { text: 'This is file content', title: 'test.txt', }, }, ], warnings: [], }); }); it('should throw error for unsupported media types', () => { expect(() => { convertToCohereChatPrompt([ { role: 'user', content: [ { type: 'file', data: Buffer.from('PDF content'), mediaType: 'application/pdf', filename: 'test.pdf', }, ], }, ]); }).toThrow("Media type 'application/pdf' is not supported"); }); }); describe('tool messages', () => { it('should convert a tool call into a cohere chatbot message', async () => { const result = convertToCohereChatPrompt([ { role: 'assistant', content: [ { type: 'text', text: 'Calling a tool', }, { type: 'tool-call', toolName: 'tool-1', toolCallId: 'tool-call-1', input: { test: 'This is a tool message' }, }, ], }, ]); expect(result).toEqual({ messages: [ { content: undefined, role: 'assistant', tool_calls: [ { id: 'tool-call-1', type: 'function', function: { name: 'tool-1', arguments: JSON.stringify({ test: 'This is a tool message' }), }, }, ], }, ], documents: [], warnings: [], }); }); it('should convert a single tool result into a cohere tool message', async () => { const result = convertToCohereChatPrompt([ { role: 'tool', content: [ { type: 'tool-result', toolName: 'tool-1', toolCallId: 'tool-call-1', output: { type: 'json', value: { test: 'This is a tool message' }, }, }, ], }, ]); expect(result).toEqual({ messages: [ { role: 'tool', content: JSON.stringify({ test: 'This is a tool message' }), tool_call_id: 'tool-call-1', }, ], documents: [], warnings: [], }); }); it('should convert multiple tool results into a cohere tool message', async () => { const result = convertToCohereChatPrompt([ { role: 'tool', content: [ { type: 'tool-result', toolName: 'tool-1', toolCallId: 'tool-call-1', output: { type: 'json', value: { test: 'This is a tool message' }, }, }, { type: 'tool-result', toolName: 'tool-2', toolCallId: 'tool-call-2', output: { type: 'json', value: { something: 'else' } }, }, ], }, ]); expect(result).toEqual({ messages: [ { role: 'tool', content: JSON.stringify({ test: 'This is a tool message' }), tool_call_id: 'tool-call-1', }, { role: 'tool', content: JSON.stringify({ something: 'else' }), tool_call_id: 'tool-call-2', }, ], documents: [], warnings: [], }); }); }); }); --- File: /ai/packages/cohere/src/convert-to-cohere-chat-prompt.ts --- import { LanguageModelV2CallWarning, LanguageModelV2Prompt, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { CohereAssistantMessage, CohereChatPrompt } from './cohere-chat-prompt'; export function convertToCohereChatPrompt(prompt: LanguageModelV2Prompt): { messages: CohereChatPrompt; documents: Array<{ data: { text: string; title?: string }; }>; warnings: LanguageModelV2CallWarning[]; } { const messages: CohereChatPrompt = []; const documents: Array<{ data: { text: string; title?: string } }> = []; const warnings: LanguageModelV2CallWarning[] = []; for (const { role, content } of prompt) { switch (role) { case 'system': { messages.push({ role: 'system', content }); break; } case 'user': { messages.push({ role: 'user', content: content .map(part => { switch (part.type) { case 'text': { return part.text; } case 'file': { // Extract documents for RAG let textContent: string; if (typeof part.data === 'string') { // Base64 or text data textContent = part.data; } else if (part.data instanceof Uint8Array) { // Check if the media type is supported for text extraction if ( !( part.mediaType?.startsWith('text/') || part.mediaType === 'application/json' ) ) { throw new UnsupportedFunctionalityError({ functionality: `document media type: ${part.mediaType}`, message: `Media type '${part.mediaType}' is not supported. Supported media types are: text/* and application/json.`, }); } textContent = new TextDecoder().decode(part.data); } else { throw new UnsupportedFunctionalityError({ functionality: 'File URL data', message: 'URLs should be downloaded by the AI SDK and not reach this point. This indicates a configuration issue.', }); } documents.push({ data: { text: textContent, title: part.filename, }, }); // Files are handled separately via the documents parameter // Return empty string to not include file content in message text return ''; } } }) .join(''), }); break; } case 'assistant': { let text = ''; const toolCalls: CohereAssistantMessage['tool_calls'] = []; for (const part of content) { switch (part.type) { case 'text': { text += part.text; break; } case 'tool-call': { toolCalls.push({ id: part.toolCallId, type: 'function' as const, function: { name: part.toolName, arguments: JSON.stringify(part.input), }, }); break; } } } messages.push({ role: 'assistant', content: toolCalls.length > 0 ? undefined : text, tool_calls: toolCalls.length > 0 ? toolCalls : undefined, tool_plan: undefined, }); break; } case 'tool': { messages.push( ...content.map(toolResult => { const output = toolResult.output; let contentValue: string; switch (output.type) { case 'text': case 'error-text': contentValue = output.value; break; case 'content': case 'json': case 'error-json': contentValue = JSON.stringify(output.value); break; } return { role: 'tool' as const, content: contentValue, tool_call_id: toolResult.toolCallId, }; }), ); break; } default: { const _exhaustiveCheck: never = role; throw new Error(`Unsupported role: ${_exhaustiveCheck}`); } } } return { messages, documents, warnings }; } --- File: /ai/packages/cohere/src/index.ts --- export { cohere, createCohere } from './cohere-provider'; export type { CohereProvider, CohereProviderSettings } from './cohere-provider'; --- File: /ai/packages/cohere/src/map-cohere-finish-reason.ts --- import { LanguageModelV2FinishReason } from '@ai-sdk/provider'; export function mapCohereFinishReason( finishReason: string | null | undefined, ): LanguageModelV2FinishReason { switch (finishReason) { case 'COMPLETE': case 'STOP_SEQUENCE': return 'stop'; case 'MAX_TOKENS': return 'length'; case 'ERROR': return 'error'; case 'TOOL_CALL': return 'tool-calls'; default: return 'unknown'; } } --- File: /ai/packages/cohere/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/cohere/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/cohere/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/deepgram/src/deepgram-api-types.ts --- export type DeepgramTranscriptionAPITypes = { // Base parameters language?: string; model?: string; // Formatting options smart_format?: boolean; punctuate?: boolean; paragraphs?: boolean; // Summarization and analysis summarize?: 'v2' | false; topics?: boolean; intents?: boolean; sentiment?: boolean; // Entity detection detect_entities?: boolean; // Redaction options redact?: string | string[]; replace?: string; // Search and keywords search?: string; keyterm?: string; // Speaker-related features diarize?: boolean; utterances?: boolean; utt_split?: number; // Miscellaneous filler_words?: boolean; }; --- File: /ai/packages/deepgram/src/deepgram-config.ts --- import { FetchFunction } from '@ai-sdk/provider-utils'; export type DeepgramConfig = { provider: string; url: (options: { modelId: string; path: string }) => string; headers: () => Record<string, string | undefined>; fetch?: FetchFunction; generateId?: () => string; }; --- File: /ai/packages/deepgram/src/deepgram-error.test.ts --- import { safeParseJSON } from '@ai-sdk/provider-utils'; import { deepgramErrorDataSchema } from './deepgram-error'; describe('deepgramErrorDataSchema', () => { it('should parse Deepgram resource exhausted error', async () => { const error = ` {"error":{"message":"{\\n \\"error\\": {\\n \\"code\\": 429,\\n \\"message\\": \\"Resource has been exhausted (e.g. check quota).\\",\\n \\"status\\": \\"RESOURCE_EXHAUSTED\\"\\n }\\n}\\n","code":429}} `; const result = await safeParseJSON({ text: error, schema: deepgramErrorDataSchema, }); expect(result).toStrictEqual({ success: true, value: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, rawValue: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, }); }); }); --- File: /ai/packages/deepgram/src/deepgram-error.ts --- import { z } from 'zod/v4'; import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; export const deepgramErrorDataSchema = z.object({ error: z.object({ message: z.string(), code: z.number(), }), }); export type DeepgramErrorData = z.infer<typeof deepgramErrorDataSchema>; export const deepgramFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: deepgramErrorDataSchema, errorToMessage: data => data.error.message, }); --- File: /ai/packages/deepgram/src/deepgram-provider.ts --- import { TranscriptionModelV2, ProviderV2, NoSuchModelError, } from '@ai-sdk/provider'; import { FetchFunction, loadApiKey } from '@ai-sdk/provider-utils'; import { DeepgramTranscriptionModel } from './deepgram-transcription-model'; import { DeepgramTranscriptionModelId } from './deepgram-transcription-options'; export interface DeepgramProvider extends ProviderV2 { ( modelId: 'nova-3', settings?: {}, ): { transcription: DeepgramTranscriptionModel; }; /** Creates a model for transcription. */ transcription(modelId: DeepgramTranscriptionModelId): TranscriptionModelV2; } export interface DeepgramProviderSettings { /** API key for authenticating requests. */ apiKey?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } /** Create an Deepgram provider instance. */ export function createDeepgram( options: DeepgramProviderSettings = {}, ): DeepgramProvider { const getHeaders = () => ({ authorization: `Token ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'DEEPGRAM_API_KEY', description: 'Deepgram', })}`, ...options.headers, }); const createTranscriptionModel = (modelId: DeepgramTranscriptionModelId) => new DeepgramTranscriptionModel(modelId, { provider: `deepgram.transcription`, url: ({ path }) => `https://api.deepgram.com${path}`, headers: getHeaders, fetch: options.fetch, }); const provider = function (modelId: DeepgramTranscriptionModelId) { return { transcription: createTranscriptionModel(modelId), }; }; provider.transcription = createTranscriptionModel; provider.transcriptionModel = createTranscriptionModel; // Required ProviderV2 methods that are not supported provider.languageModel = () => { throw new NoSuchModelError({ modelId: 'unknown', modelType: 'languageModel', message: 'Deepgram does not provide language models', }); }; provider.textEmbeddingModel = () => { throw new NoSuchModelError({ modelId: 'unknown', modelType: 'textEmbeddingModel', message: 'Deepgram does not provide text embedding models', }); }; provider.imageModel = () => { throw new NoSuchModelError({ modelId: 'unknown', modelType: 'imageModel', message: 'Deepgram does not provide image models', }); }; return provider as DeepgramProvider; } /** Default Deepgram provider instance. */ export const deepgram = createDeepgram(); --- File: /ai/packages/deepgram/src/deepgram-transcription-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { DeepgramTranscriptionModel } from './deepgram-transcription-model'; import { createDeepgram } from './deepgram-provider'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; const audioData = await readFile(path.join(__dirname, 'transcript-test.mp3')); const provider = createDeepgram({ apiKey: 'test-api-key' }); const model = provider.transcription('nova-3'); const server = createTestServer({ 'https://api.deepgram.com/v1/listen': {}, }); describe('doGenerate', () => { function prepareJsonResponse({ headers, }: { headers?: Record<string, string>; } = {}) { server.urls['https://api.deepgram.com/v1/listen'].response = { type: 'json-value', headers, body: { metadata: { transaction_key: 'deprecated', request_id: '2479c8c8-8185-40ac-9ac6-f0874419f793', sha256: '154e291ecfa8be6ab8343560bcc109008fa7853eb5372533e8efdefc9b504c33', created: '2024-02-06T19:56:16.180Z', duration: 25.933313, channels: 1, models: ['30089e05-99d1-4376-b32e-c263170674af'], model_info: { '30089e05-99d1-4376-b32e-c263170674af': { name: '2-general-nova', version: '2024-01-09.29447', arch: 'nova-3', }, }, }, results: { channels: [ { alternatives: [ { transcript: 'Hello world!', confidence: 0.99902344, words: [ { word: 'hello', start: 0.08, end: 0.32, confidence: 0.9975586, punctuated_word: 'Hello.', }, { word: 'world', start: 0.32, end: 0.79999995, confidence: 0.9921875, punctuated_word: 'World', }, ], paragraphs: { transcript: 'Hello world!', paragraphs: [ { sentences: [ { text: 'Hello world!', start: 0.08, end: 0.32, }, ], num_words: 2, start: 0.08, end: 0.79999995, }, ], }, }, ], }, ], }, }, }; } it('should pass the model', async () => { prepareJsonResponse(); await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(await server.calls[0].requestBodyMultipart).toMatchObject({}); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createDeepgram({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.transcription('nova-3').doGenerate({ audio: audioData, mediaType: 'audio/wav', headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toMatchObject({ authorization: 'Token test-api-key', 'content-type': 'audio/wav', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should extract the transcription text', async () => { prepareJsonResponse(); const result = await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.text).toBe('Hello world!'); }); it('should include response data with timestamp, modelId and headers', async () => { prepareJsonResponse({ headers: { 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); const testDate = new Date(0); const customModel = new DeepgramTranscriptionModel('nova-3', { provider: 'test-provider', url: () => 'https://api.deepgram.com/v1/listen', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.response).toMatchObject({ timestamp: testDate, modelId: 'nova-3', headers: { 'content-type': 'application/json', 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); }); it('should use real date when no custom date provider is specified', async () => { prepareJsonResponse(); const testDate = new Date(0); const customModel = new DeepgramTranscriptionModel('nova-3', { provider: 'test-provider', url: () => 'https://api.deepgram.com/v1/listen', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.response.timestamp.getTime()).toEqual(testDate.getTime()); expect(result.response.modelId).toBe('nova-3'); }); }); --- File: /ai/packages/deepgram/src/deepgram-transcription-model.ts --- import { TranscriptionModelV2, TranscriptionModelV2CallWarning, } from '@ai-sdk/provider'; import { combineHeaders, convertBase64ToUint8Array, createJsonResponseHandler, parseProviderOptions, postToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { DeepgramConfig } from './deepgram-config'; import { deepgramFailedResponseHandler } from './deepgram-error'; import { DeepgramTranscriptionModelId } from './deepgram-transcription-options'; import { DeepgramTranscriptionAPITypes } from './deepgram-api-types'; // https://developers.deepgram.com/docs/pre-recorded-audio#results const deepgramProviderOptionsSchema = z.object({ /** Language to use for transcription. If not specified, Deepgram will auto-detect the language. */ language: z.string().nullish(), /** Whether to use smart formatting, which formats written-out numbers, dates, times, etc. */ smartFormat: z.boolean().nullish(), /** Whether to add punctuation to the transcript. */ punctuate: z.boolean().nullish(), /** Whether to format the transcript into paragraphs. */ paragraphs: z.boolean().nullish(), /** Whether to generate a summary of the transcript. Use 'v2' for the latest version or false to disable. */ summarize: z.union([z.literal('v2'), z.literal(false)]).nullish(), /** Whether to identify topics in the transcript. */ topics: z.boolean().nullish(), /** Whether to identify intents in the transcript. */ intents: z.boolean().nullish(), /** Whether to analyze sentiment in the transcript. */ sentiment: z.boolean().nullish(), /** Whether to detect and tag named entities in the transcript. */ detectEntities: z.boolean().nullish(), /** Specify terms or patterns to redact from the transcript. Can be a string or array of strings. */ redact: z.union([z.string(), z.array(z.string())]).nullish(), /** String to replace redacted content with. */ replace: z.string().nullish(), /** Term or phrase to search for in the transcript. */ search: z.string().nullish(), /** Key term to identify in the transcript. */ keyterm: z.string().nullish(), /** Whether to identify different speakers in the audio. */ diarize: z.boolean().nullish(), /** Whether to segment the transcript into utterances. */ utterances: z.boolean().nullish(), /** Minimum duration of silence (in seconds) to trigger a new utterance. */ uttSplit: z.number().nullish(), /** Whether to include filler words (um, uh, etc.) in the transcript. */ fillerWords: z.boolean().nullish(), }); export type DeepgramTranscriptionCallOptions = z.infer< typeof deepgramProviderOptionsSchema >; interface DeepgramTranscriptionModelConfig extends DeepgramConfig { _internal?: { currentDate?: () => Date; }; } export class DeepgramTranscriptionModel implements TranscriptionModelV2 { readonly specificationVersion = 'v2'; get provider(): string { return this.config.provider; } constructor( readonly modelId: DeepgramTranscriptionModelId, private readonly config: DeepgramTranscriptionModelConfig, ) {} private async getArgs({ providerOptions, }: Parameters<TranscriptionModelV2['doGenerate']>[0]) { const warnings: TranscriptionModelV2CallWarning[] = []; // Parse provider options const deepgramOptions = await parseProviderOptions({ provider: 'deepgram', providerOptions, schema: deepgramProviderOptionsSchema, }); const body: DeepgramTranscriptionAPITypes = { model: this.modelId, diarize: true, }; // Add provider-specific options if (deepgramOptions) { body.detect_entities = deepgramOptions.detectEntities ?? undefined; body.filler_words = deepgramOptions.fillerWords ?? undefined; body.language = deepgramOptions.language ?? undefined; body.punctuate = deepgramOptions.punctuate ?? undefined; body.redact = deepgramOptions.redact ?? undefined; body.search = deepgramOptions.search ?? undefined; body.smart_format = deepgramOptions.smartFormat ?? undefined; body.summarize = deepgramOptions.summarize ?? undefined; body.topics = deepgramOptions.topics ?? undefined; body.utterances = deepgramOptions.utterances ?? undefined; body.utt_split = deepgramOptions.uttSplit ?? undefined; if (typeof deepgramOptions.diarize === 'boolean') { body.diarize = deepgramOptions.diarize; } } // Convert body to URL query parameters const queryParams = new URLSearchParams(); for (const [key, value] of Object.entries(body)) { if (value !== undefined) { queryParams.append(key, String(value)); } } return { queryParams, warnings, }; } async doGenerate( options: Parameters<TranscriptionModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<TranscriptionModelV2['doGenerate']>>> { const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { queryParams, warnings } = await this.getArgs(options); const { value: response, responseHeaders, rawValue: rawResponse, } = await postToApi({ url: this.config.url({ path: '/v1/listen', modelId: this.modelId, }) + '?' + queryParams.toString(), headers: { ...combineHeaders(this.config.headers(), options.headers), 'Content-Type': options.mediaType, }, body: { content: options.audio, values: options.audio, }, failedResponseHandler: deepgramFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( deepgramTranscriptionResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); return { text: response.results?.channels.at(0)?.alternatives.at(0)?.transcript ?? '', segments: response.results?.channels[0].alternatives[0].words?.map(word => ({ text: word.word, startSecond: word.start, endSecond: word.end, })) ?? [], language: undefined, durationInSeconds: response.metadata?.duration ?? undefined, warnings, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, body: rawResponse, }, }; } } const deepgramTranscriptionResponseSchema = z.object({ metadata: z .object({ duration: z.number(), }) .nullish(), results: z .object({ channels: z.array( z.object({ alternatives: z.array( z.object({ transcript: z.string(), words: z.array( z.object({ word: z.string(), start: z.number(), end: z.number(), }), ), }), ), }), ), }) .nullish(), }); --- File: /ai/packages/deepgram/src/deepgram-transcription-options.ts --- export type DeepgramTranscriptionModelId = | 'base' | 'base-general' | 'base-meeting' | 'base-phonecall' | 'base-finance' | 'base-conversationalai' | 'base-voicemail' | 'base-video' | 'enhanced' | 'enhanced-general' | 'enhanced-meeting' | 'enhanced-phonecall' | 'enhanced-finance' | 'nova' | 'nova-general' | 'nova-phonecall' | 'nova-medical' | 'nova-2' | 'nova-2-general' | 'nova-2-meeting' | 'nova-2-phonecall' | 'nova-2-finance' | 'nova-2-conversationalai' | 'nova-2-voicemail' | 'nova-2-video' | 'nova-2-medical' | 'nova-2-drivethru' | 'nova-2-automotive' | 'nova-2-atc' | 'nova-3' | 'nova-3-general' | 'nova-3-medical' | (string & {}); --- File: /ai/packages/deepgram/src/index.ts --- export { createDeepgram, deepgram } from './deepgram-provider'; export type { DeepgramProvider, DeepgramProviderSettings, } from './deepgram-provider'; --- File: /ai/packages/deepgram/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/deepgram/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/deepgram/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/deepinfra/src/deepinfra-chat-options.ts --- // https://deepinfra.com/models/text-generation export type DeepInfraChatModelId = | 'meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8' | 'meta-llama/Llama-4-Scout-17B-16E-Instruct' | 'meta-llama/Llama-3.3-70B-Instruct' | 'meta-llama/Llama-3.3-70B-Instruct-Turbo' | 'meta-llama/Meta-Llama-3.1-70B-Instruct' | 'meta-llama/Meta-Llama-3.1-8B-Instruct' | 'meta-llama/Meta-Llama-3.1-405B-Instruct' | 'Qwen/QwQ-32B-Preview' | 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo' | 'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo' | 'Qwen/Qwen2.5-Coder-32B-Instruct' | 'nvidia/Llama-3.1-Nemotron-70B-Instruct' | 'Qwen/Qwen2.5-72B-Instruct' | 'meta-llama/Llama-3.2-90B-Vision-Instruct' | 'meta-llama/Llama-3.2-11B-Vision-Instruct' | 'microsoft/WizardLM-2-8x22B' | '01-ai/Yi-34B-Chat' | 'Austism/chronos-hermes-13b-v2' | 'Gryphe/MythoMax-L2-13b' | 'Gryphe/MythoMax-L2-13b-turbo' | 'HuggingFaceH4/zephyr-orpo-141b-A35b-v0.1' | 'KoboldAI/LLaMA2-13B-Tiefighter' | 'NousResearch/Hermes-3-Llama-3.1-405B' | 'Phind/Phind-CodeLlama-34B-v2' | 'Qwen/Qwen2-72B-Instruct' | 'Qwen/Qwen2-7B-Instruct' | 'Qwen/Qwen2.5-7B-Instruct' | 'Qwen/Qwen2.5-Coder-7B' | 'Sao10K/L3-70B-Euryale-v2.1' | 'Sao10K/L3-8B-Lunaris-v1' | 'Sao10K/L3.1-70B-Euryale-v2.2' | 'bigcode/starcoder2-15b' | 'bigcode/starcoder2-15b-instruct-v0.1' | 'codellama/CodeLlama-34b-Instruct-hf' | 'codellama/CodeLlama-70b-Instruct-hf' | 'cognitivecomputations/dolphin-2.6-mixtral-8x7b' | 'cognitivecomputations/dolphin-2.9.1-llama-3-70b' | 'databricks/dbrx-instruct' | 'deepinfra/airoboros-70b' | 'deepseek-ai/DeepSeek-V3' | 'google/codegemma-7b-it' | 'google/gemma-1.1-7b-it' | 'google/gemma-2-27b-it' | 'google/gemma-2-9b-it' | 'lizpreciatior/lzlv_70b_fp16_hf' | 'mattshumer/Reflection-Llama-3.1-70B' | 'meta-llama/Llama-2-13b-chat-hf' | 'meta-llama/Llama-2-70b-chat-hf' | 'meta-llama/Llama-2-7b-chat-hf' | 'meta-llama/Llama-3.2-1B-Instruct' | 'meta-llama/Llama-3.2-3B-Instruct' | 'meta-llama/Meta-Llama-3-70B-Instruct' | 'meta-llama/Meta-Llama-3-8B-Instruct' | 'microsoft/Phi-3-medium-4k-instruct' | 'microsoft/WizardLM-2-7B' | 'mistralai/Mistral-7B-Instruct-v0.1' | 'mistralai/Mistral-7B-Instruct-v0.2' | 'mistralai/Mistral-7B-Instruct-v0.3' | 'mistralai/Mistral-Nemo-Instruct-2407' | 'mistralai/Mixtral-8x22B-Instruct-v0.1' | 'mistralai/Mixtral-8x22B-v0.1' | 'mistralai/Mixtral-8x7B-Instruct-v0.1' | 'nvidia/Nemotron-4-340B-Instruct' | 'openbmb/MiniCPM-Llama3-V-2_5' | 'openchat/openchat-3.6-8b' | 'openchat/openchat_3.5' | (string & {}); --- File: /ai/packages/deepinfra/src/deepinfra-completion-options.ts --- import { DeepInfraChatModelId } from './deepinfra-chat-options'; // Use the same model IDs as chat export type DeepInfraCompletionModelId = DeepInfraChatModelId; --- File: /ai/packages/deepinfra/src/deepinfra-embedding-options.ts --- // https://deepinfra.com/models/embeddings export type DeepInfraEmbeddingModelId = | 'BAAI/bge-base-en-v1.5' | 'BAAI/bge-large-en-v1.5' | 'BAAI/bge-m3' | 'intfloat/e5-base-v2' | 'intfloat/e5-large-v2' | 'intfloat/multilingual-e5-large' | 'sentence-transformers/all-MiniLM-L12-v2' | 'sentence-transformers/all-MiniLM-L6-v2' | 'sentence-transformers/all-mpnet-base-v2' | 'sentence-transformers/clip-ViT-B-32' | 'sentence-transformers/clip-ViT-B-32-multilingual-v1' | 'sentence-transformers/multi-qa-mpnet-base-dot-v1' | 'sentence-transformers/paraphrase-MiniLM-L6-v2' | 'shibing624/text2vec-base-chinese' | 'thenlper/gte-base' | 'thenlper/gte-large' | (string & {}); --- File: /ai/packages/deepinfra/src/deepinfra-image-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { describe, expect, it } from 'vitest'; import { DeepInfraImageModel } from './deepinfra-image-model'; import { FetchFunction } from '@ai-sdk/provider-utils'; const prompt = 'A cute baby sea otter'; function createBasicModel({ headers, fetch, currentDate, }: { headers?: () => Record<string, string>; fetch?: FetchFunction; currentDate?: () => Date; } = {}) { return new DeepInfraImageModel('stability-ai/sdxl', { provider: 'deepinfra', baseURL: 'https://api.example.com', headers: headers ?? (() => ({ 'api-key': 'test-key' })), fetch, _internal: { currentDate, }, }); } describe('DeepInfraImageModel', () => { const testDate = new Date('2024-01-01T00:00:00Z'); const server = createTestServer({ 'https://api.example.com/*': { response: { type: 'json-value', body: { images: ['data:image/png;base64,test-image-data'], }, }, }, }); describe('doGenerate', () => { it('should pass the correct parameters including aspect ratio and seed', async () => { const model = createBasicModel(); await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: '16:9', seed: 42, providerOptions: { deepinfra: { additional_param: 'value' } }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ prompt, aspect_ratio: '16:9', seed: 42, num_images: 1, additional_param: 'value', }); }); it('should call the correct url', async () => { const model = createBasicModel(); await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(server.calls[0].requestMethod).toStrictEqual('POST'); expect(server.calls[0].requestUrl).toStrictEqual( 'https://api.example.com/stability-ai/sdxl', ); }); it('should pass headers', async () => { const modelWithHeaders = createBasicModel({ headers: () => ({ 'Custom-Provider-Header': 'provider-header-value', }), }); await modelWithHeaders.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should handle API errors', async () => { server.urls['https://api.example.com/*'].response = { type: 'error', status: 400, body: JSON.stringify({ error: { message: 'Bad Request', }, }), }; const model = createBasicModel(); await expect( model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }), ).rejects.toThrow('Bad Request'); }); it('should handle size parameter', async () => { const model = createBasicModel(); await model.doGenerate({ prompt, n: 1, size: '1024x768', aspectRatio: undefined, seed: 42, providerOptions: {}, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ prompt, width: '1024', height: '768', seed: 42, num_images: 1, }); }); it('should respect the abort signal', async () => { const model = createBasicModel(); const controller = new AbortController(); const generatePromise = model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, abortSignal: controller.signal, }); controller.abort(); await expect(generatePromise).rejects.toThrow( 'This operation was aborted', ); }); describe('response metadata', () => { it('should include timestamp, headers and modelId in response', async () => { const model = createBasicModel({ currentDate: () => testDate, }); const result = await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.response).toStrictEqual({ timestamp: testDate, modelId: 'stability-ai/sdxl', headers: expect.any(Object), }); }); it('should include response headers from API call', async () => { server.urls['https://api.example.com/*'].response = { type: 'json-value', headers: { 'x-request-id': 'test-request-id', }, body: { images: ['data:image/png;base64,test-image-data'], }, }; const model = createBasicModel(); const result = await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.response.headers).toStrictEqual({ 'content-length': '52', 'x-request-id': 'test-request-id', 'content-type': 'application/json', }); }); }); }); describe('constructor', () => { it('should expose correct provider and model information', () => { const model = createBasicModel(); expect(model.provider).toBe('deepinfra'); expect(model.modelId).toBe('stability-ai/sdxl'); expect(model.specificationVersion).toBe('v2'); expect(model.maxImagesPerCall).toBe(1); }); }); }); --- File: /ai/packages/deepinfra/src/deepinfra-image-model.ts --- import { ImageModelV2, ImageModelV2CallWarning } from '@ai-sdk/provider'; import { FetchFunction, combineHeaders, createJsonErrorResponseHandler, createJsonResponseHandler, postJsonToApi, } from '@ai-sdk/provider-utils'; import { DeepInfraImageModelId } from './deepinfra-image-settings'; import { z } from 'zod/v4'; interface DeepInfraImageModelConfig { provider: string; baseURL: string; headers: () => Record<string, string>; fetch?: FetchFunction; _internal?: { currentDate?: () => Date; }; } export class DeepInfraImageModel implements ImageModelV2 { readonly specificationVersion = 'v2'; readonly maxImagesPerCall = 1; get provider(): string { return this.config.provider; } constructor( readonly modelId: DeepInfraImageModelId, private config: DeepInfraImageModelConfig, ) {} async doGenerate({ prompt, n, size, aspectRatio, seed, providerOptions, headers, abortSignal, }: Parameters<ImageModelV2['doGenerate']>[0]): Promise< Awaited<ReturnType<ImageModelV2['doGenerate']>> > { const warnings: Array<ImageModelV2CallWarning> = []; // Some deepinfra models support size while others support aspect ratio. // Allow passing either and leave it up to the server to validate. const splitSize = size?.split('x'); const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { value: response, responseHeaders } = await postJsonToApi({ url: `${this.config.baseURL}/${this.modelId}`, headers: combineHeaders(this.config.headers(), headers), body: { prompt, num_images: n, ...(aspectRatio && { aspect_ratio: aspectRatio }), ...(splitSize && { width: splitSize[0], height: splitSize[1] }), ...(seed != null && { seed }), ...(providerOptions.deepinfra ?? {}), }, failedResponseHandler: createJsonErrorResponseHandler({ errorSchema: deepInfraErrorSchema, errorToMessage: error => error.detail.error, }), successfulResponseHandler: createJsonResponseHandler( deepInfraImageResponseSchema, ), abortSignal, fetch: this.config.fetch, }); return { images: response.images.map(image => image.replace(/^data:image\/\w+;base64,/, ''), ), warnings, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, }, }; } } export const deepInfraErrorSchema = z.object({ detail: z.object({ error: z.string(), }), }); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency export const deepInfraImageResponseSchema = z.object({ images: z.array(z.string()), }); --- File: /ai/packages/deepinfra/src/deepinfra-image-settings.ts --- // https://deepinfra.com/models/text-to-image export type DeepInfraImageModelId = | 'stabilityai/sd3.5' | 'black-forest-labs/FLUX-1.1-pro' | 'black-forest-labs/FLUX-1-schnell' | 'black-forest-labs/FLUX-1-dev' | 'black-forest-labs/FLUX-pro' | 'stabilityai/sd3.5-medium' | 'stabilityai/sdxl-turbo' | (string & {}); --- File: /ai/packages/deepinfra/src/deepinfra-provider.test.ts --- import { DeepInfraImageModel } from './deepinfra-image-model'; import { createDeepInfra } from './deepinfra-provider'; import { OpenAICompatibleChatLanguageModel, OpenAICompatibleCompletionLanguageModel, OpenAICompatibleEmbeddingModel, } from '@ai-sdk/openai-compatible'; import { LanguageModelV2, EmbeddingModelV2 } from '@ai-sdk/provider'; import { loadApiKey } from '@ai-sdk/provider-utils'; import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; // Add type assertion for the mocked class const OpenAICompatibleChatLanguageModelMock = OpenAICompatibleChatLanguageModel as unknown as Mock; vi.mock('@ai-sdk/openai-compatible', () => ({ OpenAICompatibleChatLanguageModel: vi.fn(), OpenAICompatibleCompletionLanguageModel: vi.fn(), OpenAICompatibleEmbeddingModel: vi.fn(), })); vi.mock('@ai-sdk/provider-utils', () => ({ loadApiKey: vi.fn().mockReturnValue('mock-api-key'), withoutTrailingSlash: vi.fn(url => url), })); vi.mock('./deepinfra-image-model', () => ({ DeepInfraImageModel: vi.fn(), })); describe('DeepInfraProvider', () => { let mockLanguageModel: LanguageModelV2; let mockEmbeddingModel: EmbeddingModelV2<string>; beforeEach(() => { // Mock implementations of models mockLanguageModel = { // Add any required methods for LanguageModelV2 } as LanguageModelV2; mockEmbeddingModel = { // Add any required methods for EmbeddingModelV2 } as EmbeddingModelV2<string>; // Reset mocks vi.clearAllMocks(); }); describe('createDeepInfra', () => { it('should create a DeepInfraProvider instance with default options', () => { const provider = createDeepInfra(); const model = provider('model-id'); // Use the mocked version const constructorCall = OpenAICompatibleChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; config.headers(); expect(loadApiKey).toHaveBeenCalledWith({ apiKey: undefined, environmentVariableName: 'DEEPINFRA_API_KEY', description: "DeepInfra's API key", }); }); it('should create a DeepInfraProvider instance with custom options', () => { const options = { apiKey: 'custom-key', baseURL: 'https://custom.url', headers: { 'Custom-Header': 'value' }, }; const provider = createDeepInfra(options); const model = provider('model-id'); const constructorCall = OpenAICompatibleChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; config.headers(); expect(loadApiKey).toHaveBeenCalledWith({ apiKey: 'custom-key', environmentVariableName: 'DEEPINFRA_API_KEY', description: "DeepInfra's API key", }); }); it('should return a chat model when called as a function', () => { const provider = createDeepInfra(); const modelId = 'foo-model-id'; const model = provider(modelId); expect(model).toBeInstanceOf(OpenAICompatibleChatLanguageModel); }); }); describe('chatModel', () => { it('should construct a chat model with correct configuration', () => { const provider = createDeepInfra(); const modelId = 'deepinfra-chat-model'; const model = provider.chatModel(modelId); expect(model).toBeInstanceOf(OpenAICompatibleChatLanguageModel); expect(OpenAICompatibleChatLanguageModelMock).toHaveBeenCalledWith( modelId, expect.objectContaining({ provider: 'deepinfra.chat', }), ); }); }); describe('completionModel', () => { it('should construct a completion model with correct configuration', () => { const provider = createDeepInfra(); const modelId = 'deepinfra-completion-model'; const model = provider.completionModel(modelId); expect(model).toBeInstanceOf(OpenAICompatibleCompletionLanguageModel); }); }); describe('textEmbeddingModel', () => { it('should construct a text embedding model with correct configuration', () => { const provider = createDeepInfra(); const modelId = 'deepinfra-embedding-model'; const model = provider.textEmbeddingModel(modelId); expect(model).toBeInstanceOf(OpenAICompatibleEmbeddingModel); }); }); describe('image', () => { it('should construct an image model with correct configuration', () => { const provider = createDeepInfra(); const modelId = 'deepinfra-image-model'; const model = provider.image(modelId); expect(model).toBeInstanceOf(DeepInfraImageModel); expect(DeepInfraImageModel).toHaveBeenCalledWith( modelId, expect.objectContaining({ provider: 'deepinfra.image', baseURL: 'https://api.deepinfra.com/v1/inference', }), ); }); it('should use default settings when none provided', () => { const provider = createDeepInfra(); const modelId = 'deepinfra-image-model'; const model = provider.image(modelId); expect(model).toBeInstanceOf(DeepInfraImageModel); expect(DeepInfraImageModel).toHaveBeenCalledWith( modelId, expect.any(Object), ); }); it('should respect custom baseURL', () => { const customBaseURL = 'https://custom.api.deepinfra.com'; const provider = createDeepInfra({ baseURL: customBaseURL }); const modelId = 'deepinfra-image-model'; const model = provider.image(modelId); expect(DeepInfraImageModel).toHaveBeenCalledWith( modelId, expect.objectContaining({ baseURL: `${customBaseURL}/inference`, }), ); }); }); }); --- File: /ai/packages/deepinfra/src/deepinfra-provider.ts --- import { LanguageModelV2, EmbeddingModelV2, ProviderV2, ImageModelV2, } from '@ai-sdk/provider'; import { OpenAICompatibleChatLanguageModel, OpenAICompatibleCompletionLanguageModel, OpenAICompatibleEmbeddingModel, } from '@ai-sdk/openai-compatible'; import { FetchFunction, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { DeepInfraChatModelId } from './deepinfra-chat-options'; import { DeepInfraEmbeddingModelId } from './deepinfra-embedding-options'; import { DeepInfraCompletionModelId } from './deepinfra-completion-options'; import { DeepInfraImageModelId } from './deepinfra-image-settings'; import { DeepInfraImageModel } from './deepinfra-image-model'; export interface DeepInfraProviderSettings { /** DeepInfra API key. */ apiKey?: string; /** Base URL for the API calls. */ baseURL?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } export interface DeepInfraProvider extends ProviderV2 { /** Creates a model for text generation. */ (modelId: DeepInfraChatModelId): LanguageModelV2; /** Creates a chat model for text generation. */ chatModel(modelId: DeepInfraChatModelId): LanguageModelV2; /** Creates a model for image generation. */ image(modelId: DeepInfraImageModelId): ImageModelV2; /** Creates a model for image generation. */ imageModel(modelId: DeepInfraImageModelId): ImageModelV2; /** Creates a chat model for text generation. */ languageModel(modelId: DeepInfraChatModelId): LanguageModelV2; /** Creates a completion model for text generation. */ completionModel(modelId: DeepInfraCompletionModelId): LanguageModelV2; /** Creates a text embedding model for text generation. */ textEmbeddingModel( modelId: DeepInfraEmbeddingModelId, ): EmbeddingModelV2<string>; } export function createDeepInfra( options: DeepInfraProviderSettings = {}, ): DeepInfraProvider { const baseURL = withoutTrailingSlash( options.baseURL ?? 'https://api.deepinfra.com/v1', ); const getHeaders = () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'DEEPINFRA_API_KEY', description: "DeepInfra's API key", })}`, ...options.headers, }); interface CommonModelConfig { provider: string; url: ({ path }: { path: string }) => string; headers: () => Record<string, string>; fetch?: FetchFunction; } const getCommonModelConfig = (modelType: string): CommonModelConfig => ({ provider: `deepinfra.${modelType}`, url: ({ path }) => `${baseURL}/openai${path}`, headers: getHeaders, fetch: options.fetch, }); const createChatModel = (modelId: DeepInfraChatModelId) => { return new OpenAICompatibleChatLanguageModel( modelId, getCommonModelConfig('chat'), ); }; const createCompletionModel = (modelId: DeepInfraCompletionModelId) => new OpenAICompatibleCompletionLanguageModel( modelId, getCommonModelConfig('completion'), ); const createTextEmbeddingModel = (modelId: DeepInfraEmbeddingModelId) => new OpenAICompatibleEmbeddingModel( modelId, getCommonModelConfig('embedding'), ); const createImageModel = (modelId: DeepInfraImageModelId) => new DeepInfraImageModel(modelId, { ...getCommonModelConfig('image'), baseURL: baseURL ? `${baseURL}/inference` : 'https://api.deepinfra.com/v1/inference', }); const provider = (modelId: DeepInfraChatModelId) => createChatModel(modelId); provider.completionModel = createCompletionModel; provider.chatModel = createChatModel; provider.image = createImageModel; provider.imageModel = createImageModel; provider.languageModel = createChatModel; provider.textEmbeddingModel = createTextEmbeddingModel; return provider; } export const deepinfra = createDeepInfra(); --- File: /ai/packages/deepinfra/src/index.ts --- export { createDeepInfra, deepinfra } from './deepinfra-provider'; export type { DeepInfraProvider, DeepInfraProviderSettings, } from './deepinfra-provider'; export type { OpenAICompatibleErrorData as DeepInfraErrorData } from '@ai-sdk/openai-compatible'; --- File: /ai/packages/deepinfra/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/deepinfra/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/deepinfra/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/deepseek/src/deepseek-chat-options.ts --- // https://api-docs.deepseek.com/quick_start/pricing export type DeepSeekChatModelId = | 'deepseek-chat' | 'deepseek-reasoner' | (string & {}); --- File: /ai/packages/deepseek/src/deepseek-metadata-extractor.test.ts --- import { deepSeekMetadataExtractor } from './deepseek-metadata-extractor'; describe('buildMetadataFromResponse', () => { it('should extract metadata from complete response with usage data', async () => { const response = { usage: { prompt_cache_hit_tokens: 100, prompt_cache_miss_tokens: 50, }, }; const metadata = await deepSeekMetadataExtractor.extractMetadata({ parsedBody: response, }); expect(metadata).toEqual({ deepseek: { promptCacheHitTokens: 100, promptCacheMissTokens: 50, }, }); }); it('should handle missing usage data', async () => { const response = { id: 'test-id', choices: [], }; const metadata = await deepSeekMetadataExtractor.extractMetadata({ parsedBody: response, }); expect(metadata).toBeUndefined(); }); it('should handle invalid response data', async () => { const response = 'invalid data'; const metadata = await deepSeekMetadataExtractor.extractMetadata({ parsedBody: response, }); expect(metadata).toBeUndefined(); }); }); describe('streaming metadata extractor', () => { it('should process streaming chunks and build final metadata', async () => { const extractor = deepSeekMetadataExtractor.createStreamExtractor(); // Process initial chunks without usage data await extractor.processChunk({ choices: [{ finish_reason: null }], }); // Process final chunk with usage data await extractor.processChunk({ choices: [{ finish_reason: 'stop' }], usage: { prompt_cache_hit_tokens: 100, prompt_cache_miss_tokens: 50, }, }); const finalMetadata = extractor.buildMetadata(); expect(finalMetadata).toEqual({ deepseek: { promptCacheHitTokens: 100, promptCacheMissTokens: 50, }, }); }); it('should handle streaming chunks without usage data', async () => { const extractor = deepSeekMetadataExtractor.createStreamExtractor(); await extractor.processChunk({ choices: [{ finish_reason: 'stop' }], }); const finalMetadata = extractor.buildMetadata(); expect(finalMetadata).toBeUndefined(); }); it('should handle invalid streaming chunks', async () => { const extractor = deepSeekMetadataExtractor.createStreamExtractor(); await extractor.processChunk('invalid chunk'); const finalMetadata = extractor.buildMetadata(); expect(finalMetadata).toBeUndefined(); }); it('should only capture usage data from final chunk with stop reason', async () => { const extractor = deepSeekMetadataExtractor.createStreamExtractor(); // Process chunk with usage but no stop reason await extractor.processChunk({ choices: [{ finish_reason: null }], usage: { prompt_cache_hit_tokens: 50, prompt_cache_miss_tokens: 25, }, }); // Process final chunk with different usage data await extractor.processChunk({ choices: [{ finish_reason: 'stop' }], usage: { prompt_cache_hit_tokens: 100, prompt_cache_miss_tokens: 50, }, }); const finalMetadata = extractor.buildMetadata(); expect(finalMetadata).toEqual({ deepseek: { promptCacheHitTokens: 100, promptCacheMissTokens: 50, }, }); }); it('should handle null values in usage data', async () => { const extractor = deepSeekMetadataExtractor.createStreamExtractor(); await extractor.processChunk({ choices: [{ finish_reason: 'stop' }], usage: { prompt_cache_hit_tokens: null, prompt_cache_miss_tokens: 50, }, }); const finalMetadata = extractor.buildMetadata(); expect(finalMetadata).toEqual({ deepseek: { promptCacheHitTokens: NaN, promptCacheMissTokens: 50, }, }); }); }); --- File: /ai/packages/deepseek/src/deepseek-metadata-extractor.ts --- import { MetadataExtractor } from '@ai-sdk/openai-compatible'; import { safeValidateTypes } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; const buildDeepseekMetadata = ( usage: z.infer<typeof deepSeekUsageSchema> | undefined, ) => { return usage == null ? undefined : { deepseek: { promptCacheHitTokens: usage.prompt_cache_hit_tokens ?? NaN, promptCacheMissTokens: usage.prompt_cache_miss_tokens ?? NaN, }, }; }; export const deepSeekMetadataExtractor: MetadataExtractor = { extractMetadata: async ({ parsedBody }: { parsedBody: unknown }) => { const parsed = await safeValidateTypes({ value: parsedBody, schema: deepSeekResponseSchema, }); return !parsed.success || parsed.value.usage == null ? undefined : buildDeepseekMetadata(parsed.value.usage); }, createStreamExtractor: () => { let usage: z.infer<typeof deepSeekUsageSchema> | undefined; return { processChunk: async (chunk: unknown) => { const parsed = await safeValidateTypes({ value: chunk, schema: deepSeekStreamChunkSchema, }); if ( parsed.success && parsed.value.choices?.[0]?.finish_reason === 'stop' && parsed.value.usage ) { usage = parsed.value.usage; } }, buildMetadata: () => buildDeepseekMetadata(usage), }; }, }; const deepSeekUsageSchema = z.object({ prompt_cache_hit_tokens: z.number().nullish(), prompt_cache_miss_tokens: z.number().nullish(), }); const deepSeekResponseSchema = z.object({ usage: deepSeekUsageSchema.nullish(), }); const deepSeekStreamChunkSchema = z.object({ choices: z .array( z.object({ finish_reason: z.string().nullish(), }), ) .nullish(), usage: deepSeekUsageSchema.nullish(), }); --- File: /ai/packages/deepseek/src/deepseek-provider.test.ts --- import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; import { createDeepSeek } from './deepseek-provider'; import { loadApiKey } from '@ai-sdk/provider-utils'; import { OpenAICompatibleChatLanguageModel } from '@ai-sdk/openai-compatible'; // Add type assertion for the mocked class const OpenAICompatibleChatLanguageModelMock = OpenAICompatibleChatLanguageModel as unknown as Mock; vi.mock('@ai-sdk/openai-compatible', () => ({ OpenAICompatibleChatLanguageModel: vi.fn(), })); vi.mock('@ai-sdk/provider-utils', () => ({ loadApiKey: vi.fn().mockReturnValue('mock-api-key'), withoutTrailingSlash: vi.fn(url => url), })); describe('DeepSeekProvider', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('createDeepSeek', () => { it('should create a DeepSeekProvider instance with default options', () => { const provider = createDeepSeek(); const model = provider('model-id'); const constructorCall = OpenAICompatibleChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; config.headers(); expect(loadApiKey).toHaveBeenCalledWith({ apiKey: undefined, environmentVariableName: 'DEEPSEEK_API_KEY', description: 'DeepSeek API key', }); }); it('should create a DeepSeekProvider instance with custom options', () => { const options = { apiKey: 'custom-key', baseURL: 'https://custom.url', headers: { 'Custom-Header': 'value' }, }; const provider = createDeepSeek(options); provider('model-id'); const constructorCall = OpenAICompatibleChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; config.headers(); expect(loadApiKey).toHaveBeenCalledWith({ apiKey: 'custom-key', environmentVariableName: 'DEEPSEEK_API_KEY', description: 'DeepSeek API key', }); }); it('should return a chat model when called as a function', () => { const provider = createDeepSeek(); const modelId = 'foo-model-id'; const model = provider(modelId); expect(model).toBeInstanceOf(OpenAICompatibleChatLanguageModel); }); }); describe('chat', () => { it('should construct a chat model with correct configuration', () => { const provider = createDeepSeek(); const modelId = 'deepseek-chat-model'; const model = provider.chat(modelId); expect(model).toBeInstanceOf(OpenAICompatibleChatLanguageModel); }); }); }); --- File: /ai/packages/deepseek/src/deepseek-provider.ts --- import { OpenAICompatibleChatLanguageModel } from '@ai-sdk/openai-compatible'; import { LanguageModelV2, NoSuchModelError, ProviderV2, } from '@ai-sdk/provider'; import { FetchFunction, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { DeepSeekChatModelId } from './deepseek-chat-options'; import { deepSeekMetadataExtractor } from './deepseek-metadata-extractor'; export interface DeepSeekProviderSettings { /** DeepSeek API key. */ apiKey?: string; /** Base URL for the API calls. */ baseURL?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } export interface DeepSeekProvider extends ProviderV2 { /** Creates a DeepSeek model for text generation. */ (modelId: DeepSeekChatModelId): LanguageModelV2; /** Creates a DeepSeek model for text generation. */ languageModel(modelId: DeepSeekChatModelId): LanguageModelV2; /** Creates a DeepSeek chat model for text generation. */ chat(modelId: DeepSeekChatModelId): LanguageModelV2; } export function createDeepSeek( options: DeepSeekProviderSettings = {}, ): DeepSeekProvider { const baseURL = withoutTrailingSlash( options.baseURL ?? 'https://api.deepseek.com/v1', ); const getHeaders = () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'DEEPSEEK_API_KEY', description: 'DeepSeek API key', })}`, ...options.headers, }); const createLanguageModel = (modelId: DeepSeekChatModelId) => { return new OpenAICompatibleChatLanguageModel(modelId, { provider: `deepseek.chat`, url: ({ path }) => `${baseURL}${path}`, headers: getHeaders, fetch: options.fetch, metadataExtractor: deepSeekMetadataExtractor, }); }; const provider = (modelId: DeepSeekChatModelId) => createLanguageModel(modelId); provider.languageModel = createLanguageModel; provider.chat = createLanguageModel; provider.textEmbeddingModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'textEmbeddingModel' }); }; provider.imageModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'imageModel' }); }; return provider; } export const deepseek = createDeepSeek(); --- File: /ai/packages/deepseek/src/index.ts --- export { createDeepSeek, deepseek } from './deepseek-provider'; export type { DeepSeekProvider, DeepSeekProviderSettings, } from './deepseek-provider'; export type { OpenAICompatibleErrorData as DeepSeekErrorData } from '@ai-sdk/openai-compatible'; --- File: /ai/packages/deepseek/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/deepseek/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/deepseek/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/elevenlabs/src/elevenlabs-api-types.ts --- export type ElevenLabsTranscriptionAPITypes = { /** * An ISO-639-1 or ISO-639-3 language_code corresponding to the language of the audio file. * Can sometimes improve transcription performance if known beforehand. * Defaults to null, in this case the language is predicted automatically. */ language_code?: string; /** * Whether to tag audio events like (laughter), (footsteps), etc. in the transcription. * @default true */ tag_audio_events?: boolean; /** * The maximum amount of speakers talking in the uploaded file. * Can help with predicting who speaks when. * The maximum amount of speakers that can be predicted is 32. * Defaults to null, in this case the amount of speakers is set to the maximum value the model supports. * @min 1 * @max 32 */ num_speakers?: number; /** * The granularity of the timestamps in the transcription. * 'word' provides word-level timestamps and 'character' provides character-level timestamps per word. * @default 'word' */ timestamps_granularity?: 'none' | 'word' | 'character'; /** * Whether to annotate which speaker is currently talking in the uploaded file. * @default false */ diarize?: boolean; /** * A list of additional formats to export the transcript to. */ additional_formats?: Array< | { format: 'docx'; include_speakers?: boolean; include_timestamps?: boolean; max_segment_chars?: number; max_segment_duration_s?: number; segment_on_silence_longer_than_s?: number; } | { format: 'html'; include_speakers?: boolean; include_timestamps?: boolean; max_segment_chars?: number; max_segment_duration_s?: number; segment_on_silence_longer_than_s?: number; } | { format: 'pdf'; include_speakers?: boolean; include_timestamps?: boolean; max_segment_chars?: number; max_segment_duration_s?: number; segment_on_silence_longer_than_s?: number; } | { format: 'segmented_json'; max_segment_chars?: number; max_segment_duration_s?: number; segment_on_silence_longer_than_s?: number; } | { format: 'srt'; include_speakers?: boolean; include_timestamps?: boolean; max_characters_per_line?: number; max_segment_chars?: number; max_segment_duration_s?: number; segment_on_silence_longer_than_s?: number; } | { format: 'txt'; include_speakers?: boolean; include_timestamps?: boolean; max_characters_per_line?: number; max_segment_chars?: number; max_segment_duration_s?: number; segment_on_silence_longer_than_s?: number; } >; /** * The format of input audio. * For pcm_s16le_16, the input audio must be 16-bit PCM at a 16kHz sample rate, * single channel (mono), and little-endian byte order. * Latency will be lower than with passing an encoded waveform. * @default 'other' */ file_format?: 'pcm_s16le_16' | 'other'; }; --- File: /ai/packages/elevenlabs/src/elevenlabs-config.ts --- import { FetchFunction } from '@ai-sdk/provider-utils'; export type ElevenLabsConfig = { provider: string; url: (options: { modelId: string; path: string }) => string; headers: () => Record<string, string | undefined>; fetch?: FetchFunction; generateId?: () => string; }; --- File: /ai/packages/elevenlabs/src/elevenlabs-error.test.ts --- import { safeParseJSON } from '@ai-sdk/provider-utils'; import { elevenlabsErrorDataSchema } from './elevenlabs-error'; describe('elevenlabsErrorDataSchema', () => { it('should parse ElevenLabs resource exhausted error', async () => { const error = ` {"error":{"message":"{\\n \\"error\\": {\\n \\"code\\": 429,\\n \\"message\\": \\"Resource has been exhausted (e.g. check quota).\\",\\n \\"status\\": \\"RESOURCE_EXHAUSTED\\"\\n }\\n}\\n","code":429}} `; const result = await safeParseJSON({ text: error, schema: elevenlabsErrorDataSchema, }); expect(result).toStrictEqual({ success: true, value: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, rawValue: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, }); }); }); --- File: /ai/packages/elevenlabs/src/elevenlabs-error.ts --- import { z } from 'zod/v4'; import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; export const elevenlabsErrorDataSchema = z.object({ error: z.object({ message: z.string(), code: z.number(), }), }); export type ElevenLabsErrorData = z.infer<typeof elevenlabsErrorDataSchema>; export const elevenlabsFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: elevenlabsErrorDataSchema, errorToMessage: data => data.error.message, }); --- File: /ai/packages/elevenlabs/src/elevenlabs-provider.ts --- import { TranscriptionModelV2, ProviderV2, NoSuchModelError, } from '@ai-sdk/provider'; import { FetchFunction, loadApiKey } from '@ai-sdk/provider-utils'; import { ElevenLabsTranscriptionModel } from './elevenlabs-transcription-model'; import { ElevenLabsTranscriptionModelId } from './elevenlabs-transcription-options'; export interface ElevenLabsProvider extends ProviderV2 { ( modelId: 'scribe_v1', settings?: {}, ): { transcription: ElevenLabsTranscriptionModel; }; /** Creates a model for transcription. */ transcription(modelId: ElevenLabsTranscriptionModelId): TranscriptionModelV2; } export interface ElevenLabsProviderSettings { /** API key for authenticating requests. */ apiKey?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } /** Create an ElevenLabs provider instance. */ export function createElevenLabs( options: ElevenLabsProviderSettings = {}, ): ElevenLabsProvider { const getHeaders = () => ({ 'xi-api-key': loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'ELEVENLABS_API_KEY', description: 'ElevenLabs', }), ...options.headers, }); const createTranscriptionModel = (modelId: ElevenLabsTranscriptionModelId) => new ElevenLabsTranscriptionModel(modelId, { provider: `elevenlabs.transcription`, url: ({ path }) => `https://api.elevenlabs.io${path}`, headers: getHeaders, fetch: options.fetch, }); const provider = function (modelId: ElevenLabsTranscriptionModelId) { return { transcription: createTranscriptionModel(modelId), }; }; provider.transcription = createTranscriptionModel; provider.transcriptionModel = createTranscriptionModel; provider.languageModel = () => { throw new NoSuchModelError({ modelId: 'unknown', modelType: 'languageModel', message: 'ElevenLabs does not provide language models', }); }; provider.textEmbeddingModel = () => { throw new NoSuchModelError({ modelId: 'unknown', modelType: 'textEmbeddingModel', message: 'ElevenLabs does not provide text embedding models', }); }; provider.imageModel = () => { throw new NoSuchModelError({ modelId: 'unknown', modelType: 'imageModel', message: 'ElevenLabs does not provide image models', }); }; return provider as ElevenLabsProvider; } /** Default ElevenLabs provider instance. */ export const elevenlabs = createElevenLabs(); --- File: /ai/packages/elevenlabs/src/elevenlabs-transcription-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { ElevenLabsTranscriptionModel } from './elevenlabs-transcription-model'; import { createElevenLabs } from './elevenlabs-provider'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; const audioData = await readFile(path.join(__dirname, 'transcript-test.mp3')); const provider = createElevenLabs({ apiKey: 'test-api-key' }); const model = provider.transcription('scribe_v1'); const server = createTestServer({ 'https://api.elevenlabs.io/v1/speech-to-text': {}, }); describe('doGenerate', () => { function prepareJsonResponse({ headers, }: { headers?: Record<string, string>; } = {}) { server.urls['https://api.elevenlabs.io/v1/speech-to-text'].response = { type: 'json-value', headers, body: { language_code: 'en', language_probability: 0.98, text: 'Hello world!', words: [ { text: 'Hello', type: 'word', start: 0, end: 0.5, speaker_id: 'speaker_1', characters: [ { text: 'text', start: 0, end: 0.1, }, ], }, { text: ' ', type: 'spacing', start: 0.5, end: 0.5, speaker_id: 'speaker_1', characters: [ { text: 'text', start: 0, end: 0.1, }, ], }, { text: 'world!', type: 'word', start: 0.5, end: 1.2, speaker_id: 'speaker_1', characters: [ { text: 'text', start: 0, end: 0.1, }, ], }, ], additional_formats: [ { requested_format: 'requested_format', file_extension: 'file_extension', content_type: 'content_type', is_base64_encoded: true, content: 'content', }, ], }, }; } it('should pass the model', async () => { prepareJsonResponse(); await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(await server.calls[0].requestBodyMultipart).toMatchObject({ model_id: 'scribe_v1', }); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createElevenLabs({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.transcription('scribe_v1').doGenerate({ audio: audioData, mediaType: 'audio/wav', headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toMatchObject({ 'xi-api-key': 'test-api-key', 'content-type': expect.stringMatching( /^multipart\/form-data; boundary=----formdata-undici-\d+$/, ), 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should extract the transcription text', async () => { prepareJsonResponse(); const result = await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.text).toBe('Hello world!'); }); it('should include response data with timestamp, modelId and headers', async () => { prepareJsonResponse({ headers: { 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); const testDate = new Date(0); const customModel = new ElevenLabsTranscriptionModel('scribe_v1', { provider: 'test-provider', url: () => 'https://api.elevenlabs.io/v1/speech-to-text', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.response).toMatchObject({ timestamp: testDate, modelId: 'scribe_v1', headers: { 'content-type': 'application/json', 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); }); it('should use real date when no custom date provider is specified', async () => { prepareJsonResponse(); const testDate = new Date(0); const customModel = new ElevenLabsTranscriptionModel('scribe_v1', { provider: 'test-provider', url: () => 'https://api.elevenlabs.io/v1/speech-to-text', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.response.timestamp.getTime()).toEqual(testDate.getTime()); expect(result.response.modelId).toBe('scribe_v1'); }); it('should work when no additional formats are returned', async () => { server.urls['https://api.elevenlabs.io/v1/speech-to-text'].response = { type: 'json-value', body: { language_code: 'en', language_probability: 0.98, text: 'Hello world!', words: [ { text: 'Hello', type: 'word', start: 0, end: 0.5, speaker_id: 'speaker_1', characters: [ { text: 'text', start: 0, end: 0.1, }, ], }, { text: ' ', type: 'spacing', start: 0.5, end: 0.5, speaker_id: 'speaker_1', characters: [ { text: 'text', start: 0, end: 0.1, }, ], }, { text: 'world!', type: 'word', start: 0.5, end: 1.2, speaker_id: 'speaker_1', characters: [ { text: 'text', start: 0, end: 0.1, }, ], }, ], }, }; const testDate = new Date(0); const customModel = new ElevenLabsTranscriptionModel('scribe_v1', { provider: 'test-provider', url: () => 'https://api.elevenlabs.io/v1/speech-to-text', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result).toMatchInlineSnapshot(` { "durationInSeconds": 1.2, "language": "en", "response": { "body": { "language_code": "en", "language_probability": 0.98, "text": "Hello world!", "words": [ { "characters": [ { "end": 0.1, "start": 0, "text": "text", }, ], "end": 0.5, "speaker_id": "speaker_1", "start": 0, "text": "Hello", "type": "word", }, { "characters": [ { "end": 0.1, "start": 0, "text": "text", }, ], "end": 0.5, "speaker_id": "speaker_1", "start": 0.5, "text": " ", "type": "spacing", }, { "characters": [ { "end": 0.1, "start": 0, "text": "text", }, ], "end": 1.2, "speaker_id": "speaker_1", "start": 0.5, "text": "world!", "type": "word", }, ], }, "headers": { "content-length": "467", "content-type": "application/json", }, "modelId": "scribe_v1", "timestamp": 1970-01-01T00:00:00.000Z, }, "segments": [ { "endSecond": 0.5, "startSecond": 0, "text": "Hello", }, { "endSecond": 0.5, "startSecond": 0.5, "text": " ", }, { "endSecond": 1.2, "startSecond": 0.5, "text": "world!", }, ], "text": "Hello world!", "warnings": [], } `); }); it('should pass provider options correctly', async () => { prepareJsonResponse(); await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', providerOptions: { elevenlabs: { languageCode: 'en', fileFormat: 'pcm_s16le_16', tagAudioEvents: false, numSpeakers: 2, timestampsGranularity: 'character', diarize: true, }, }, }); expect(await server.calls[0].requestBodyMultipart).toMatchInlineSnapshot(` { "diarize": "true", "file": File { Symbol(kHandle): Blob {}, Symbol(kLength): 40169, Symbol(kType): "audio/wav", }, "file_format": "pcm_s16le_16", "language_code": "en", "model_id": "scribe_v1", "num_speakers": "2", "tag_audio_events": "false", "timestamps_granularity": "character", } `); }); }); --- File: /ai/packages/elevenlabs/src/elevenlabs-transcription-model.ts --- import { TranscriptionModelV2, TranscriptionModelV2CallOptions, TranscriptionModelV2CallWarning, } from '@ai-sdk/provider'; import { combineHeaders, convertBase64ToUint8Array, createJsonResponseHandler, parseProviderOptions, postFormDataToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { ElevenLabsConfig } from './elevenlabs-config'; import { elevenlabsFailedResponseHandler } from './elevenlabs-error'; import { ElevenLabsTranscriptionModelId } from './elevenlabs-transcription-options'; import { ElevenLabsTranscriptionAPITypes } from './elevenlabs-api-types'; // https://elevenlabs.io/docs/api-reference/speech-to-text/convert const elevenLabsProviderOptionsSchema = z.object({ languageCode: z.string().nullish(), tagAudioEvents: z.boolean().nullish().default(true), numSpeakers: z.number().int().min(1).max(32).nullish(), timestampsGranularity: z .enum(['none', 'word', 'character']) .nullish() .default('word'), diarize: z.boolean().nullish().default(false), fileFormat: z.enum(['pcm_s16le_16', 'other']).nullish().default('other'), }); export type ElevenLabsTranscriptionCallOptions = z.infer< typeof elevenLabsProviderOptionsSchema >; interface ElevenLabsTranscriptionModelConfig extends ElevenLabsConfig { _internal?: { currentDate?: () => Date; }; } export class ElevenLabsTranscriptionModel implements TranscriptionModelV2 { readonly specificationVersion = 'v2'; get provider(): string { return this.config.provider; } constructor( readonly modelId: ElevenLabsTranscriptionModelId, private readonly config: ElevenLabsTranscriptionModelConfig, ) {} private async getArgs({ audio, mediaType, providerOptions, }: Parameters<TranscriptionModelV2['doGenerate']>[0]) { const warnings: TranscriptionModelV2CallWarning[] = []; // Parse provider options const elevenlabsOptions = await parseProviderOptions({ provider: 'elevenlabs', providerOptions, schema: elevenLabsProviderOptionsSchema, }); // Create form data with base fields const formData = new FormData(); const blob = audio instanceof Uint8Array ? new Blob([audio]) : new Blob([convertBase64ToUint8Array(audio)]); formData.append('model_id', this.modelId); formData.append('file', new File([blob], 'audio', { type: mediaType })); formData.append('diarize', 'true'); // Add provider-specific options if (elevenlabsOptions) { const transcriptionModelOptions: ElevenLabsTranscriptionAPITypes = { language_code: elevenlabsOptions.languageCode ?? undefined, tag_audio_events: elevenlabsOptions.tagAudioEvents ?? undefined, num_speakers: elevenlabsOptions.numSpeakers ?? undefined, timestamps_granularity: elevenlabsOptions.timestampsGranularity ?? undefined, file_format: elevenlabsOptions.fileFormat ?? undefined, }; if (typeof elevenlabsOptions.diarize === 'boolean') { formData.append('diarize', String(elevenlabsOptions.diarize)); } for (const key in transcriptionModelOptions) { const value = transcriptionModelOptions[ key as keyof ElevenLabsTranscriptionAPITypes ]; if (value !== undefined) { formData.append(key, String(value)); } } } return { formData, warnings, }; } async doGenerate( options: Parameters<TranscriptionModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<TranscriptionModelV2['doGenerate']>>> { const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { formData, warnings } = await this.getArgs(options); const { value: response, responseHeaders, rawValue: rawResponse, } = await postFormDataToApi({ url: this.config.url({ path: '/v1/speech-to-text', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), formData, failedResponseHandler: elevenlabsFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( elevenlabsTranscriptionResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); return { text: response.text, segments: response.words?.map(word => ({ text: word.text, startSecond: word.start ?? 0, endSecond: word.end ?? 0, })) ?? [], language: response.language_code, durationInSeconds: response.words?.at(-1)?.end ?? undefined, warnings, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, body: rawResponse, }, }; } } const elevenlabsTranscriptionResponseSchema = z.object({ language_code: z.string(), language_probability: z.number(), text: z.string(), words: z .array( z.object({ text: z.string(), type: z.enum(['word', 'spacing', 'audio_event']), start: z.number().nullish(), end: z.number().nullish(), speaker_id: z.string().nullish(), characters: z .array( z.object({ text: z.string(), start: z.number().nullish(), end: z.number().nullish(), }), ) .nullish(), }), ) .nullish(), }); --- File: /ai/packages/elevenlabs/src/elevenlabs-transcription-options.ts --- export type ElevenLabsTranscriptionModelId = | 'scribe_v1' | 'scribe_v1_experimental' | (string & {}); --- File: /ai/packages/elevenlabs/src/index.ts --- export { createElevenLabs, elevenlabs } from './elevenlabs-provider'; export type { ElevenLabsProvider, ElevenLabsProviderSettings, } from './elevenlabs-provider'; --- File: /ai/packages/elevenlabs/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/elevenlabs/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/elevenlabs/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/fal/src/fal-api-types.ts --- export type FalTranscriptionAPITypes = { /** * URL of the audio file to transcribe. Supported formats: mp3, mp4, mpeg, mpga, m4a, wav or webm. */ audio_url: string; /** * Task to perform on the audio file. Either transcribe or translate. Default value: "transcribe" */ task?: 'transcribe' | 'translate'; /** * Language of the audio file. If set to null, the language will be automatically detected. Defaults to null. * * If translate is selected as the task, the audio will be translated to English, regardless of the language selected. */ language?: | 'af' | 'am' | 'ar' | 'as' | 'az' | 'ba' | 'be' | 'bg' | 'bn' | 'bo' | 'br' | 'bs' | 'ca' | 'cs' | 'cy' | 'da' | 'de' | 'el' | 'en' | 'es' | 'et' | 'eu' | 'fa' | 'fi' | 'fo' | 'fr' | 'gl' | 'gu' | 'ha' | 'haw' | 'he' | 'hi' | 'hr' | 'ht' | 'hu' | 'hy' | 'id' | 'is' | 'it' | 'ja' | 'jw' | 'ka' | 'kk' | 'km' | 'kn' | 'ko' | 'la' | 'lb' | 'ln' | 'lo' | 'lt' | 'lv' | 'mg' | 'mi' | 'mk' | 'ml' | 'mn' | 'mr' | 'ms' | 'mt' | 'my' | 'ne' | 'nl' | 'nn' | 'no' | 'oc' | 'pa' | 'pl' | 'ps' | 'pt' | 'ro' | 'ru' | 'sa' | 'sd' | 'si' | 'sk' | 'sl' | 'sn' | 'so' | 'sq' | 'sr' | 'su' | 'sv' | 'sw' | 'ta' | 'te' | 'tg' | 'th' | 'tk' | 'tl' | 'tr' | 'tt' | 'uk' | 'ur' | 'uz' | 'vi' | 'yi' | 'yo' | 'yue' | 'zh' | null; /** * Whether to diarize the audio file. Defaults to true. */ diarize?: boolean; /** * Level of the chunks to return. Either segment or word. Default value: "segment" */ chunk_level?: 'segment' | 'word'; /** * Version of the model to use. All of the models are the Whisper large variant. Default value: "3" */ version?: '3'; /** * Default value: 64 */ batch_size?: number; /** * Prompt to use for generation. Defaults to an empty string. Default value: "" */ prompt?: string; /** * Number of speakers in the audio file. Defaults to null. If not provided, the number of speakers will be automatically detected. */ num_speakers?: number | null; }; --- File: /ai/packages/fal/src/fal-config.ts --- import { FetchFunction } from '@ai-sdk/provider-utils'; export type FalConfig = { provider: string; url: (options: { modelId: string; path: string }) => string; headers: () => Record<string, string | undefined>; fetch?: FetchFunction; generateId?: () => string; }; --- File: /ai/packages/fal/src/fal-error.test.ts --- import { safeParseJSON } from '@ai-sdk/provider-utils'; import { falErrorDataSchema } from './fal-error'; describe('falErrorDataSchema', () => { it('should parse Fal resource exhausted error', async () => { const error = ` {"error":{"message":"{\\n \\"error\\": {\\n \\"code\\": 429,\\n \\"message\\": \\"Resource has been exhausted (e.g. check quota).\\",\\n \\"status\\": \\"RESOURCE_EXHAUSTED\\"\\n }\\n}\\n","code":429}} `; const result = await safeParseJSON({ text: error, schema: falErrorDataSchema, }); expect(result).toStrictEqual({ success: true, value: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, rawValue: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, }); }); }); --- File: /ai/packages/fal/src/fal-error.ts --- import { z } from 'zod/v4'; import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; export const falErrorDataSchema = z.object({ error: z.object({ message: z.string(), code: z.number(), }), }); export type FalErrorData = z.infer<typeof falErrorDataSchema>; export const falFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: falErrorDataSchema, errorToMessage: data => data.error.message, }); --- File: /ai/packages/fal/src/fal-image-model.test.ts --- import { FetchFunction } from '@ai-sdk/provider-utils'; import { createTestServer } from '@ai-sdk/provider-utils/test'; import { describe, expect, it } from 'vitest'; import { FalImageModel } from './fal-image-model'; const prompt = 'A cute baby sea otter'; function createBasicModel({ headers, fetch, currentDate, }: { headers?: Record<string, string | undefined>; fetch?: FetchFunction; currentDate?: () => Date; settings?: any; } = {}) { return new FalImageModel('stable-diffusion-xl', { provider: 'fal', baseURL: 'https://api.example.com', headers: headers ?? { 'api-key': 'test-key' }, fetch, _internal: { currentDate, }, }); } describe('FalImageModel', () => { const server = createTestServer({ 'https://api.example.com/stable-diffusion-xl': { response: { type: 'json-value', body: { images: [ { url: 'https://api.example.com/image.png', width: 1024, height: 1024, content_type: 'image/png', }, ], }, }, }, 'https://api.example.com/image.png': { response: { type: 'binary', body: Buffer.from('test-binary-content'), }, }, }); describe('doGenerate', () => { it('should pass the correct parameters including size', async () => { const model = createBasicModel(); await model.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: 123, providerOptions: { fal: { additional_param: 'value' } }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ prompt, seed: 123, image_size: { width: 1024, height: 1024 }, num_images: 1, additional_param: 'value', }); }); it('should convert aspect ratio to size', async () => { const model = createBasicModel(); await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: '16:9', seed: undefined, providerOptions: {}, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ prompt, image_size: 'landscape_16_9', num_images: 1, }); }); it('should pass headers', async () => { const modelWithHeaders = createBasicModel({ headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await modelWithHeaders.doGenerate({ prompt, n: 1, providerOptions: {}, headers: { 'Custom-Request-Header': 'request-header-value', }, size: undefined, seed: undefined, aspectRatio: undefined, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should handle API errors', async () => { server.urls['https://api.example.com/stable-diffusion-xl'].response = { type: 'error', status: 400, body: JSON.stringify({ detail: [ { loc: ['prompt'], msg: 'Invalid prompt', type: 'value_error', }, ], }), }; const model = createBasicModel(); await expect( model.doGenerate({ prompt, n: 1, providerOptions: {}, size: undefined, seed: undefined, aspectRatio: undefined, }), ).rejects.toMatchObject({ message: 'prompt: Invalid prompt', statusCode: 400, url: 'https://api.example.com/stable-diffusion-xl', }); }); describe('response metadata', () => { it('should include timestamp, headers and modelId in response', async () => { const testDate = new Date('2024-01-01T00:00:00Z'); const model = createBasicModel({ currentDate: () => testDate, }); const result = await model.doGenerate({ prompt, n: 1, providerOptions: {}, size: undefined, seed: undefined, aspectRatio: undefined, }); expect(result.response).toStrictEqual({ timestamp: testDate, modelId: 'stable-diffusion-xl', headers: expect.any(Object), }); }); }); describe('providerMetaData', () => { // https://fal.ai/models/fal-ai/lora/api#schema-output it('for lora', async () => { const responseMetaData = { prompt: '<prompt>', seed: 123, has_nsfw_concepts: [true], debug_latents: { url: '<debug_latents url>', content_type: '<debug_latents content_type>', file_name: '<debug_latents file_name>', file_data: '<debug_latents file_data>', file_size: 123, }, debug_per_pass_latents: { url: '<debug_per_pass_latents url>', content_type: '<debug_per_pass_latents content_type>', file_name: '<debug_per_pass_latents file_name>', file_data: '<debug_per_pass_latents file_data>', file_size: 456, }, }; server.urls['https://api.example.com/stable-diffusion-xl'].response = { type: 'json-value', body: { images: [ { url: 'https://api.example.com/image.png', width: 1024, height: 1024, content_type: 'image/png', file_data: '<image file_data>', file_size: 123, file_name: '<image file_name>', }, ], ...responseMetaData, }, }; const model = createBasicModel(); const result = await model.doGenerate({ prompt, n: 1, providerOptions: {}, size: undefined, seed: undefined, aspectRatio: undefined, }); expect(result.providerMetadata).toStrictEqual({ fal: { images: [ { width: 1024, height: 1024, contentType: 'image/png', fileName: '<image file_name>', fileData: '<image file_data>', fileSize: 123, nsfw: true, }, ], seed: 123, debug_latents: { url: '<debug_latents url>', content_type: '<debug_latents content_type>', file_name: '<debug_latents file_name>', file_data: '<debug_latents file_data>', file_size: 123, }, debug_per_pass_latents: { url: '<debug_per_pass_latents url>', content_type: '<debug_per_pass_latents content_type>', file_name: '<debug_per_pass_latents file_name>', file_data: '<debug_per_pass_latents file_data>', file_size: 456, }, }, }); }); it('for lcm', async () => { const responseMetaData = { seed: 123, num_inference_steps: 456, nsfw_content_detected: [false], }; server.urls['https://api.example.com/stable-diffusion-xl'].response = { type: 'json-value', body: { images: [ { url: 'https://api.example.com/image.png', width: 1024, height: 1024, }, ], ...responseMetaData, }, }; const model = createBasicModel(); const result = await model.doGenerate({ prompt, n: 1, providerOptions: {}, size: undefined, seed: undefined, aspectRatio: undefined, }); expect(result.providerMetadata).toStrictEqual({ fal: { images: [ { width: 1024, height: 1024, nsfw: false, }, ], seed: 123, num_inference_steps: 456, }, }); }); }); }); describe('constructor', () => { it('should expose correct provider and model information', () => { const model = createBasicModel(); expect(model.provider).toBe('fal'); expect(model.modelId).toBe('stable-diffusion-xl'); expect(model.specificationVersion).toBe('v2'); expect(model.maxImagesPerCall).toBe(1); }); }); describe('response schema validation', () => { it('should parse single image response', async () => { server.urls['https://api.example.com/stable-diffusion-xl'].response = { type: 'json-value', body: { image: { url: 'https://api.example.com/image.png', width: 1024, height: 1024, content_type: 'image/png', }, }, }; const model = createBasicModel(); const result = await model.doGenerate({ prompt, n: 1, providerOptions: {}, size: undefined, seed: undefined, aspectRatio: undefined, }); expect(result.images).toHaveLength(1); expect(result.images[0]).toBeInstanceOf(Uint8Array); }); it('should parse multiple images response', async () => { server.urls['https://api.example.com/stable-diffusion-xl'].response = { type: 'json-value', body: { images: [ { url: 'https://api.example.com/image.png', width: 1024, height: 1024, content_type: 'image/png', }, { url: 'https://api.example.com/image.png', width: 1024, height: 1024, content_type: 'image/png', }, ], }, }; const model = createBasicModel(); const result = await model.doGenerate({ prompt, n: 2, providerOptions: {}, size: undefined, seed: undefined, aspectRatio: undefined, }); expect(result.images).toHaveLength(2); expect(result.images[0]).toBeInstanceOf(Uint8Array); expect(result.images[1]).toBeInstanceOf(Uint8Array); }); it('should handle null file_name and file_size values', async () => { server.urls['https://api.example.com/stable-diffusion-xl'].response = { type: 'json-value', body: { images: [ { url: 'https://api.example.com/image.png', content_type: 'image/png', file_name: null, file_size: null, width: 944, height: 1104, }, ], timings: { inference: 5.875932216644287 }, seed: 328395684, has_nsfw_concepts: [false], prompt: 'A female model holding this book, keeping the book unchanged.', }, }; const model = createBasicModel(); const result = await model.doGenerate({ prompt, n: 1, providerOptions: {}, size: undefined, seed: undefined, aspectRatio: undefined, }); expect(result.images).toHaveLength(1); expect(result.images[0]).toBeInstanceOf(Uint8Array); expect(result.providerMetadata?.fal).toMatchObject({ images: [ { width: 944, height: 1104, contentType: 'image/png', fileName: null, fileSize: null, nsfw: false, }, ], timings: { inference: 5.875932216644287 }, seed: 328395684, }); }); it('should handle empty timings object', async () => { server.urls['https://api.example.com/stable-diffusion-xl'].response = { type: 'json-value', body: { images: [ { url: 'https://api.example.com/image.png', content_type: 'image/png', file_name: null, file_size: null, width: 880, height: 1184, }, ], timings: {}, seed: 235205040, has_nsfw_concepts: [false], prompt: 'Change the plates to colorful ones', }, }; const model = createBasicModel(); const result = await model.doGenerate({ prompt, n: 1, providerOptions: {}, size: undefined, seed: undefined, aspectRatio: undefined, }); expect(result.images).toHaveLength(1); expect(result.images[0]).toBeInstanceOf(Uint8Array); expect(result.providerMetadata?.fal).toMatchObject({ images: [ { width: 880, height: 1184, contentType: 'image/png', fileName: null, fileSize: null, nsfw: false, }, ], timings: {}, seed: 235205040, }); }); }); }); --- File: /ai/packages/fal/src/fal-image-model.ts --- import type { ImageModelV2, ImageModelV2CallWarning, JSONObject, } from '@ai-sdk/provider'; import type { Resolvable } from '@ai-sdk/provider-utils'; import { FetchFunction, combineHeaders, createBinaryResponseHandler, createJsonResponseHandler, createJsonErrorResponseHandler, createStatusCodeErrorResponseHandler, getFromApi, postJsonToApi, resolve, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { FalImageModelId, FalImageSize } from './fal-image-settings'; interface FalImageModelConfig { provider: string; baseURL: string; headers?: Resolvable<Record<string, string | undefined>>; fetch?: FetchFunction; _internal?: { currentDate?: () => Date; }; } export class FalImageModel implements ImageModelV2 { readonly specificationVersion = 'v2'; readonly maxImagesPerCall = 1; get provider(): string { return this.config.provider; } constructor( readonly modelId: FalImageModelId, private readonly config: FalImageModelConfig, ) {} async doGenerate({ prompt, n, size, aspectRatio, seed, providerOptions, headers, abortSignal, }: Parameters<ImageModelV2['doGenerate']>[0]): Promise< Awaited<ReturnType<ImageModelV2['doGenerate']>> > { const warnings: Array<ImageModelV2CallWarning> = []; let imageSize: FalImageSize | undefined; if (size) { const [width, height] = size.split('x').map(Number); imageSize = { width, height }; } else if (aspectRatio) { imageSize = convertAspectRatioToSize(aspectRatio); } const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { value, responseHeaders } = await postJsonToApi({ url: `${this.config.baseURL}/${this.modelId}`, headers: combineHeaders(await resolve(this.config.headers), headers), body: { prompt, seed, image_size: imageSize, num_images: n, ...(providerOptions.fal ?? {}), }, failedResponseHandler: falFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( falImageResponseSchema, ), abortSignal, fetch: this.config.fetch, }); // download the images: const targetImages = 'images' in value ? value.images : [value.image]; const downloadedImages = await Promise.all( targetImages.map(image => this.downloadImage(image.url, abortSignal)), ); const { // @ts-expect-error - either image or images is present, not both. image, // @ts-expect-error - either image or images is present, not both. images, // prompt is just passed through and not a revised prompt per image prompt: _prompt, // NSFW information is normalized merged into `providerMetadata.fal.images` has_nsfw_concepts, nsfw_content_detected, // pass through other properties to providerMetadata ...responseMetaData } = value; return { images: downloadedImages, warnings, response: { modelId: this.modelId, timestamp: currentDate, headers: responseHeaders, }, providerMetadata: { fal: { images: targetImages.map((image, index) => { const { url, content_type: contentType, file_name: fileName, file_data: fileData, file_size: fileSize, ...imageMetaData } = image; const nsfw = value.has_nsfw_concepts?.[index] ?? value.nsfw_content_detected?.[index]; return { ...imageMetaData, ...(contentType !== undefined ? { contentType } : undefined), ...(fileName !== undefined ? { fileName } : undefined), ...(fileData !== undefined ? { fileData } : undefined), ...(fileSize !== undefined ? { fileSize } : undefined), ...(nsfw !== undefined ? { nsfw } : undefined), }; }), ...responseMetaData, }, }, }; } private async downloadImage( url: string, abortSignal: AbortSignal | undefined, ): Promise<Uint8Array> { const { value: response } = await getFromApi({ url, // No specific headers should be needed for this request as it's a // generated image provided by fal.ai. abortSignal, failedResponseHandler: createStatusCodeErrorResponseHandler(), successfulResponseHandler: createBinaryResponseHandler(), fetch: this.config.fetch, }); return response; } } /** Converts an aspect ratio to an image size compatible with fal.ai APIs. @param aspectRatio - The aspect ratio to convert. @returns The image size. */ function convertAspectRatioToSize( aspectRatio: `${number}:${number}`, ): FalImageSize | undefined { switch (aspectRatio) { case '1:1': return 'square_hd'; case '16:9': return 'landscape_16_9'; case '9:16': return 'portrait_16_9'; case '4:3': return 'landscape_4_3'; case '3:4': return 'portrait_4_3'; case '16:10': return { width: 1280, height: 800 }; case '10:16': return { width: 800, height: 1280 }; case '21:9': return { width: 2560, height: 1080 }; case '9:21': return { width: 1080, height: 2560 }; } return undefined; } // Validation error has a particular payload to inform the exact property that is invalid const falValidationErrorSchema = z.object({ detail: z.array( z.object({ loc: z.array(z.string()), msg: z.string(), type: z.string(), }), ), }); type ValidationError = z.infer<typeof falValidationErrorSchema>; // Other errors have a message property const falHttpErrorSchema = z.object({ message: z.string(), }); const falErrorSchema = z.union([falValidationErrorSchema, falHttpErrorSchema]); const falImageSchema = z.object({ url: z.string(), width: z.number().optional(), height: z.number().optional(), content_type: z.string().optional(), // e.g. https://fal.ai/models/fal-ai/flowedit/api#schema-output file_name: z.string().nullable().optional(), file_data: z.string().optional(), file_size: z.number().nullable().optional(), }); // https://fal.ai/models/fal-ai/lora/api#type-File const loraFileSchema = z.object({ url: z.string(), content_type: z.string().optional(), file_name: z.string().nullable().optional(), file_data: z.string().optional(), file_size: z.number().nullable().optional(), }); const commonResponseSchema = z.object({ timings: z .object({ inference: z.number().optional(), }) .optional(), seed: z.number().optional(), has_nsfw_concepts: z.array(z.boolean()).optional(), prompt: z.string().optional(), // https://fal.ai/models/fal-ai/lcm/api#schema-output nsfw_content_detected: z.array(z.boolean()).optional(), num_inference_steps: z.number().optional(), // https://fal.ai/models/fal-ai/lora/api#schema-output debug_latents: loraFileSchema.optional(), debug_per_pass_latents: loraFileSchema.optional(), }); // Most FAL image models respond with an array of images, but some have a response // with a single image, e.g. https://fal.ai/models/easel-ai/easel-avatar/api#schema-output const falImageResponseSchema = z.union([ z .object({ images: z.array(falImageSchema), }) .merge(commonResponseSchema), z .object({ image: falImageSchema, }) .merge(commonResponseSchema), ]); function isValidationError(error: unknown): error is ValidationError { return falValidationErrorSchema.safeParse(error).success; } const falFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: falErrorSchema, errorToMessage: error => { if (isValidationError(error)) { return error.detail .map(detail => `${detail.loc.join('.')}: ${detail.msg}`) .join('\n'); } return error.message ?? 'Unknown fal error'; }, }); --- File: /ai/packages/fal/src/fal-image-settings.ts --- // https://fal.ai/models export type FalImageModelId = | 'fal-ai/flux-pro/kontext/max' | 'fal-ai/flux-pro/kontext' | 'fal-ai/aura-flow' | 'fal-ai/aura-sr' | 'fal-ai/bria/eraser' | 'fal-ai/bria/product-shot' | 'fal-ai/bria/text-to-image/base' | 'fal-ai/bria/text-to-image/fast' | 'fal-ai/bria/text-to-image/hd' | 'fal-ai/bria/text-to-image/turbo' | 'fal-ai/ccsr' | 'fal-ai/clarity-upscaler' | 'fal-ai/creative-upscaler' | 'fal-ai/esrgan' | 'fal-ai/fast-sdxl' | 'fal-ai/flux-general' | 'fal-ai/flux-general/differential-diffusion' | 'fal-ai/flux-general/image-to-image' | 'fal-ai/flux-general/inpainting' | 'fal-ai/flux-general/rf-inversion' | 'fal-ai/flux-lora' | 'fal-ai/flux-lora/image-to-image' | 'fal-ai/flux-lora/inpainting' | 'fal-ai/flux-pro/v1.1' | 'fal-ai/flux-pro/v1.1-ultra' | 'fal-ai/flux-pro/v1.1-ultra-finetuned' | 'fal-ai/flux-pro/v1.1-ultra/redux' | 'fal-ai/flux-pro/v1.1/redux' | 'fal-ai/flux/dev' | 'fal-ai/flux/dev/image-to-image' | 'fal-ai/flux/dev/redux' | 'fal-ai/flux/schnell' | 'fal-ai/flux/schnell/redux' | 'fal-ai/hyper-sdxl' | 'fal-ai/ideogram/v2' | 'fal-ai/ideogram/v2/remix' | 'fal-ai/ideogram/v2/turbo' | 'fal-ai/ideogram/v2/turbo/edit' | 'fal-ai/ideogram/v2/turbo/remix' | 'fal-ai/janus' | 'fal-ai/luma-photon' | 'fal-ai/luma-photon/flash' | 'fal-ai/omnigen-v1' | 'fal-ai/playground-v25' | 'fal-ai/recraft-20b' | 'fal-ai/recraft-v3' | 'fal-ai/sana' | 'fal-ai/stable-cascade' | 'fal-ai/stable-diffusion-3.5-large' | 'fal-ai/stable-diffusion-3.5-medium' | 'fashn/tryon' | (string & {}); export type FalImageSize = | 'square' | 'square_hd' | 'landscape_16_9' | 'landscape_4_3' | 'portrait_16_9' | 'portrait_4_3' | { width: number; height: number; }; --- File: /ai/packages/fal/src/fal-provider.test.ts --- import { describe, it, expect, beforeEach, vi } from 'vitest'; import { createFal } from './fal-provider'; import { FalImageModel } from './fal-image-model'; vi.mock('./fal-image-model', () => ({ FalImageModel: vi.fn(), })); describe('createFal', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('image', () => { it('should construct an image model with default configuration', () => { const provider = createFal(); const modelId = 'fal-ai/flux/dev'; const model = provider.image(modelId); expect(model).toBeInstanceOf(FalImageModel); expect(FalImageModel).toHaveBeenCalledWith( modelId, expect.objectContaining({ provider: 'fal.image', baseURL: 'https://fal.run', }), ); }); it('should respect custom configuration options', () => { const customBaseURL = 'https://custom.fal.run'; const customHeaders = { 'X-Custom-Header': 'value' }; const mockFetch = vi.fn(); const provider = createFal({ apiKey: 'custom-api-key', baseURL: customBaseURL, headers: customHeaders, fetch: mockFetch, }); const modelId = 'fal-ai/flux/dev'; provider.image(modelId); expect(FalImageModel).toHaveBeenCalledWith( modelId, expect.objectContaining({ baseURL: customBaseURL, headers: expect.any(Function), fetch: mockFetch, provider: 'fal.image', }), ); }); }); }); --- File: /ai/packages/fal/src/fal-provider.ts --- import { ImageModelV2, NoSuchModelError, ProviderV2, TranscriptionModelV2, } from '@ai-sdk/provider'; import type { FetchFunction } from '@ai-sdk/provider-utils'; import { withoutTrailingSlash } from '@ai-sdk/provider-utils'; import { FalImageModel } from './fal-image-model'; import { FalImageModelId } from './fal-image-settings'; import { FalTranscriptionModelId } from './fal-transcription-options'; import { FalTranscriptionModel } from './fal-transcription-model'; export interface FalProviderSettings { /** fal.ai API key. Default value is taken from the `FAL_API_KEY` environment variable, falling back to `FAL_KEY`. */ apiKey?: string; /** Base URL for the API calls. The default prefix is `https://fal.run`. */ baseURL?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } export interface FalProvider extends ProviderV2 { /** Creates a model for image generation. */ image(modelId: FalImageModelId): ImageModelV2; /** Creates a model for image generation. */ imageModel(modelId: FalImageModelId): ImageModelV2; /** Creates a model for transcription. */ transcription(modelId: FalTranscriptionModelId): TranscriptionModelV2; } const defaultBaseURL = 'https://fal.run'; function loadFalApiKey({ apiKey, description = 'fal.ai', }: { apiKey: string | undefined; description?: string; }): string { if (typeof apiKey === 'string') { return apiKey; } if (apiKey != null) { throw new Error(`${description} API key must be a string.`); } if (typeof process === 'undefined') { throw new Error( `${description} API key is missing. Pass it using the 'apiKey' parameter. Environment variables are not supported in this environment.`, ); } let envApiKey = process.env.FAL_API_KEY; if (envApiKey == null) { envApiKey = process.env.FAL_KEY; } if (envApiKey == null) { throw new Error( `${description} API key is missing. Pass it using the 'apiKey' parameter or set either the FAL_API_KEY or FAL_KEY environment variable.`, ); } if (typeof envApiKey !== 'string') { throw new Error( `${description} API key must be a string. The value of the environment variable is not a string.`, ); } return envApiKey; } /** Create a fal.ai provider instance. */ export function createFal(options: FalProviderSettings = {}): FalProvider { const baseURL = withoutTrailingSlash(options.baseURL ?? defaultBaseURL); const getHeaders = () => ({ Authorization: `Key ${loadFalApiKey({ apiKey: options.apiKey, })}`, ...options.headers, }); const createImageModel = (modelId: FalImageModelId) => new FalImageModel(modelId, { provider: 'fal.image', baseURL: baseURL ?? defaultBaseURL, headers: getHeaders, fetch: options.fetch, }); const createTranscriptionModel = (modelId: FalTranscriptionModelId) => new FalTranscriptionModel(modelId, { provider: `fal.transcription`, url: ({ path }) => path, headers: getHeaders, fetch: options.fetch, }); return { imageModel: createImageModel, image: createImageModel, languageModel: () => { throw new NoSuchModelError({ modelId: 'languageModel', modelType: 'languageModel', }); }, textEmbeddingModel: () => { throw new NoSuchModelError({ modelId: 'textEmbeddingModel', modelType: 'textEmbeddingModel', }); }, transcription: createTranscriptionModel, }; } /** Default fal.ai provider instance. */ export const fal = createFal(); --- File: /ai/packages/fal/src/fal-transcription-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { createFal } from './fal-provider'; import { FalTranscriptionModel } from './fal-transcription-model'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; const audioData = await readFile(path.join(__dirname, 'transcript-test.mp3')); const provider = createFal({ apiKey: 'test-api-key' }); const model = provider.transcription('wizper'); const server = createTestServer({ 'https://queue.fal.run/fal-ai/wizper': {}, 'https://queue.fal.run/fal-ai/wizper/requests/test-id': {}, }); describe('doGenerate', () => { function prepareJsonResponse({ headers, }: { headers?: Record<string, string>; } = {}) { server.urls['https://queue.fal.run/fal-ai/wizper'].response = { type: 'json-value', headers, body: { status: 'COMPLETED', request_id: 'test-id', response_url: 'https://queue.fal.run/fal-ai/wizper/requests/test-id/result', status_url: 'https://queue.fal.run/fal-ai/wizper/requests/test-id', cancel_url: 'https://queue.fal.run/fal-ai/wizper/requests/test-id/cancel', logs: null, metrics: {}, queue_position: 0, }, }; server.urls[ 'https://queue.fal.run/fal-ai/wizper/requests/test-id' ].response = { type: 'json-value', headers, body: { text: 'Hello world!', chunks: [ { text: 'Hello', timestamp: [0, 1], speaker: 'speaker_1', }, { text: ' ', timestamp: [1, 2], speaker: 'speaker_1', }, { text: 'world!', timestamp: [2, 3], speaker: 'speaker_1', }, ], diarization_segments: [ { speaker: 'speaker_1', timestamp: [0, 3], }, ], }, }; } it('should pass the model', async () => { prepareJsonResponse(); await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(await server.calls[0].requestBodyJson).toMatchObject({ audio_url: expect.stringMatching(/^data:audio\//), task: 'transcribe', diarize: true, chunk_level: 'word', }); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createFal({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.transcription('wizper').doGenerate({ audio: audioData, mediaType: 'audio/wav', headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toMatchObject({ authorization: 'Key test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should extract the transcription text', async () => { prepareJsonResponse(); const result = await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.text).toBe('Hello world!'); }); it('should include response data with timestamp, modelId and headers', async () => { prepareJsonResponse({ headers: { 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); const testDate = new Date(0); const customModel = new FalTranscriptionModel('wizper', { provider: 'test-provider', url: ({ path }) => path, headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.response).toMatchObject({ timestamp: testDate, modelId: 'wizper', headers: { 'content-type': 'application/json', 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); }); it('should use real date when no custom date provider is specified', async () => { prepareJsonResponse(); const testDate = new Date(0); const customModel = new FalTranscriptionModel('wizper', { provider: 'test-provider', url: ({ path }) => path, headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.response.timestamp.getTime()).toEqual(testDate.getTime()); expect(result.response.modelId).toBe('wizper'); }); }); --- File: /ai/packages/fal/src/fal-transcription-model.ts --- import { AISDKError, TranscriptionModelV2, TranscriptionModelV2CallWarning, } from '@ai-sdk/provider'; import { combineHeaders, convertUint8ArrayToBase64, createJsonErrorResponseHandler, createJsonResponseHandler, delay, getFromApi, parseProviderOptions, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { FalConfig } from './fal-config'; import { falErrorDataSchema, falFailedResponseHandler } from './fal-error'; import { FalTranscriptionModelId } from './fal-transcription-options'; import { FalTranscriptionAPITypes } from './fal-api-types'; // https://fal.ai/models/fal-ai/whisper/api?platform=http const falProviderOptionsSchema = z.object({ /** * Language of the audio file. If set to null, the language will be automatically detected. Defaults to null. * * If translate is selected as the task, the audio will be translated to English, regardless of the language selected. */ language: z .union([z.enum(['en']), z.string()]) .nullish() .default('en'), /** * Whether to diarize the audio file. Defaults to true. */ diarize: z.boolean().nullish().default(true), /** * Level of the chunks to return. Either segment or word. Default value: "segment" */ chunkLevel: z.enum(['segment', 'word']).nullish().default('segment'), /** * Version of the model to use. All of the models are the Whisper large variant. Default value: "3" */ version: z.enum(['3']).nullish().default('3'), /** * Default value: 64 */ batchSize: z.number().nullish().default(64), /** * Number of speakers in the audio file. Defaults to null. If not provided, the number of speakers will be automatically detected. */ numSpeakers: z.number().nullable().nullish(), }); export type FalTranscriptionCallOptions = z.infer< typeof falProviderOptionsSchema >; interface FalTranscriptionModelConfig extends FalConfig { _internal?: { currentDate?: () => Date; }; } export class FalTranscriptionModel implements TranscriptionModelV2 { readonly specificationVersion = 'v2'; get provider(): string { return this.config.provider; } constructor( readonly modelId: FalTranscriptionModelId, private readonly config: FalTranscriptionModelConfig, ) {} private async getArgs({ providerOptions, }: Parameters<TranscriptionModelV2['doGenerate']>[0]) { const warnings: TranscriptionModelV2CallWarning[] = []; // Parse provider options const falOptions = await parseProviderOptions({ provider: 'fal', providerOptions, schema: falProviderOptionsSchema, }); // Create form data with base fields const body: Omit<FalTranscriptionAPITypes, 'audio_url'> = { task: 'transcribe', diarize: true, chunk_level: 'word', }; // Add provider-specific options if (falOptions) { body.language = falOptions.language as never; body.version = falOptions.version ?? undefined; body.batch_size = falOptions.batchSize ?? undefined; body.num_speakers = falOptions.numSpeakers ?? undefined; if (typeof falOptions.diarize === 'boolean') { body.diarize = falOptions.diarize; } if (falOptions.chunkLevel) { body.chunk_level = falOptions.chunkLevel; } } return { body, warnings, }; } async doGenerate( options: Parameters<TranscriptionModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<TranscriptionModelV2['doGenerate']>>> { const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { body, warnings } = await this.getArgs(options); const base64Audio = typeof options.audio === 'string' ? options.audio : convertUint8ArrayToBase64(options.audio); const audioUrl = `data:${options.mediaType};base64,${base64Audio}`; const { value: queueResponse } = await postJsonToApi({ url: this.config.url({ path: `https://queue.fal.run/fal-ai/${this.modelId}`, modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body: { ...body, audio_url: audioUrl, }, failedResponseHandler: falFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler(falJobResponseSchema), abortSignal: options.abortSignal, fetch: this.config.fetch, }); // Poll for completion with timeout const startTime = Date.now(); const timeoutMs = 60000; // 60 seconds timeout const pollIntervalMs = 1000; // 1 second interval let response; let responseHeaders; let rawResponse; while (true) { try { const { value: statusResponse, responseHeaders: statusHeaders, rawValue: statusRawResponse, } = await getFromApi({ url: this.config.url({ path: `https://queue.fal.run/fal-ai/${this.modelId}/requests/${queueResponse.request_id}`, modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), failedResponseHandler: async ({ requestBodyValues, response, url, }) => { const clone = response.clone(); const body = (await clone.json()) as { detail: string }; if (body.detail === 'Request is still in progress') { // This is not an error, just a status update that the request is still processing // Continue polling by returning a special error that signals to continue return { value: new Error('Request is still in progress'), rawValue: body, responseHeaders: {}, }; } return createJsonErrorResponseHandler({ errorSchema: falErrorDataSchema, errorToMessage: data => data.error.message, })({ requestBodyValues, response, url }); }, successfulResponseHandler: createJsonResponseHandler( falTranscriptionResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); response = statusResponse; responseHeaders = statusHeaders; rawResponse = statusRawResponse; break; } catch (error) { // If the error message indicates the request is still in progress, ignore it and continue polling if ( error instanceof Error && error.message === 'Request is still in progress' ) { // Continue with the polling loop } else { // Re-throw any other errors throw error; } } // Check if we've exceeded the timeout if (Date.now() - startTime > timeoutMs) { throw new AISDKError({ message: 'Transcription request timed out after 60 seconds', name: 'TranscriptionRequestTimedOut', cause: response, }); } // Wait before polling again await delay(pollIntervalMs); } return { text: response.text, segments: response.chunks?.map(chunk => ({ text: chunk.text, startSecond: chunk.timestamp?.at(0) ?? 0, endSecond: chunk.timestamp?.at(1) ?? 0, })) ?? [], language: response.inferred_languages?.at(0) ?? undefined, durationInSeconds: response.chunks?.at(-1)?.timestamp?.at(1) ?? undefined, warnings, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, body: rawResponse, }, }; } } const falJobResponseSchema = z.object({ request_id: z.string().nullish(), }); const falTranscriptionResponseSchema = z.object({ text: z.string(), chunks: z .array( z.object({ text: z.string(), timestamp: z.array(z.number()).nullish(), }), ) .nullish(), inferred_languages: z.array(z.string()).nullish(), }); --- File: /ai/packages/fal/src/fal-transcription-options.ts --- export type FalTranscriptionModelId = 'whisper' | 'wizper' | (string & {}); --- File: /ai/packages/fal/src/index.ts --- export { createFal, fal } from './fal-provider'; export type { FalProvider, FalProviderSettings } from './fal-provider'; --- File: /ai/packages/fal/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/fal/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/fal/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/fireworks/src/fireworks-chat-options.ts --- // https://docs.fireworks.ai/docs/serverless-models#chat-models // Below is just a subset of the available models. export type FireworksChatModelId = | 'accounts/fireworks/models/deepseek-v3' | 'accounts/fireworks/models/llama-v3p3-70b-instruct' | 'accounts/fireworks/models/llama-v3p2-3b-instruct' | 'accounts/fireworks/models/llama-v3p1-405b-instruct' | 'accounts/fireworks/models/llama-v3p1-8b-instruct' | 'accounts/fireworks/models/mixtral-8x7b-instruct' | 'accounts/fireworks/models/mixtral-8x22b-instruct' | 'accounts/fireworks/models/mixtral-8x7b-instruct-hf' | 'accounts/fireworks/models/qwen2p5-coder-32b-instruct' | 'accounts/fireworks/models/qwen2p5-72b-instruct' | 'accounts/fireworks/models/qwen-qwq-32b-preview' | 'accounts/fireworks/models/qwen2-vl-72b-instruct' | 'accounts/fireworks/models/llama-v3p2-11b-vision-instruct' | 'accounts/fireworks/models/qwq-32b' | 'accounts/fireworks/models/yi-large' | 'accounts/fireworks/models/kimi-k2-instruct' | (string & {}); --- File: /ai/packages/fireworks/src/fireworks-completion-options.ts --- // Below is just a subset of the available models. export type FireworksCompletionModelId = | 'accounts/fireworks/models/llama-v3-8b-instruct' | 'accounts/fireworks/models/llama-v2-34b-code' | (string & {}); --- File: /ai/packages/fireworks/src/fireworks-embedding-options.ts --- import { z } from 'zod/v4'; // Below is just a subset of the available models. export type FireworksEmbeddingModelId = | 'nomic-ai/nomic-embed-text-v1.5' | (string & {}); export const fireworksEmbeddingProviderOptions = z.object({}); export type FireworksEmbeddingProviderOptions = z.infer< typeof fireworksEmbeddingProviderOptions >; --- File: /ai/packages/fireworks/src/fireworks-image-model.test.ts --- import { FetchFunction } from '@ai-sdk/provider-utils'; import { createTestServer } from '@ai-sdk/provider-utils/test'; import { describe, expect, it } from 'vitest'; import { FireworksImageModel } from './fireworks-image-model'; const prompt = 'A cute baby sea otter'; function createBasicModel({ headers, fetch, currentDate, }: { headers?: () => Record<string, string>; fetch?: FetchFunction; currentDate?: () => Date; } = {}) { return new FireworksImageModel('accounts/fireworks/models/flux-1-dev-fp8', { provider: 'fireworks', baseURL: 'https://api.example.com', headers: headers ?? (() => ({ 'api-key': 'test-key' })), fetch, _internal: { currentDate, }, }); } function createSizeModel() { return new FireworksImageModel( 'accounts/fireworks/models/playground-v2-5-1024px-aesthetic', { provider: 'fireworks', baseURL: 'https://api.size-example.com', headers: () => ({ 'api-key': 'test-key' }), }, ); } describe('FireworksImageModel', () => { const server = createTestServer({ 'https://api.example.com/*': { response: { type: 'binary', body: Buffer.from('test-binary-content'), }, }, 'https://api.size-example.com/*': { response: { type: 'binary', body: Buffer.from('test-binary-content'), }, }, }); describe('doGenerate', () => { it('should pass the correct parameters including aspect ratio and seed', async () => { const model = createBasicModel(); await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: '16:9', seed: 42, providerOptions: { fireworks: { additional_param: 'value' } }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ prompt, aspect_ratio: '16:9', seed: 42, samples: 1, additional_param: 'value', }); }); it('should call the correct url', async () => { const model = createBasicModel(); await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: '16:9', seed: 42, providerOptions: { fireworks: { additional_param: 'value' } }, }); expect(server.calls[0].requestMethod).toStrictEqual('POST'); expect(server.calls[0].requestUrl).toStrictEqual( 'https://api.example.com/workflows/accounts/fireworks/models/flux-1-dev-fp8/text_to_image', ); }); it('should pass headers', async () => { const modelWithHeaders = createBasicModel({ headers: () => ({ 'Custom-Provider-Header': 'provider-header-value', }), }); await modelWithHeaders.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should handle empty response body', async () => { server.urls['https://api.example.com/*'].response = { type: 'empty', }; const model = createBasicModel(); await expect( model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }), ).rejects.toMatchObject({ message: 'Response body is empty', statusCode: 200, url: 'https://api.example.com/workflows/accounts/fireworks/models/flux-1-dev-fp8/text_to_image', requestBodyValues: { prompt: 'A cute baby sea otter', }, }); }); it('should handle API errors', async () => { server.urls['https://api.example.com/*'].response = { type: 'error', status: 400, body: 'Bad Request', }; const model = createBasicModel(); await expect( model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }), ).rejects.toMatchObject({ message: 'Bad Request', statusCode: 400, url: 'https://api.example.com/workflows/accounts/fireworks/models/flux-1-dev-fp8/text_to_image', requestBodyValues: { prompt: 'A cute baby sea otter', }, responseBody: 'Bad Request', }); }); it('should handle size parameter for supported models', async () => { const sizeModel = createSizeModel(); await sizeModel.doGenerate({ prompt, n: 1, size: '1024x768', aspectRatio: undefined, seed: 42, providerOptions: {}, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ prompt, width: '1024', height: '768', seed: 42, samples: 1, }); }); describe('warnings', () => { it('should return size warning on workflow model', async () => { const model = createBasicModel(); const result1 = await model.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: '1:1', seed: 123, providerOptions: {}, }); expect(result1.warnings).toContainEqual({ type: 'unsupported-setting', setting: 'size', details: 'This model does not support the `size` option. Use `aspectRatio` instead.', }); }); it('should return aspectRatio warning on size-supporting model', async () => { const sizeModel = createSizeModel(); const result2 = await sizeModel.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: '1:1', seed: 123, providerOptions: {}, }); expect(result2.warnings).toContainEqual({ type: 'unsupported-setting', setting: 'aspectRatio', details: 'This model does not support the `aspectRatio` option.', }); }); }); it('should respect the abort signal', async () => { const model = createBasicModel(); const controller = new AbortController(); const generatePromise = model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, abortSignal: controller.signal, }); controller.abort(); await expect(generatePromise).rejects.toThrow( 'This operation was aborted', ); }); it('should use custom fetch function when provided', async () => { const mockFetch = vi.fn().mockResolvedValue( new Response(Buffer.from('mock-image-data'), { status: 200, }), ); const model = createBasicModel({ fetch: mockFetch, }); await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(mockFetch).toHaveBeenCalled(); }); it('should pass samples parameter to API', async () => { const model = createBasicModel(); await model.doGenerate({ prompt, n: 42, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(await server.calls[0].requestBodyJson).toHaveProperty( 'samples', 42, ); }); describe('response metadata', () => { it('should include timestamp, headers and modelId in response', async () => { const testDate = new Date('2024-01-01T00:00:00Z'); const model = createBasicModel({ currentDate: () => testDate, }); const result = await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.response).toStrictEqual({ timestamp: testDate, modelId: 'accounts/fireworks/models/flux-1-dev-fp8', headers: expect.any(Object), }); }); it('should include response headers from API call', async () => { server.urls['https://api.example.com/*'].response = { type: 'binary', body: Buffer.from('test-binary-content'), headers: { 'x-request-id': 'test-request-id', 'content-type': 'image/png', }, }; const model = createBasicModel(); const result = await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.response.headers).toStrictEqual({ 'content-length': '19', 'x-request-id': 'test-request-id', 'content-type': 'image/png', }); }); }); }); describe('constructor', () => { it('should expose correct provider and model information', () => { const model = createBasicModel(); expect(model.provider).toBe('fireworks'); expect(model.modelId).toBe('accounts/fireworks/models/flux-1-dev-fp8'); expect(model.specificationVersion).toBe('v2'); expect(model.maxImagesPerCall).toBe(1); }); }); }); --- File: /ai/packages/fireworks/src/fireworks-image-model.ts --- import { ImageModelV2, ImageModelV2CallWarning } from '@ai-sdk/provider'; import { combineHeaders, createBinaryResponseHandler, createStatusCodeErrorResponseHandler, FetchFunction, postJsonToApi, } from '@ai-sdk/provider-utils'; import { FireworksImageModelId } from './fireworks-image-options'; interface FireworksImageModelBackendConfig { urlFormat: 'workflows' | 'image_generation'; supportsSize?: boolean; } const modelToBackendConfig: Partial< Record<FireworksImageModelId, FireworksImageModelBackendConfig> > = { 'accounts/fireworks/models/flux-1-dev-fp8': { urlFormat: 'workflows', }, 'accounts/fireworks/models/flux-1-schnell-fp8': { urlFormat: 'workflows', }, 'accounts/fireworks/models/playground-v2-5-1024px-aesthetic': { urlFormat: 'image_generation', supportsSize: true, }, 'accounts/fireworks/models/japanese-stable-diffusion-xl': { urlFormat: 'image_generation', supportsSize: true, }, 'accounts/fireworks/models/playground-v2-1024px-aesthetic': { urlFormat: 'image_generation', supportsSize: true, }, 'accounts/fireworks/models/stable-diffusion-xl-1024-v1-0': { urlFormat: 'image_generation', supportsSize: true, }, 'accounts/fireworks/models/SSD-1B': { urlFormat: 'image_generation', supportsSize: true, }, }; function getUrlForModel( baseUrl: string, modelId: FireworksImageModelId, ): string { switch (modelToBackendConfig[modelId]?.urlFormat) { case 'image_generation': return `${baseUrl}/image_generation/${modelId}`; case 'workflows': default: return `${baseUrl}/workflows/${modelId}/text_to_image`; } } interface FireworksImageModelConfig { provider: string; baseURL: string; headers: () => Record<string, string>; fetch?: FetchFunction; _internal?: { currentDate?: () => Date; }; } export class FireworksImageModel implements ImageModelV2 { readonly specificationVersion = 'v2'; readonly maxImagesPerCall = 1; get provider(): string { return this.config.provider; } constructor( readonly modelId: FireworksImageModelId, private config: FireworksImageModelConfig, ) {} async doGenerate({ prompt, n, size, aspectRatio, seed, providerOptions, headers, abortSignal, }: Parameters<ImageModelV2['doGenerate']>[0]): Promise< Awaited<ReturnType<ImageModelV2['doGenerate']>> > { const warnings: Array<ImageModelV2CallWarning> = []; const backendConfig = modelToBackendConfig[this.modelId]; if (!backendConfig?.supportsSize && size != null) { warnings.push({ type: 'unsupported-setting', setting: 'size', details: 'This model does not support the `size` option. Use `aspectRatio` instead.', }); } // Use supportsSize as a proxy for whether the model does not support // aspectRatio. This invariant holds for the current set of models. if (backendConfig?.supportsSize && aspectRatio != null) { warnings.push({ type: 'unsupported-setting', setting: 'aspectRatio', details: 'This model does not support the `aspectRatio` option.', }); } const splitSize = size?.split('x'); const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { value: response, responseHeaders } = await postJsonToApi({ url: getUrlForModel(this.config.baseURL, this.modelId), headers: combineHeaders(this.config.headers(), headers), body: { prompt, aspect_ratio: aspectRatio, seed, samples: n, ...(splitSize && { width: splitSize[0], height: splitSize[1] }), ...(providerOptions.fireworks ?? {}), }, failedResponseHandler: createStatusCodeErrorResponseHandler(), successfulResponseHandler: createBinaryResponseHandler(), abortSignal, fetch: this.config.fetch, }); return { images: [response], warnings, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, }, }; } } --- File: /ai/packages/fireworks/src/fireworks-image-options.ts --- // https://fireworks.ai/models?type=image export type FireworksImageModelId = | 'accounts/fireworks/models/flux-1-dev-fp8' | 'accounts/fireworks/models/flux-1-schnell-fp8' | 'accounts/fireworks/models/playground-v2-5-1024px-aesthetic' | 'accounts/fireworks/models/japanese-stable-diffusion-xl' | 'accounts/fireworks/models/playground-v2-1024px-aesthetic' | 'accounts/fireworks/models/SSD-1B' | 'accounts/fireworks/models/stable-diffusion-xl-1024-v1-0' | (string & {}); --- File: /ai/packages/fireworks/src/fireworks-provider.test.ts --- import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; import { createFireworks } from './fireworks-provider'; import { LanguageModelV2, EmbeddingModelV2 } from '@ai-sdk/provider'; import { loadApiKey } from '@ai-sdk/provider-utils'; import { OpenAICompatibleChatLanguageModel, OpenAICompatibleCompletionLanguageModel, OpenAICompatibleEmbeddingModel, } from '@ai-sdk/openai-compatible'; import { FireworksImageModel } from './fireworks-image-model'; // Add type assertion for the mocked class const OpenAICompatibleChatLanguageModelMock = OpenAICompatibleChatLanguageModel as unknown as Mock; vi.mock('@ai-sdk/openai-compatible', () => { // Create mock constructor functions that behave like classes const createMockConstructor = (providerName: string) => { const mockConstructor = vi.fn().mockImplementation(function ( this: any, modelId: string, settings: any, ) { this.provider = providerName; this.modelId = modelId; this.settings = settings; }); return mockConstructor; }; return { OpenAICompatibleChatLanguageModel: createMockConstructor('fireworks.chat'), OpenAICompatibleCompletionLanguageModel: createMockConstructor( 'fireworks.completion', ), OpenAICompatibleEmbeddingModel: createMockConstructor( 'fireworks.embedding', ), }; }); vi.mock('@ai-sdk/provider-utils', () => ({ loadApiKey: vi.fn().mockReturnValue('mock-api-key'), withoutTrailingSlash: vi.fn(url => url), })); vi.mock('./fireworks-image-model', () => ({ FireworksImageModel: vi.fn(), })); describe('FireworksProvider', () => { let mockLanguageModel: LanguageModelV2; let mockEmbeddingModel: EmbeddingModelV2<string>; beforeEach(() => { // Mock implementations of models mockLanguageModel = { // Add any required methods for LanguageModelV2 } as LanguageModelV2; mockEmbeddingModel = { // Add any required methods for EmbeddingModelV2 } as EmbeddingModelV2<string>; // Reset mocks vi.clearAllMocks(); }); describe('createFireworks', () => { it('should create a FireworksProvider instance with default options', () => { const provider = createFireworks(); const model = provider('model-id'); // Use the mocked version const constructorCall = OpenAICompatibleChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; config.headers(); expect(loadApiKey).toHaveBeenCalledWith({ apiKey: undefined, environmentVariableName: 'FIREWORKS_API_KEY', description: 'Fireworks API key', }); }); it('should create a FireworksProvider instance with custom options', () => { const options = { apiKey: 'custom-key', baseURL: 'https://custom.url', headers: { 'Custom-Header': 'value' }, }; const provider = createFireworks(options); const model = provider('model-id'); const constructorCall = OpenAICompatibleChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; config.headers(); expect(loadApiKey).toHaveBeenCalledWith({ apiKey: 'custom-key', environmentVariableName: 'FIREWORKS_API_KEY', description: 'Fireworks API key', }); }); it('should return a chat model when called as a function', () => { const provider = createFireworks(); const modelId = 'foo-model-id'; const model = provider(modelId); expect(model).toBeInstanceOf(OpenAICompatibleChatLanguageModel); }); }); describe('chatModel', () => { it('should construct a chat model with correct configuration', () => { const provider = createFireworks(); const modelId = 'fireworks-chat-model'; const model = provider.chatModel(modelId); expect(model).toBeInstanceOf(OpenAICompatibleChatLanguageModel); }); }); describe('completionModel', () => { it('should construct a completion model with correct configuration', () => { const provider = createFireworks(); const modelId = 'fireworks-completion-model'; const model = provider.completionModel(modelId); expect(model).toBeInstanceOf(OpenAICompatibleCompletionLanguageModel); }); }); describe('textEmbeddingModel', () => { it('should construct a text embedding model with correct configuration', () => { const provider = createFireworks(); const modelId = 'fireworks-embedding-model'; const model = provider.textEmbeddingModel(modelId); expect(model).toBeInstanceOf(OpenAICompatibleEmbeddingModel); }); }); describe('image', () => { it('should construct an image model with correct configuration', () => { const provider = createFireworks(); const modelId = 'accounts/fireworks/models/flux-1-dev-fp8'; const model = provider.image(modelId); expect(model).toBeInstanceOf(FireworksImageModel); expect(FireworksImageModel).toHaveBeenCalledWith( modelId, expect.objectContaining({ provider: 'fireworks.image', baseURL: 'https://api.fireworks.ai/inference/v1', }), ); }); it('should use default settings when none provided', () => { const provider = createFireworks(); const modelId = 'accounts/fireworks/models/flux-1-dev-fp8'; const model = provider.image(modelId); expect(model).toBeInstanceOf(FireworksImageModel); expect(FireworksImageModel).toHaveBeenCalledWith( modelId, expect.any(Object), ); }); it('should respect custom baseURL', () => { const customBaseURL = 'https://custom.api.fireworks.ai'; const provider = createFireworks({ baseURL: customBaseURL }); const modelId = 'accounts/fireworks/models/flux-1-dev-fp8'; provider.image(modelId); expect(FireworksImageModel).toHaveBeenCalledWith( modelId, expect.objectContaining({ baseURL: customBaseURL, }), ); }); }); }); --- File: /ai/packages/fireworks/src/fireworks-provider.ts --- import { OpenAICompatibleChatLanguageModel, OpenAICompatibleCompletionLanguageModel, OpenAICompatibleEmbeddingModel, ProviderErrorStructure, } from '@ai-sdk/openai-compatible'; import { EmbeddingModelV2, ImageModelV2, LanguageModelV2, ProviderV2, } from '@ai-sdk/provider'; import { FetchFunction, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { FireworksChatModelId } from './fireworks-chat-options'; import { FireworksCompletionModelId } from './fireworks-completion-options'; import { FireworksEmbeddingModelId } from './fireworks-embedding-options'; import { FireworksImageModel } from './fireworks-image-model'; import { FireworksImageModelId } from './fireworks-image-options'; export type FireworksErrorData = z.infer<typeof fireworksErrorSchema>; const fireworksErrorSchema = z.object({ error: z.string(), }); const fireworksErrorStructure: ProviderErrorStructure<FireworksErrorData> = { errorSchema: fireworksErrorSchema, errorToMessage: data => data.error, }; export interface FireworksProviderSettings { /** Fireworks API key. Default value is taken from the `FIREWORKS_API_KEY` environment variable. */ apiKey?: string; /** Base URL for the API calls. */ baseURL?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } export interface FireworksProvider extends ProviderV2 { /** Creates a model for text generation. */ (modelId: FireworksChatModelId): LanguageModelV2; /** Creates a chat model for text generation. */ chatModel(modelId: FireworksChatModelId): LanguageModelV2; /** Creates a completion model for text generation. */ completionModel(modelId: FireworksCompletionModelId): LanguageModelV2; /** Creates a chat model for text generation. */ languageModel(modelId: FireworksChatModelId): LanguageModelV2; /** Creates a text embedding model for text generation. */ textEmbeddingModel( modelId: FireworksEmbeddingModelId, ): EmbeddingModelV2<string>; /** Creates a model for image generation. */ image(modelId: FireworksImageModelId): ImageModelV2; /** Creates a model for image generation. */ imageModel(modelId: FireworksImageModelId): ImageModelV2; } const defaultBaseURL = 'https://api.fireworks.ai/inference/v1'; export function createFireworks( options: FireworksProviderSettings = {}, ): FireworksProvider { const baseURL = withoutTrailingSlash(options.baseURL ?? defaultBaseURL); const getHeaders = () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'FIREWORKS_API_KEY', description: 'Fireworks API key', })}`, ...options.headers, }); interface CommonModelConfig { provider: string; url: ({ path }: { path: string }) => string; headers: () => Record<string, string>; fetch?: FetchFunction; } const getCommonModelConfig = (modelType: string): CommonModelConfig => ({ provider: `fireworks.${modelType}`, url: ({ path }) => `${baseURL}${path}`, headers: getHeaders, fetch: options.fetch, }); const createChatModel = (modelId: FireworksChatModelId) => { return new OpenAICompatibleChatLanguageModel(modelId, { ...getCommonModelConfig('chat'), errorStructure: fireworksErrorStructure, }); }; const createCompletionModel = (modelId: FireworksCompletionModelId) => new OpenAICompatibleCompletionLanguageModel(modelId, { ...getCommonModelConfig('completion'), errorStructure: fireworksErrorStructure, }); const createTextEmbeddingModel = (modelId: FireworksEmbeddingModelId) => new OpenAICompatibleEmbeddingModel(modelId, { ...getCommonModelConfig('embedding'), errorStructure: fireworksErrorStructure, }); const createImageModel = (modelId: FireworksImageModelId) => new FireworksImageModel(modelId, { ...getCommonModelConfig('image'), baseURL: baseURL ?? defaultBaseURL, }); const provider = (modelId: FireworksChatModelId) => createChatModel(modelId); provider.completionModel = createCompletionModel; provider.chatModel = createChatModel; provider.languageModel = createChatModel; provider.textEmbeddingModel = createTextEmbeddingModel; provider.image = createImageModel; provider.imageModel = createImageModel; return provider; } export const fireworks = createFireworks(); --- File: /ai/packages/fireworks/src/index.ts --- export type { FireworksEmbeddingModelId, FireworksEmbeddingProviderOptions, } from './fireworks-embedding-options'; export { FireworksImageModel } from './fireworks-image-model'; export type { FireworksImageModelId } from './fireworks-image-options'; export { fireworks, createFireworks } from './fireworks-provider'; export type { FireworksProvider, FireworksProviderSettings, FireworksErrorData, } from './fireworks-provider'; --- File: /ai/packages/fireworks/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/fireworks/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/fireworks/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/gateway/src/errors/as-gateway-error.ts --- import { APICallError } from '@ai-sdk/provider'; import { extractApiCallResponse, GatewayError } from '.'; import { createGatewayErrorFromResponse } from './create-gateway-error'; export function asGatewayError( error: unknown, authMethod?: 'api-key' | 'oidc', ) { if (GatewayError.isInstance(error)) { return error; } if (APICallError.isInstance(error)) { return createGatewayErrorFromResponse({ response: extractApiCallResponse(error), statusCode: error.statusCode ?? 500, defaultMessage: 'Gateway request failed', cause: error, authMethod, }); } return createGatewayErrorFromResponse({ response: {}, statusCode: 500, defaultMessage: error instanceof Error ? `Gateway request failed: ${error.message}` : 'Unknown Gateway error', cause: error, authMethod, }); } --- File: /ai/packages/gateway/src/errors/create-gateway-error.test.ts --- import { describe, expect, it } from 'vitest'; import { createGatewayErrorFromResponse, GatewayAuthenticationError, GatewayInvalidRequestError, GatewayRateLimitError, GatewayModelNotFoundError, GatewayInternalServerError, GatewayResponseError, type GatewayErrorResponse, } from './index'; describe('Valid error responses', () => { it('should create GatewayAuthenticationError for authentication_error type', () => { const response: GatewayErrorResponse = { error: { message: 'Invalid API key', type: 'authentication_error', }, }; const error = createGatewayErrorFromResponse({ response, statusCode: 401, }); expect(error).toBeInstanceOf(GatewayAuthenticationError); expect(error.message).toContain('No authentication provided'); expect(error.statusCode).toBe(401); expect(error.type).toBe('authentication_error'); }); it('should create GatewayInvalidRequestError for invalid_request_error type', () => { const response: GatewayErrorResponse = { error: { message: 'Missing required parameter', type: 'invalid_request_error', }, }; const error = createGatewayErrorFromResponse({ response, statusCode: 400, }); expect(error).toBeInstanceOf(GatewayInvalidRequestError); expect(error.message).toBe('Missing required parameter'); expect(error.statusCode).toBe(400); }); it('should create GatewayRateLimitError for rate_limit_exceeded type', () => { const response: GatewayErrorResponse = { error: { message: 'Rate limit exceeded. Try again later.', type: 'rate_limit_exceeded', }, }; const error = createGatewayErrorFromResponse({ response, statusCode: 429, }); expect(error).toBeInstanceOf(GatewayRateLimitError); expect(error.message).toBe('Rate limit exceeded. Try again later.'); expect(error.statusCode).toBe(429); }); it('should create GatewayModelNotFoundError for model_not_found type', () => { const response: GatewayErrorResponse = { error: { message: 'Model not available', type: 'model_not_found', param: { modelId: 'gpt-4-turbo' }, }, }; const error = createGatewayErrorFromResponse({ response, statusCode: 404, }); expect(error).toBeInstanceOf(GatewayModelNotFoundError); expect(error.message).toBe('Model not available'); expect(error.statusCode).toBe(404); expect((error as GatewayModelNotFoundError).modelId).toBe('gpt-4-turbo'); }); it('should create GatewayModelNotFoundError without modelId for invalid param', () => { const response: GatewayErrorResponse = { error: { message: 'Model not available', type: 'model_not_found', param: { invalidField: 'value' }, }, }; const error = createGatewayErrorFromResponse({ response, statusCode: 404, }); expect(error).toBeInstanceOf(GatewayModelNotFoundError); expect((error as GatewayModelNotFoundError).modelId).toBeUndefined(); }); it('should create GatewayInternalServerError for internal_server_error type', () => { const response: GatewayErrorResponse = { error: { message: 'Internal server error occurred', type: 'internal_server_error', }, }; const error = createGatewayErrorFromResponse({ response, statusCode: 500, }); expect(error).toBeInstanceOf(GatewayInternalServerError); expect(error.message).toBe('Internal server error occurred'); expect(error.statusCode).toBe(500); }); it('should create GatewayInternalServerError for unknown error type', () => { const response: GatewayErrorResponse = { error: { message: 'Unknown error occurred', type: 'unknown_error_type', }, }; const error = createGatewayErrorFromResponse({ response, statusCode: 500, }); expect(error).toBeInstanceOf(GatewayInternalServerError); expect(error.message).toBe('Unknown error occurred'); expect(error.statusCode).toBe(500); }); }); describe('Error response edge cases', () => { it('should preserve empty string messages from Gateway', () => { const response: GatewayErrorResponse = { error: { message: '', type: 'authentication_error', }, }; const error = createGatewayErrorFromResponse({ response, statusCode: 401, defaultMessage: 'Custom default message', }); expect(error.message).toContain('No authentication provided'); // Uses contextual message }); it('should use defaultMessage when response message is null', () => { const response = { error: { message: null, type: 'authentication_error', }, }; const error = createGatewayErrorFromResponse({ response, statusCode: 401, defaultMessage: 'Custom default message', }); // When the response doesn't pass schema validation, it creates a response error expect(error).toBeInstanceOf(GatewayResponseError); expect(error.message).toBe( 'Invalid error response format: Custom default message', ); // Verify debugging information is included const responseError = error as GatewayResponseError; expect(responseError.response).toBe(response); expect(responseError.validationError).toBeDefined(); }); it('should handle error type as null', () => { const response: GatewayErrorResponse = { error: { message: 'Some error', type: null, }, }; const error = createGatewayErrorFromResponse({ response, statusCode: 500, }); expect(error).toBeInstanceOf(GatewayInternalServerError); }); it('should include cause in the created error', () => { const originalCause = new Error('Original network error'); const response: GatewayErrorResponse = { error: { message: 'Gateway timeout', type: 'internal_server_error', }, }; const error = createGatewayErrorFromResponse({ response, statusCode: 504, cause: originalCause, }); expect(error.cause).toBe(originalCause); }); }); describe('Malformed responses', () => { it('should create GatewayResponseError for completely invalid response', () => { const response = { invalidField: 'value', }; const error = createGatewayErrorFromResponse({ response, statusCode: 500, }); expect(error).toBeInstanceOf(GatewayResponseError); expect(error.message).toBe( 'Invalid error response format: Gateway request failed', ); expect(error.statusCode).toBe(500); // Verify debugging information is included const responseError = error as GatewayResponseError; expect(responseError.response).toBe(response); expect(responseError.validationError).toBeDefined(); expect(responseError.validationError?.issues).toBeDefined(); }); it('should create GatewayResponseError for missing error field', () => { const response = { data: 'some data', }; const error = createGatewayErrorFromResponse({ response, statusCode: 500, defaultMessage: 'Custom error message', }); expect(error).toBeInstanceOf(GatewayResponseError); expect(error.message).toBe( 'Invalid error response format: Custom error message', ); // Verify debugging information is included const responseError = error as GatewayResponseError; expect(responseError.response).toBe(response); expect(responseError.validationError).toBeDefined(); }); it('should create GatewayResponseError for null response', () => { const response = null; const error = createGatewayErrorFromResponse({ response, statusCode: 500, }); expect(error).toBeInstanceOf(GatewayResponseError); expect(error.message).toBe( 'Invalid error response format: Gateway request failed', ); // Verify debugging information is included const responseError = error as GatewayResponseError; expect(responseError.response).toBe(response); expect(responseError.validationError).toBeDefined(); }); it('should create GatewayResponseError for string response', () => { const response = 'Error string'; const error = createGatewayErrorFromResponse({ response, statusCode: 500, }); expect(error).toBeInstanceOf(GatewayResponseError); expect(error.message).toBe( 'Invalid error response format: Gateway request failed', ); // Verify debugging information is included const responseError = error as GatewayResponseError; expect(responseError.response).toBe(response); expect(responseError.validationError).toBeDefined(); }); it('should create GatewayResponseError for array response', () => { const response = ['error', 'array']; const error = createGatewayErrorFromResponse({ response, statusCode: 500, }); expect(error).toBeInstanceOf(GatewayResponseError); expect(error.message).toBe( 'Invalid error response format: Gateway request failed', ); // Verify debugging information is included const responseError = error as GatewayResponseError; expect(responseError.response).toBe(response); expect(responseError.validationError).toBeDefined(); }); }); describe('Object parameter validation', () => { it('should use default defaultMessage when not provided', () => { const response = { invalidField: 'value', }; const error = createGatewayErrorFromResponse({ response, statusCode: 500, }); expect(error).toBeInstanceOf(GatewayResponseError); expect(error.message).toBe( 'Invalid error response format: Gateway request failed', ); // Verify debugging information is included const responseError = error as GatewayResponseError; expect(responseError.response).toBe(response); expect(responseError.validationError).toBeDefined(); }); it('should handle undefined cause', () => { const response: GatewayErrorResponse = { error: { message: 'Test error', type: 'authentication_error', }, }; const error = createGatewayErrorFromResponse({ response, statusCode: 401, cause: undefined, }); expect(error.cause).toBeUndefined(); }); }); describe('Complex scenarios', () => { it('should handle model_not_found with missing param field', () => { const response: GatewayErrorResponse = { error: { message: 'Model not found', type: 'model_not_found', // param field missing }, }; const error = createGatewayErrorFromResponse({ response, statusCode: 404, }); expect(error).toBeInstanceOf(GatewayModelNotFoundError); expect((error as GatewayModelNotFoundError).modelId).toBeUndefined(); }); it('should handle response with extra fields', () => { const response = { error: { message: 'Test error', type: 'authentication_error', code: 'AUTH_FAILED', param: null, extraField: 'should be ignored', }, metadata: 'should be ignored', }; const error = createGatewayErrorFromResponse({ response, statusCode: 401, }); expect(error).toBeInstanceOf(GatewayAuthenticationError); expect(error.message).toContain('No authentication provided'); }); it('should preserve error properties correctly', () => { const originalCause = new TypeError('Type error'); const response: GatewayErrorResponse = { error: { message: 'Rate limit hit', type: 'rate_limit_exceeded', }, }; const error = createGatewayErrorFromResponse({ response, statusCode: 429, defaultMessage: 'Fallback message', cause: originalCause, }); expect(error).toBeInstanceOf(GatewayRateLimitError); expect(error.message).toBe('Rate limit hit'); // Uses response message, not default expect(error.statusCode).toBe(429); expect(error.cause).toBe(originalCause); expect(error.name).toBe('GatewayRateLimitError'); expect(error.type).toBe('rate_limit_exceeded'); }); }); describe('authentication_error with authMethod context', () => { it('should create contextual error for API key authentication failure', () => { const error = createGatewayErrorFromResponse({ response: { error: { type: 'authentication_error', message: 'Invalid API key', }, }, statusCode: 401, authMethod: 'api-key', }); expect(error).toBeInstanceOf(GatewayAuthenticationError); expect(error.message).toContain('Invalid API key provided'); expect(error.statusCode).toBe(401); }); it('should create contextual error for OIDC authentication failure', () => { const error = createGatewayErrorFromResponse({ response: { error: { type: 'authentication_error', message: 'Invalid OIDC token', }, }, statusCode: 401, authMethod: 'oidc', }); expect(error).toBeInstanceOf(GatewayAuthenticationError); expect(error.message).toContain('Invalid OIDC token provided'); expect(error.statusCode).toBe(401); }); it('should create contextual error without authMethod context', () => { const error = createGatewayErrorFromResponse({ response: { error: { type: 'authentication_error', message: 'Authentication failed', }, }, statusCode: 401, }); expect(error).toBeInstanceOf(GatewayAuthenticationError); expect(error.message).toContain('No authentication provided'); expect(error.statusCode).toBe(401); }); }); --- File: /ai/packages/gateway/src/errors/create-gateway-error.ts --- import { z } from 'zod/v4'; import type { GatewayError } from './gateway-error'; import { GatewayAuthenticationError } from './gateway-authentication-error'; import { GatewayInvalidRequestError } from './gateway-invalid-request-error'; import { GatewayRateLimitError } from './gateway-rate-limit-error'; import { GatewayModelNotFoundError, modelNotFoundParamSchema, } from './gateway-model-not-found-error'; import { GatewayInternalServerError } from './gateway-internal-server-error'; import { GatewayResponseError } from './gateway-response-error'; export function createGatewayErrorFromResponse({ response, statusCode, defaultMessage = 'Gateway request failed', cause, authMethod, }: { response: unknown; statusCode: number; defaultMessage?: string; cause?: unknown; authMethod?: 'api-key' | 'oidc'; }): GatewayError { const parseResult = gatewayErrorResponseSchema.safeParse(response); if (!parseResult.success) { return new GatewayResponseError({ message: `Invalid error response format: ${defaultMessage}`, statusCode, response, validationError: parseResult.error, cause, }); } const validatedResponse: GatewayErrorResponse = parseResult.data; const errorType = validatedResponse.error.type; const message = validatedResponse.error.message; switch (errorType) { case 'authentication_error': return GatewayAuthenticationError.createContextualError({ apiKeyProvided: authMethod === 'api-key', oidcTokenProvided: authMethod === 'oidc', statusCode, cause, }); case 'invalid_request_error': return new GatewayInvalidRequestError({ message, statusCode, cause }); case 'rate_limit_exceeded': return new GatewayRateLimitError({ message, statusCode, cause }); case 'model_not_found': { const modelResult = modelNotFoundParamSchema.safeParse( validatedResponse.error.param, ); return new GatewayModelNotFoundError({ message, statusCode, modelId: modelResult.success ? modelResult.data.modelId : undefined, cause, }); } case 'internal_server_error': return new GatewayInternalServerError({ message, statusCode, cause }); default: return new GatewayInternalServerError({ message, statusCode, cause }); } } const gatewayErrorResponseSchema = z.object({ error: z.object({ message: z.string(), type: z.string().nullish(), param: z.unknown().nullish(), code: z.union([z.string(), z.number()]).nullish(), }), }); export type GatewayErrorResponse = z.infer<typeof gatewayErrorResponseSchema>; --- File: /ai/packages/gateway/src/errors/extract-api-call-response.test.ts --- import { describe, expect, it } from 'vitest'; import { APICallError } from '@ai-sdk/provider'; import { extractApiCallResponse } from './extract-api-call-response'; describe('extractResponseFromAPICallError', () => { describe('when error.data is available', () => { it('should return error.data when successfully parsed by AI SDK', () => { const parsedData = { error: { message: 'Parsed error', type: 'authentication_error' }, }; const apiCallError = new APICallError({ message: 'Request failed', statusCode: 401, data: parsedData, responseHeaders: {}, responseBody: JSON.stringify({ different: 'data' }), url: 'http://test.url', requestBodyValues: {}, }); const result = extractApiCallResponse(apiCallError); expect(result).toBe(parsedData); // Should prefer parsed data over responseBody }); it('should return error.data even when it is null', () => { const apiCallError = new APICallError({ message: 'Request failed', statusCode: 500, data: null, responseHeaders: {}, responseBody: '{"fallback": "data"}', url: 'http://test.url', requestBodyValues: {}, }); const result = extractApiCallResponse(apiCallError); expect(result).toBeNull(); // Should return null, not fallback to responseBody }); it('should return error.data even when it is an empty object', () => { const emptyData = {}; const apiCallError = new APICallError({ message: 'Request failed', statusCode: 400, data: emptyData, responseHeaders: {}, responseBody: '{"fallback": "data"}', url: 'http://test.url', requestBodyValues: {}, }); const result = extractApiCallResponse(apiCallError); expect(result).toBe(emptyData); // Should return empty object, not fallback }); }); describe('when error.data is undefined', () => { it('should parse and return responseBody as JSON when valid', () => { const responseData = { ferror: { message: 'Malformed error', type: 'model_not_found' }, }; const apiCallError = new APICallError({ message: 'Request failed', statusCode: 404, data: undefined, responseHeaders: {}, responseBody: JSON.stringify(responseData), url: 'http://test.url', requestBodyValues: {}, }); const result = extractApiCallResponse(apiCallError); expect(result).toEqual(responseData); }); it('should return raw responseBody when JSON parsing fails', () => { const invalidJson = 'This is not valid JSON'; const apiCallError = new APICallError({ message: 'Request failed', statusCode: 500, data: undefined, responseHeaders: {}, responseBody: invalidJson, url: 'http://test.url', requestBodyValues: {}, }); const result = extractApiCallResponse(apiCallError); expect(result).toBe(invalidJson); }); it('should handle HTML error responses', () => { const htmlResponse = '<html><body><h1>500 Internal Server Error</h1></body></html>'; const apiCallError = new APICallError({ message: 'Request failed', statusCode: 500, data: undefined, responseHeaders: {}, responseBody: htmlResponse, url: 'http://test.url', requestBodyValues: {}, }); const result = extractApiCallResponse(apiCallError); expect(result).toBe(htmlResponse); }); it('should handle empty string responseBody', () => { const apiCallError = new APICallError({ message: 'Request failed', statusCode: 502, data: undefined, responseHeaders: {}, responseBody: '', url: 'http://test.url', requestBodyValues: {}, }); const result = extractApiCallResponse(apiCallError); expect(result).toBe(''); }); it('should handle malformed JSON gracefully', () => { const malformedJson = '{"incomplete": json'; const apiCallError = new APICallError({ message: 'Request failed', statusCode: 500, data: undefined, responseHeaders: {}, responseBody: malformedJson, url: 'http://test.url', requestBodyValues: {}, }); const result = extractApiCallResponse(apiCallError); expect(result).toBe(malformedJson); // Should return raw string, not throw }); it('should parse complex nested JSON structures', () => { const complexData = { error: { message: 'Complex error', type: 'validation_error', details: { field: 'prompt', issues: [ { code: 'too_long', message: 'Prompt exceeds maximum length' }, { code: 'invalid_format', message: 'Contains invalid characters', }, ], }, }, metadata: { requestId: '12345', timestamp: '2024-01-01T00:00:00Z', }, }; const apiCallError = new APICallError({ message: 'Request failed', statusCode: 400, data: undefined, responseHeaders: {}, responseBody: JSON.stringify(complexData), url: 'http://test.url', requestBodyValues: {}, }); const result = extractApiCallResponse(apiCallError); expect(result).toEqual(complexData); }); }); describe('when responseBody is not available', () => { it('should return empty object when both data and responseBody are undefined', () => { const apiCallError = new APICallError({ message: 'Request failed', statusCode: 500, data: undefined, responseHeaders: {}, responseBody: undefined as any, // Simulating missing responseBody url: 'http://test.url', requestBodyValues: {}, }); const result = extractApiCallResponse(apiCallError); expect(result).toEqual({}); }); it('should return empty object when responseBody is null', () => { const apiCallError = new APICallError({ message: 'Request failed', statusCode: 500, data: undefined, responseHeaders: {}, responseBody: null as any, url: 'http://test.url', requestBodyValues: {}, }); const result = extractApiCallResponse(apiCallError); expect(result).toEqual({}); }); }); describe('edge cases', () => { it('should handle numeric responseBody', () => { const apiCallError = new APICallError({ message: 'Request failed', statusCode: 500, data: undefined, responseHeaders: {}, responseBody: '404', url: 'http://test.url', requestBodyValues: {}, }); const result = extractApiCallResponse(apiCallError); expect(result).toBe(404); // Should parse as number }); it('should handle boolean responseBody', () => { const apiCallError = new APICallError({ message: 'Request failed', statusCode: 500, data: undefined, responseHeaders: {}, responseBody: 'true', url: 'http://test.url', requestBodyValues: {}, }); const result = extractApiCallResponse(apiCallError); expect(result).toBe(true); // Should parse as boolean }); it('should handle array responseBody', () => { const arrayData = ['error1', 'error2', 'error3']; const apiCallError = new APICallError({ message: 'Request failed', statusCode: 400, data: undefined, responseHeaders: {}, responseBody: JSON.stringify(arrayData), url: 'http://test.url', requestBodyValues: {}, }); const result = extractApiCallResponse(apiCallError); expect(result).toEqual(arrayData); }); }); }); --- File: /ai/packages/gateway/src/errors/extract-api-call-response.ts --- import type { APICallError } from '@ai-sdk/provider'; export function extractApiCallResponse(error: APICallError): unknown { if (error.data !== undefined) { return error.data; } if (error.responseBody != null) { try { return JSON.parse(error.responseBody); } catch { return error.responseBody; } } return {}; } --- File: /ai/packages/gateway/src/errors/gateway-authentication-error.ts --- import { GatewayError } from './gateway-error'; const name = 'GatewayAuthenticationError'; const marker = `vercel.ai.gateway.error.${name}`; const symbol = Symbol.for(marker); /** * Authentication failed - invalid API key or OIDC token */ export class GatewayAuthenticationError extends GatewayError { private readonly [symbol] = true; // used in isInstance readonly name = name; readonly type = 'authentication_error'; constructor({ message = 'Authentication failed', statusCode = 401, cause, }: { message?: string; statusCode?: number; cause?: unknown; } = {}) { super({ message, statusCode, cause }); } static isInstance(error: unknown): error is GatewayAuthenticationError { return GatewayError.hasMarker(error) && symbol in error; } /** * Creates a contextual error message when authentication fails */ static createContextualError({ apiKeyProvided, oidcTokenProvided, message = 'Authentication failed', statusCode = 401, cause, }: { apiKeyProvided: boolean; oidcTokenProvided: boolean; message?: string; statusCode?: number; cause?: unknown; }): GatewayAuthenticationError { let contextualMessage: string; if (apiKeyProvided) { contextualMessage = `AI Gateway authentication failed: Invalid API key provided. The token is expected to be provided via the 'apiKey' option or 'AI_GATEWAY_API_KEY' environment variable.`; } else if (oidcTokenProvided) { contextualMessage = `AI Gateway authentication failed: Invalid OIDC token provided. The token is expected to be provided via the 'VERCEL_OIDC_TOKEN' environment variable. It expires every 12 hours. - make sure your Vercel project settings have OIDC enabled - if running locally with 'vercel dev', the token is automatically obtained and refreshed - if running locally with your own dev server, run 'vercel env pull' to fetch the token - in production/preview, the token is automatically obtained and refreshed Alternative: Provide an API key via 'apiKey' option or 'AI_GATEWAY_API_KEY' environment variable.`; } else { contextualMessage = `AI Gateway authentication failed: No authentication provided. Provide either an API key or OIDC token. API key instructions: The token is expected to be provided via the 'apiKey' option or 'AI_GATEWAY_API_KEY' environment variable. OIDC token instructions: The token is expected to be provided via the 'VERCEL_OIDC_TOKEN' environment variable. It expires every 12 hours. - make sure your Vercel project settings have OIDC enabled - if running locally with 'vercel dev', the token is automatically obtained and refreshed - if running locally with your own dev server, run 'vercel env pull' to fetch the token - in production/preview, the token is automatically obtained and refreshed`; } return new GatewayAuthenticationError({ message: contextualMessage, statusCode, cause, }); } } --- File: /ai/packages/gateway/src/errors/gateway-error-types.test.ts --- import { describe, expect, it } from 'vitest'; import { GatewayError, GatewayAuthenticationError, GatewayInvalidRequestError, GatewayRateLimitError, GatewayModelNotFoundError, GatewayInternalServerError, GatewayResponseError, } from './index'; describe('GatewayAuthenticationError', () => { it('should create error with default values', () => { const error = new GatewayAuthenticationError(); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(GatewayError); expect(error.name).toBe('GatewayAuthenticationError'); expect(error.type).toBe('authentication_error'); expect(error.message).toBe('Authentication failed'); expect(error.statusCode).toBe(401); expect(error.cause).toBeUndefined(); }); it('should create error with custom values', () => { const cause = new Error('Original error'); const error = new GatewayAuthenticationError({ message: 'Custom auth failed', statusCode: 403, cause, }); expect(error.message).toBe('Custom auth failed'); expect(error.statusCode).toBe(403); expect(error.cause).toBe(cause); }); it('should be identifiable via instance check', () => { const error = new GatewayAuthenticationError(); const otherError = new Error('Not a gateway error'); expect(GatewayAuthenticationError.isInstance(error)).toBe(true); expect(GatewayAuthenticationError.isInstance(otherError)).toBe(false); expect(GatewayError.isInstance(error)).toBe(true); }); describe('createContextualError', () => { it('should create error for invalid API key only', () => { const error = GatewayAuthenticationError.createContextualError({ apiKeyProvided: true, oidcTokenProvided: false, }); expect(error.message).toContain('Invalid API key provided'); expect(error.statusCode).toBe(401); }); it('should create error for invalid OIDC token only', () => { const error = GatewayAuthenticationError.createContextualError({ apiKeyProvided: false, oidcTokenProvided: true, }); expect(error.message).toContain('Invalid OIDC token provided'); expect(error.statusCode).toBe(401); }); it('should create error for no authentication provided', () => { const error = GatewayAuthenticationError.createContextualError({ apiKeyProvided: false, oidcTokenProvided: false, }); expect(error.message).toContain('No authentication provided'); expect(error.message).toContain('VERCEL_OIDC_TOKEN'); expect(error.statusCode).toBe(401); }); it('should prioritize API key error when both were provided', () => { const error = GatewayAuthenticationError.createContextualError({ apiKeyProvided: true, oidcTokenProvided: true, }); expect(error.message).toContain('Invalid API key provided'); expect(error.statusCode).toBe(401); }); it('should create error for neither provided (legacy test)', () => { const error = GatewayAuthenticationError.createContextualError({ apiKeyProvided: false, oidcTokenProvided: false, }); expect(error.message).toContain('No authentication provided'); expect(error.message).toContain('VERCEL_OIDC_TOKEN'); expect(error.statusCode).toBe(401); }); }); }); describe('GatewayInvalidRequestError', () => { it('should create error with default values', () => { const error = new GatewayInvalidRequestError(); expect(error.name).toBe('GatewayInvalidRequestError'); expect(error.type).toBe('invalid_request_error'); expect(error.message).toBe('Invalid request'); expect(error.statusCode).toBe(400); }); it('should create error with custom values', () => { const error = new GatewayInvalidRequestError({ message: 'Missing required field', statusCode: 422, }); expect(error.message).toBe('Missing required field'); expect(error.statusCode).toBe(422); }); it('should be identifiable via instance check', () => { const error = new GatewayInvalidRequestError(); expect(GatewayInvalidRequestError.isInstance(error)).toBe(true); expect(GatewayAuthenticationError.isInstance(error)).toBe(false); expect(GatewayError.isInstance(error)).toBe(true); }); }); describe('GatewayRateLimitError', () => { it('should create error with default values', () => { const error = new GatewayRateLimitError(); expect(error.name).toBe('GatewayRateLimitError'); expect(error.type).toBe('rate_limit_exceeded'); expect(error.message).toBe('Rate limit exceeded'); expect(error.statusCode).toBe(429); }); it('should be identifiable via instance check', () => { const error = new GatewayRateLimitError(); expect(GatewayRateLimitError.isInstance(error)).toBe(true); expect(GatewayError.isInstance(error)).toBe(true); }); }); describe('GatewayModelNotFoundError', () => { it('should create error with default values', () => { const error = new GatewayModelNotFoundError(); expect(error.name).toBe('GatewayModelNotFoundError'); expect(error.type).toBe('model_not_found'); expect(error.message).toBe('Model not found'); expect(error.statusCode).toBe(404); expect(error.modelId).toBeUndefined(); }); it('should create error with model ID', () => { const error = new GatewayModelNotFoundError({ message: 'Model gpt-4 not found', modelId: 'gpt-4', }); expect(error.message).toBe('Model gpt-4 not found'); expect(error.modelId).toBe('gpt-4'); }); it('should be identifiable via instance check', () => { const error = new GatewayModelNotFoundError(); expect(GatewayModelNotFoundError.isInstance(error)).toBe(true); expect(GatewayError.isInstance(error)).toBe(true); }); }); describe('GatewayInternalServerError', () => { it('should create error with default values', () => { const error = new GatewayInternalServerError(); expect(error.name).toBe('GatewayInternalServerError'); expect(error.type).toBe('internal_server_error'); expect(error.message).toBe('Internal server error'); expect(error.statusCode).toBe(500); }); it('should be identifiable via instance check', () => { const error = new GatewayInternalServerError(); expect(GatewayInternalServerError.isInstance(error)).toBe(true); expect(GatewayError.isInstance(error)).toBe(true); }); }); describe('GatewayResponseError', () => { it('should create error with default values', () => { const error = new GatewayResponseError(); expect(error.name).toBe('GatewayResponseError'); expect(error.type).toBe('response_error'); expect(error.message).toBe('Invalid response from Gateway'); expect(error.statusCode).toBe(502); expect(error.response).toBeUndefined(); expect(error.validationError).toBeUndefined(); }); it('should create error with response and validation error details', () => { const response = { invalidField: 'value' }; const validationError = { issues: [{ path: ['error'], message: 'Required' }], } as any; // Mock ZodError structure const error = new GatewayResponseError({ message: 'Custom parsing error', statusCode: 422, response, validationError, }); expect(error.message).toBe('Custom parsing error'); expect(error.statusCode).toBe(422); expect(error.response).toBe(response); expect(error.validationError).toBe(validationError); }); it('should be identifiable via instance check', () => { const error = new GatewayResponseError(); expect(GatewayResponseError.isInstance(error)).toBe(true); expect(GatewayError.isInstance(error)).toBe(true); }); }); describe('Cross-realm instance checking', () => { it('should work with symbol-based type checking', () => { const error = new GatewayAuthenticationError(); // Simulate different realm by creating a new instance in different context const gatewayErrorMarker = Symbol.for('vercel.ai.gateway.error'); const authErrorMarker = Symbol.for( 'vercel.ai.gateway.error.GatewayAuthenticationError', ); // Verify the symbols are present expect((error as any)[gatewayErrorMarker]).toBe(true); expect((error as any)[authErrorMarker]).toBe(true); // Test cross-realm safety expect(GatewayError.hasMarker(error)).toBe(true); expect(GatewayAuthenticationError.isInstance(error)).toBe(true); }); }); describe('Error inheritance chain', () => { it('should maintain proper inheritance', () => { const error = new GatewayAuthenticationError(); expect(error instanceof Error).toBe(true); expect(error instanceof GatewayError).toBe(true); expect(error instanceof GatewayAuthenticationError).toBe(true); }); it('should have proper stack traces', () => { const error = new GatewayAuthenticationError({ message: 'Test error', }); expect(error.stack).toBeDefined(); expect(error.stack).toContain('GatewayAuthenticationError'); expect(error.stack).toContain('Test error'); }); }); --- File: /ai/packages/gateway/src/errors/gateway-error.ts --- const marker = 'vercel.ai.gateway.error'; const symbol = Symbol.for(marker); export abstract class GatewayError extends Error { private readonly [symbol] = true; // used in isInstance abstract readonly name: string; abstract readonly type: string; readonly statusCode: number; readonly cause?: unknown; constructor({ message, statusCode = 500, cause, }: { message: string; statusCode?: number; cause?: unknown; }) { super(message); this.statusCode = statusCode; this.cause = cause; } /** * Checks if the given error is a Gateway Error. * @param {unknown} error - The error to check. * @returns {boolean} True if the error is a Gateway Error, false otherwise. */ static isInstance(error: unknown): error is GatewayError { return GatewayError.hasMarker(error); } static hasMarker(error: unknown): error is GatewayError { return ( typeof error === 'object' && error !== null && symbol in error && (error as any)[symbol] === true ); } } --- File: /ai/packages/gateway/src/errors/gateway-internal-server-error.ts --- import { GatewayError } from './gateway-error'; const name = 'GatewayInternalServerError'; const marker = `vercel.ai.gateway.error.${name}`; const symbol = Symbol.for(marker); /** * Internal server error from the Gateway */ export class GatewayInternalServerError extends GatewayError { private readonly [symbol] = true; // used in isInstance readonly name = name; readonly type = 'internal_server_error'; constructor({ message = 'Internal server error', statusCode = 500, cause, }: { message?: string; statusCode?: number; cause?: unknown; } = {}) { super({ message, statusCode, cause }); } static isInstance(error: unknown): error is GatewayInternalServerError { return GatewayError.hasMarker(error) && symbol in error; } } --- File: /ai/packages/gateway/src/errors/gateway-invalid-request-error.ts --- import { GatewayError } from './gateway-error'; const name = 'GatewayInvalidRequestError'; const marker = `vercel.ai.gateway.error.${name}`; const symbol = Symbol.for(marker); /** * Invalid request - missing headers, malformed data, etc. */ export class GatewayInvalidRequestError extends GatewayError { private readonly [symbol] = true; // used in isInstance readonly name = name; readonly type = 'invalid_request_error'; constructor({ message = 'Invalid request', statusCode = 400, cause, }: { message?: string; statusCode?: number; cause?: unknown; } = {}) { super({ message, statusCode, cause }); } static isInstance(error: unknown): error is GatewayInvalidRequestError { return GatewayError.hasMarker(error) && symbol in error; } } --- File: /ai/packages/gateway/src/errors/gateway-model-not-found-error.ts --- import { z } from 'zod/v4'; import { GatewayError } from './gateway-error'; const name = 'GatewayModelNotFoundError'; const marker = `vercel.ai.gateway.error.${name}`; const symbol = Symbol.for(marker); export const modelNotFoundParamSchema = z.object({ modelId: z.string(), }); /** * Model not found or not available */ export class GatewayModelNotFoundError extends GatewayError { private readonly [symbol] = true; // used in isInstance readonly name = name; readonly type = 'model_not_found'; readonly modelId?: string; constructor({ message = 'Model not found', statusCode = 404, modelId, cause, }: { message?: string; statusCode?: number; modelId?: string; cause?: unknown; } = {}) { super({ message, statusCode, cause }); this.modelId = modelId; } static isInstance(error: unknown): error is GatewayModelNotFoundError { return GatewayError.hasMarker(error) && symbol in error; } } --- File: /ai/packages/gateway/src/errors/gateway-rate-limit-error.ts --- import { GatewayError } from './gateway-error'; const name = 'GatewayRateLimitError'; const marker = `vercel.ai.gateway.error.${name}`; const symbol = Symbol.for(marker); /** * Rate limit exceeded. */ export class GatewayRateLimitError extends GatewayError { private readonly [symbol] = true; // used in isInstance readonly name = name; readonly type = 'rate_limit_exceeded'; constructor({ message = 'Rate limit exceeded', statusCode = 429, cause, }: { message?: string; statusCode?: number; cause?: unknown; } = {}) { super({ message, statusCode, cause }); } static isInstance(error: unknown): error is GatewayRateLimitError { return GatewayError.hasMarker(error) && symbol in error; } } --- File: /ai/packages/gateway/src/errors/gateway-response-error.ts --- import { GatewayError } from './gateway-error'; import type { ZodError } from 'zod/v4'; const name = 'GatewayResponseError'; const marker = `vercel.ai.gateway.error.${name}`; const symbol = Symbol.for(marker); /** * Gateway response parsing error */ export class GatewayResponseError extends GatewayError { private readonly [symbol] = true; // used in isInstance readonly name = name; readonly type = 'response_error'; readonly response?: unknown; readonly validationError?: ZodError; constructor({ message = 'Invalid response from Gateway', statusCode = 502, response, validationError, cause, }: { message?: string; statusCode?: number; response?: unknown; validationError?: ZodError; cause?: unknown; } = {}) { super({ message, statusCode, cause }); this.response = response; this.validationError = validationError; } static isInstance(error: unknown): error is GatewayResponseError { return GatewayError.hasMarker(error) && symbol in error; } } --- File: /ai/packages/gateway/src/errors/index.ts --- export { asGatewayError } from './as-gateway-error'; export { createGatewayErrorFromResponse, type GatewayErrorResponse, } from './create-gateway-error'; export { extractApiCallResponse } from './extract-api-call-response'; export { GatewayError } from './gateway-error'; export { GatewayAuthenticationError } from './gateway-authentication-error'; export { GatewayInternalServerError } from './gateway-internal-server-error'; export { GatewayInvalidRequestError } from './gateway-invalid-request-error'; export { GatewayModelNotFoundError, modelNotFoundParamSchema, } from './gateway-model-not-found-error'; export { GatewayRateLimitError } from './gateway-rate-limit-error'; export { GatewayResponseError } from './gateway-response-error'; --- File: /ai/packages/gateway/src/errors/parse-auth-method.test.ts --- import { describe, expect, it } from 'vitest'; import { GATEWAY_AUTH_METHOD_HEADER, parseAuthMethod, } from './parse-auth-method'; describe('GATEWAY_AUTH_METHOD_HEADER', () => { it('should export the correct header name', () => { expect(GATEWAY_AUTH_METHOD_HEADER).toBe('ai-gateway-auth-method'); }); }); describe('parseAuthMethod', () => { describe('valid authentication methods', () => { it('should parse "api-key" auth method', () => { const headers = { [GATEWAY_AUTH_METHOD_HEADER]: 'api-key', }; const result = parseAuthMethod(headers); expect(result).toBe('api-key'); }); it('should parse "oidc" auth method', () => { const headers = { [GATEWAY_AUTH_METHOD_HEADER]: 'oidc', }; const result = parseAuthMethod(headers); expect(result).toBe('oidc'); }); it('should handle headers with other fields present', () => { const headers = { authorization: 'Bearer token', 'content-type': 'application/json', [GATEWAY_AUTH_METHOD_HEADER]: 'api-key', 'user-agent': 'test-agent', }; const result = parseAuthMethod(headers); expect(result).toBe('api-key'); }); }); describe('invalid authentication methods', () => { it('should return undefined for invalid auth method string', () => { const headers = { [GATEWAY_AUTH_METHOD_HEADER]: 'invalid-method', }; expect(parseAuthMethod(headers)).toBeUndefined(); }); it('should return undefined for empty string', () => { const headers = { [GATEWAY_AUTH_METHOD_HEADER]: '', }; expect(parseAuthMethod(headers)).toBeUndefined(); }); it('should return undefined for numeric value', () => { const headers = { [GATEWAY_AUTH_METHOD_HEADER]: '123', }; expect(parseAuthMethod(headers)).toBeUndefined(); }); it('should return undefined for boolean-like strings', () => { const headers = { [GATEWAY_AUTH_METHOD_HEADER]: 'true', }; expect(parseAuthMethod(headers)).toBeUndefined(); }); it('should return undefined for case-sensitive variations', () => { const headers = { [GATEWAY_AUTH_METHOD_HEADER]: 'API-KEY', }; expect(parseAuthMethod(headers)).toBeUndefined(); }); it('should return undefined for OIDC case variations', () => { const headers = { [GATEWAY_AUTH_METHOD_HEADER]: 'OIDC', }; expect(parseAuthMethod(headers)).toBeUndefined(); }); }); describe('missing or undefined headers', () => { it('should return undefined when header is missing', () => { const headers = { authorization: 'Bearer token', }; expect(parseAuthMethod(headers)).toBeUndefined(); }); it('should return undefined when header is undefined', () => { const headers = { [GATEWAY_AUTH_METHOD_HEADER]: undefined, }; expect(parseAuthMethod(headers)).toBeUndefined(); }); it('should return undefined when headers object is empty', () => { const headers = {}; expect(parseAuthMethod(headers)).toBeUndefined(); }); }); describe('edge cases', () => { it('should return undefined for whitespace-only strings', () => { const headers = { [GATEWAY_AUTH_METHOD_HEADER]: ' ', }; expect(parseAuthMethod(headers)).toBeUndefined(); }); it('should return undefined for auth methods with extra whitespace', () => { const headers = { [GATEWAY_AUTH_METHOD_HEADER]: ' api-key ', }; expect(parseAuthMethod(headers)).toBeUndefined(); }); it('should handle null values', () => { const headers = { [GATEWAY_AUTH_METHOD_HEADER]: null as any, }; expect(parseAuthMethod(headers)).toBeUndefined(); }); }); }); --- File: /ai/packages/gateway/src/errors/parse-auth-method.ts --- import { z } from 'zod/v4'; export const GATEWAY_AUTH_METHOD_HEADER = 'ai-gateway-auth-method' as const; export function parseAuthMethod(headers: Record<string, string | undefined>) { const result = gatewayAuthMethodSchema.safeParse( headers[GATEWAY_AUTH_METHOD_HEADER], ); return result.success ? result.data : undefined; } const gatewayAuthMethodSchema = z.union([ z.literal('api-key'), z.literal('oidc'), ]); --- File: /ai/packages/gateway/src/gateway-config.ts --- import type { FetchFunction, Resolvable } from '@ai-sdk/provider-utils'; export type GatewayConfig = { baseURL: string; headers: () => Resolvable<Record<string, string | undefined>>; fetch?: FetchFunction; }; --- File: /ai/packages/gateway/src/gateway-embedding-model-settings.ts --- export type GatewayEmbeddingModelId = | 'amazon/titan-embed-text-v2' | 'cohere/embed-v4.0' | 'google/gemini-embedding-001' | 'google/text-embedding-005' | 'google/text-multilingual-embedding-002' | 'mistral/codestral-embed' | 'mistral/mistral-embed' | 'openai/text-embedding-3-large' | 'openai/text-embedding-3-small' | 'openai/text-embedding-ada-002' | (string & {}); --- File: /ai/packages/gateway/src/gateway-embedding-model.test.ts --- import { describe, it, expect } from 'vitest'; import { createTestServer } from '@ai-sdk/provider-utils/test'; import { GatewayEmbeddingModel } from './gateway-embedding-model'; import type { GatewayConfig } from './gateway-config'; import { GatewayInvalidRequestError, GatewayInternalServerError, } from './errors'; const dummyEmbeddings = [ [0.1, 0.2, 0.3], [0.4, 0.5, 0.6], ]; const testValues = ['sunny day at the beach', 'rainy afternoon in the city']; const server = createTestServer({ 'https://api.test.com/embedding-model': {}, }); const createTestModel = ( config: Partial< GatewayConfig & { o11yHeaders?: Record<string, string> } > = {}, ) => new GatewayEmbeddingModel('openai/text-embedding-3-small', { provider: 'gateway', baseURL: 'https://api.test.com', headers: () => ({ Authorization: 'Bearer test-token', 'ai-gateway-auth-method': 'api-key', }), fetch: globalThis.fetch, o11yHeaders: config.o11yHeaders || {}, ...config, }); describe('GatewayEmbeddingModel', () => { function prepareJsonResponse({ embeddings = dummyEmbeddings, usage = { tokens: 8 }, headers, }: { embeddings?: number[][]; usage?: { tokens: number }; headers?: Record<string, string>; } = {}) { server.urls['https://api.test.com/embedding-model'].response = { type: 'json-value', headers, body: { embeddings, usage, }, }; } describe('doEmbed', () => { it('should pass headers correctly', async () => { prepareJsonResponse(); await createTestModel().doEmbed({ values: testValues, headers: { 'Custom-Header': 'test-value' }, }); const headers = server.calls[0].requestHeaders; expect(headers).toMatchObject({ authorization: 'Bearer test-token', 'custom-header': 'test-value', 'ai-embedding-model-specification-version': '2', 'ai-model-id': 'openai/text-embedding-3-small', }); }); it('should include o11y headers', async () => { prepareJsonResponse(); const o11yHeaders = { 'ai-o11y-deployment-id': 'deployment-1', 'ai-o11y-environment': 'production', 'ai-o11y-region': 'iad1', } as const; await createTestModel({ o11yHeaders }).doEmbed({ values: testValues }); const headers = server.calls[0].requestHeaders; expect(headers).toMatchObject(o11yHeaders); }); it('should extract embeddings and usage', async () => { prepareJsonResponse({ embeddings: dummyEmbeddings, usage: { tokens: 42 }, }); const { embeddings, usage } = await createTestModel().doEmbed({ values: testValues, }); expect(embeddings).toStrictEqual(dummyEmbeddings); expect(usage).toStrictEqual({ tokens: 42 }); }); it('should send single value as string, multiple values as array', async () => { prepareJsonResponse(); await createTestModel().doEmbed({ values: testValues }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ input: testValues, }); await createTestModel().doEmbed({ values: [testValues[0]] }); expect(await server.calls[1].requestBodyJson).toStrictEqual({ input: testValues[0], }); }); it('should pass providerOptions into request body', async () => { prepareJsonResponse(); await createTestModel().doEmbed({ values: testValues, providerOptions: { openai: { dimensions: 64 } }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ input: testValues, openai: { dimensions: 64 }, }); }); it('should convert gateway error responses', async () => { server.urls['https://api.test.com/embedding-model'].response = { type: 'error', status: 400, body: JSON.stringify({ error: { message: 'Invalid input', type: 'invalid_request_error', }, }), }; await expect( createTestModel().doEmbed({ values: testValues }), ).rejects.toSatisfy( err => GatewayInvalidRequestError.isInstance(err) && err.statusCode === 400, ); server.urls['https://api.test.com/embedding-model'].response = { type: 'error', status: 500, body: JSON.stringify({ error: { message: 'Server blew up', type: 'internal_server_error', }, }), }; await expect( createTestModel().doEmbed({ values: testValues }), ).rejects.toSatisfy( err => GatewayInternalServerError.isInstance(err) && err.statusCode === 500, ); }); it('should include providerMetadata in response body', async () => { server.urls['https://api.test.com/embedding-model'].response = { type: 'json-value', body: { embeddings: dummyEmbeddings, usage: { tokens: 5 }, providerMetadata: { gateway: { routing: { test: true } } }, }, }; const { response } = await createTestModel().doEmbed({ values: testValues, }); expect(response?.body).toMatchObject({ providerMetadata: { gateway: { routing: { test: true } } }, }); }); it('should extract providerMetadata to top level', async () => { server.urls['https://api.test.com/embedding-model'].response = { type: 'json-value', body: { embeddings: dummyEmbeddings, usage: { tokens: 5 }, providerMetadata: { gateway: { routing: { test: true } } }, }, }; const result = await createTestModel().doEmbed({ values: testValues, }); expect(result.providerMetadata).toStrictEqual({ gateway: { routing: { test: true } }, }); }); }); }); --- File: /ai/packages/gateway/src/gateway-embedding-model.ts --- import type { EmbeddingModelV2 } from '@ai-sdk/provider'; import { combineHeaders, createJsonResponseHandler, createJsonErrorResponseHandler, postJsonToApi, resolve, type Resolvable, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import type { GatewayConfig } from './gateway-config'; import { asGatewayError } from './errors'; import { parseAuthMethod } from './errors/parse-auth-method'; import type { SharedV2ProviderMetadata } from '@ai-sdk/provider'; export class GatewayEmbeddingModel implements EmbeddingModelV2<string> { readonly specificationVersion = 'v2'; readonly maxEmbeddingsPerCall = 2048; readonly supportsParallelCalls = true; constructor( readonly modelId: string, private readonly config: GatewayConfig & { provider: string; o11yHeaders: Resolvable<Record<string, string>>; }, ) {} get provider(): string { return this.config.provider; } async doEmbed({ values, headers, abortSignal, providerOptions, }: Parameters<EmbeddingModelV2<string>['doEmbed']>[0]): Promise< Awaited<ReturnType<EmbeddingModelV2<string>['doEmbed']>> > { const resolvedHeaders = await resolve(this.config.headers()); try { const { responseHeaders, value: responseBody, rawValue, } = await postJsonToApi({ url: this.getUrl(), headers: combineHeaders( resolvedHeaders, headers ?? {}, this.getModelConfigHeaders(), await resolve(this.config.o11yHeaders), ), body: { input: values.length === 1 ? values[0] : values, ...(providerOptions ?? {}), }, successfulResponseHandler: createJsonResponseHandler( gatewayEmbeddingResponseSchema, ), failedResponseHandler: createJsonErrorResponseHandler({ errorSchema: z.any(), errorToMessage: data => data, }), ...(abortSignal && { abortSignal }), fetch: this.config.fetch, }); return { embeddings: responseBody.embeddings, usage: responseBody.usage ?? undefined, providerMetadata: responseBody.providerMetadata as unknown as SharedV2ProviderMetadata, response: { headers: responseHeaders, body: rawValue }, }; } catch (error) { throw asGatewayError(error, parseAuthMethod(resolvedHeaders)); } } private getUrl() { return `${this.config.baseURL}/embedding-model`; } private getModelConfigHeaders() { return { 'ai-embedding-model-specification-version': '2', 'ai-model-id': this.modelId, }; } } const gatewayEmbeddingResponseSchema = z.object({ embeddings: z.array(z.array(z.number())), usage: z.object({ tokens: z.number() }).nullish(), providerMetadata: z .record(z.string(), z.record(z.string(), z.unknown())) .optional(), }); --- File: /ai/packages/gateway/src/gateway-fetch-metadata.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { describe, expect, it, vi } from 'vitest'; import { GatewayFetchMetadata } from './gateway-fetch-metadata'; import type { FetchFunction } from '@ai-sdk/provider-utils'; import { GatewayAuthenticationError, GatewayInternalServerError, GatewayRateLimitError, GatewayResponseError, GatewayError, } from './errors'; function createBasicMetadataFetcher({ headers, fetch, }: { headers?: () => Record<string, string>; fetch?: FetchFunction; } = {}) { return new GatewayFetchMetadata({ baseURL: 'https://api.example.com', headers: headers ?? (() => ({ Authorization: 'Bearer test-token' })), fetch, }); } describe('GatewayFetchMetadata', () => { const mockModelEntry = { id: 'model-1', name: 'Model One', description: 'A test model', pricing: { input: '0.000001', output: '0.000002', }, specification: { specificationVersion: 'v2' as const, provider: 'test-provider', modelId: 'model-1', }, }; const mockModelEntryWithoutPricing = { id: 'model-2', name: 'Model Two', specification: { specificationVersion: 'v2' as const, provider: 'test-provider', modelId: 'model-2', }, }; const server = createTestServer({ 'https://api.example.com/*': { response: { type: 'json-value', body: { models: [mockModelEntry], }, }, }, }); describe('getAvailableModels', () => { it('should fetch available models from the correct endpoint', async () => { const metadata = createBasicMetadataFetcher(); const result = await metadata.getAvailableModels(); expect(server.calls[0].requestMethod).toBe('GET'); expect(server.calls[0].requestUrl).toBe('https://api.example.com/config'); expect(result).toEqual({ models: [mockModelEntry], }); }); it('should handle models with pricing information', async () => { server.urls['https://api.example.com/*'].response = { type: 'json-value', body: { models: [mockModelEntry], }, }; const metadata = createBasicMetadataFetcher(); const result = await metadata.getAvailableModels(); expect(result.models[0]).toEqual(mockModelEntry); expect(result.models[0].pricing).toEqual({ input: '0.000001', output: '0.000002', }); }); it('should handle models without pricing information', async () => { server.urls['https://api.example.com/*'].response = { type: 'json-value', body: { models: [mockModelEntryWithoutPricing], }, }; const metadata = createBasicMetadataFetcher(); const result = await metadata.getAvailableModels(); expect(result.models[0]).toEqual(mockModelEntryWithoutPricing); expect(result.models[0].pricing).toBeUndefined(); }); it('should handle mixed models with and without pricing', async () => { server.urls['https://api.example.com/*'].response = { type: 'json-value', body: { models: [mockModelEntry, mockModelEntryWithoutPricing], }, }; const metadata = createBasicMetadataFetcher(); const result = await metadata.getAvailableModels(); expect(result.models).toHaveLength(2); expect(result.models[0].pricing).toEqual({ input: '0.000001', output: '0.000002', }); expect(result.models[1].pricing).toBeUndefined(); }); it('should handle models with description', async () => { const modelWithDescription = { ...mockModelEntry, description: 'A powerful language model', }; server.urls['https://api.example.com/*'].response = { type: 'json-value', body: { models: [modelWithDescription], }, }; const metadata = createBasicMetadataFetcher(); const result = await metadata.getAvailableModels(); expect(result.models[0].description).toBe('A powerful language model'); }); it('should pass headers correctly', async () => { const metadata = createBasicMetadataFetcher({ headers: () => ({ Authorization: 'Bearer custom-token', 'Custom-Header': 'custom-value', }), }); await metadata.getAvailableModels(); expect(server.calls[0].requestHeaders).toEqual({ authorization: 'Bearer custom-token', 'custom-header': 'custom-value', }); }); it('should handle API errors', async () => { server.urls['https://api.example.com/*'].response = { type: 'error', status: 401, body: JSON.stringify({ error: { message: 'Unauthorized', type: 'authentication_error', }, }), }; const metadata = createBasicMetadataFetcher(); try { await metadata.getAvailableModels(); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayAuthenticationError.isInstance(error)).toBe(true); const authError = error as GatewayAuthenticationError; expect(authError.message).toContain('No authentication provided'); expect(authError.type).toBe('authentication_error'); expect(authError.statusCode).toBe(401); } }); it('should convert API call errors to Gateway errors', async () => { server.urls['https://api.example.com/*'].response = { type: 'error', status: 403, body: JSON.stringify({ error: { message: 'Forbidden access', type: 'authentication_error', }, }), }; const metadata = createBasicMetadataFetcher(); try { await metadata.getAvailableModels(); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayAuthenticationError.isInstance(error)).toBe(true); const authError = error as GatewayAuthenticationError; expect(authError.message).toContain('No authentication provided'); expect(authError.type).toBe('authentication_error'); expect(authError.statusCode).toBe(403); } }); it('should handle malformed JSON error responses', async () => { server.urls['https://api.example.com/*'].response = { type: 'error', status: 500, body: '{ invalid json', }; const metadata = createBasicMetadataFetcher(); try { await metadata.getAvailableModels(); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayResponseError.isInstance(error)).toBe(true); const responseError = error as GatewayResponseError; expect(responseError.statusCode).toBe(500); expect(responseError.type).toBe('response_error'); } }); it('should handle malformed response data', async () => { server.urls['https://api.example.com/*'].response = { type: 'json-value', body: { invalid: 'response', }, }; const metadata = createBasicMetadataFetcher(); await expect(metadata.getAvailableModels()).rejects.toThrow(); }); it('should reject models with invalid pricing format', async () => { server.urls['https://api.example.com/*'].response = { type: 'json-value', body: { models: [ { id: 'model-1', name: 'Model One', pricing: { input: 123, // Should be string, not number output: '0.000002', }, specification: { specificationVersion: 'v2', provider: 'test-provider', modelId: 'model-1', }, }, ], }, }; const metadata = createBasicMetadataFetcher(); await expect(metadata.getAvailableModels()).rejects.toThrow(); }); it('should not double-wrap existing Gateway errors', async () => { // Create a Gateway error and verify it doesn't get wrapped const existingError = new GatewayAuthenticationError({ message: 'Already wrapped', statusCode: 401, }); // Test the catch block logic directly try { throw existingError; } catch (error: unknown) { if (GatewayError.isInstance(error)) { expect(error).toBe(existingError); // Should be the same instance expect(error.message).toBe('Already wrapped'); return; } throw new Error('Should not reach here'); } }); it('should handle various server error types', async () => { // Test rate limit error server.urls['https://api.example.com/*'].response = { type: 'error', status: 429, body: JSON.stringify({ error: { message: 'Rate limit exceeded', type: 'rate_limit_exceeded', }, }), }; const metadata = createBasicMetadataFetcher(); try { await metadata.getAvailableModels(); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayRateLimitError.isInstance(error)).toBe(true); const rateLimitError = error as GatewayRateLimitError; expect(rateLimitError.message).toBe('Rate limit exceeded'); expect(rateLimitError.type).toBe('rate_limit_exceeded'); expect(rateLimitError.statusCode).toBe(429); } }); it('should handle internal server errors', async () => { server.urls['https://api.example.com/*'].response = { type: 'error', status: 500, body: JSON.stringify({ error: { message: 'Database connection failed', type: 'internal_server_error', }, }), }; const metadata = createBasicMetadataFetcher(); try { await metadata.getAvailableModels(); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayInternalServerError.isInstance(error)).toBe(true); const serverError = error as GatewayInternalServerError; expect(serverError.message).toBe('Database connection failed'); expect(serverError.type).toBe('internal_server_error'); expect(serverError.statusCode).toBe(500); } }); it('should preserve error cause chain', async () => { server.urls['https://api.example.com/*'].response = { type: 'error', status: 401, body: JSON.stringify({ error: { message: 'Token expired', type: 'authentication_error', }, }), }; const metadata = createBasicMetadataFetcher(); try { await metadata.getAvailableModels(); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayAuthenticationError.isInstance(error)).toBe(true); const authError = error as GatewayAuthenticationError; expect(authError.cause).toBeDefined(); } }); it('should use custom fetch function when provided', async () => { const customModelEntry = { id: 'custom-model-1', name: 'Custom Model One', description: 'Custom model description', pricing: { input: '0.000005', output: '0.000010', }, specification: { specificationVersion: 'v2' as const, provider: 'custom-provider', modelId: 'custom-model-1', }, }; const mockFetch = vi.fn().mockResolvedValue( new Response( JSON.stringify({ models: [customModelEntry], }), { status: 200, headers: { 'Content-Type': 'application/json', }, }, ), ); const metadata = createBasicMetadataFetcher({ fetch: mockFetch, }); const result = await metadata.getAvailableModels(); expect(mockFetch).toHaveBeenCalled(); expect(result).toEqual({ models: [customModelEntry], }); }); it('should handle empty response', async () => { server.urls['https://api.example.com/*'].response = { type: 'json-value', body: { models: [], }, }; const metadata = createBasicMetadataFetcher(); const result = await metadata.getAvailableModels(); expect(result).toEqual({ models: [], }); }); }); }); --- File: /ai/packages/gateway/src/gateway-fetch-metadata.ts --- import { createJsonErrorResponseHandler, createJsonResponseHandler, getFromApi, resolve, } from '@ai-sdk/provider-utils'; import { asGatewayError } from './errors'; import type { GatewayConfig } from './gateway-config'; import type { GatewayLanguageModelEntry } from './gateway-model-entry'; import { z } from 'zod/v4'; type GatewayFetchMetadataConfig = GatewayConfig; export interface GatewayFetchMetadataResponse { models: GatewayLanguageModelEntry[]; } export class GatewayFetchMetadata { constructor(private readonly config: GatewayFetchMetadataConfig) {} async getAvailableModels(): Promise<GatewayFetchMetadataResponse> { try { const { value } = await getFromApi({ url: `${this.config.baseURL}/config`, headers: await resolve(this.config.headers()), successfulResponseHandler: createJsonResponseHandler( gatewayFetchMetadataSchema, ), failedResponseHandler: createJsonErrorResponseHandler({ errorSchema: z.any(), errorToMessage: data => data, }), fetch: this.config.fetch, }); return value; } catch (error) { throw asGatewayError(error); } } } const gatewayLanguageModelSpecificationSchema = z.object({ specificationVersion: z.literal('v2'), provider: z.string(), modelId: z.string(), }); const gatewayLanguageModelPricingSchema = z.object({ input: z.string(), output: z.string(), }); const gatewayLanguageModelEntrySchema = z.object({ id: z.string(), name: z.string(), description: z.string().nullish(), pricing: gatewayLanguageModelPricingSchema.nullish(), specification: gatewayLanguageModelSpecificationSchema, }); const gatewayFetchMetadataSchema = z.object({ models: z.array(gatewayLanguageModelEntrySchema), }); --- File: /ai/packages/gateway/src/gateway-language-model-settings.ts --- export type GatewayModelId = | 'alibaba/qwen-3-14b' | 'alibaba/qwen-3-235b' | 'alibaba/qwen-3-30b' | 'alibaba/qwen-3-32b' | 'alibaba/qwen3-coder' | 'amazon/nova-lite' | 'amazon/nova-micro' | 'amazon/nova-pro' | 'anthropic/claude-3-haiku' | 'anthropic/claude-3-opus' | 'anthropic/claude-3.5-haiku' | 'anthropic/claude-3.5-sonnet' | 'anthropic/claude-3.7-sonnet' | 'anthropic/claude-4-opus' | 'anthropic/claude-4-sonnet' | 'anthropic/claude-4.1-opus' | 'cohere/command-a' | 'cohere/command-r' | 'cohere/command-r-plus' | 'deepseek/deepseek-r1' | 'deepseek/deepseek-r1-distill-llama-70b' | 'deepseek/deepseek-v3' | 'google/gemini-2.0-flash' | 'google/gemini-2.0-flash-lite' | 'google/gemini-2.5-flash' | 'google/gemini-2.5-pro' | 'google/gemma-2-9b' | 'inception/mercury-coder-small' | 'meta/llama-3-70b' | 'meta/llama-3-8b' | 'meta/llama-3.1-70b' | 'meta/llama-3.1-8b' | 'meta/llama-3.2-11b' | 'meta/llama-3.2-1b' | 'meta/llama-3.2-3b' | 'meta/llama-3.2-90b' | 'meta/llama-3.3-70b' | 'meta/llama-4-maverick' | 'meta/llama-4-scout' | 'mistral/codestral' | 'mistral/devstral-small' | 'mistral/magistral-medium' | 'mistral/magistral-small' | 'mistral/ministral-3b' | 'mistral/ministral-8b' | 'mistral/mistral-large' | 'mistral/mistral-saba-24b' | 'mistral/mistral-small' | 'mistral/mixtral-8x22b-instruct' | 'mistral/pixtral-12b' | 'mistral/pixtral-large' | 'moonshotai/kimi-k2' | 'morph/morph-v3-fast' | 'morph/morph-v3-large' | 'openai/gpt-3.5-turbo' | 'openai/gpt-3.5-turbo-instruct' | 'openai/gpt-4-turbo' | 'openai/gpt-4.1' | 'openai/gpt-4.1-mini' | 'openai/gpt-4.1-nano' | 'openai/gpt-4o' | 'openai/gpt-4o-mini' | 'openai/gpt-oss-120b' | 'openai/gpt-oss-20b' | 'openai/o1' | 'openai/o3' | 'openai/o3-mini' | 'openai/o4-mini' | 'perplexity/sonar' | 'perplexity/sonar-pro' | 'perplexity/sonar-reasoning' | 'perplexity/sonar-reasoning-pro' | 'vercel/v0-1.0-md' | 'vercel/v0-1.5-md' | 'xai/grok-2' | 'xai/grok-2-vision' | 'xai/grok-3' | 'xai/grok-3-fast' | 'xai/grok-3-mini' | 'xai/grok-3-mini-fast' | 'xai/grok-4' | 'zai/glm-4.5' | 'zai/glm-4.5-air' | (string & {}); --- File: /ai/packages/gateway/src/gateway-language-model.test.ts --- import type { LanguageModelV2Prompt, LanguageModelV2FilePart, } from '@ai-sdk/provider'; import { convertReadableStreamToArray, createTestServer, } from '@ai-sdk/provider-utils/test'; import { GatewayLanguageModel } from './gateway-language-model'; import type { GatewayConfig } from './gateway-config'; import { GatewayAuthenticationError, GatewayRateLimitError, GatewayInternalServerError, GatewayInvalidRequestError, GatewayModelNotFoundError, GatewayResponseError, } from './errors'; const TEST_PROMPT: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; const createTestModel = ( config: Partial< GatewayConfig & { o11yHeaders?: Record<string, string> } > = {}, ) => { return new GatewayLanguageModel('test-model', { provider: 'test-provider', baseURL: 'https://api.test.com', headers: () => ({ Authorization: 'Bearer test-token', 'ai-gateway-auth-method': 'api-key', }), fetch: globalThis.fetch, o11yHeaders: config.o11yHeaders || {}, ...config, }); }; describe('GatewayLanguageModel', () => { const server = createTestServer({ 'https://api.test.com/language-model': {}, }); describe('constructor', () => { it('should set basic properties', () => { const model = createTestModel(); expect(model.modelId).toBe('test-model'); expect(model.provider).toBe('test-provider'); expect(model.specificationVersion).toBe('v2'); }); }); describe('doGenerate', () => { function prepareJsonResponse({ content = { type: 'text', text: '' }, usage = { prompt_tokens: 4, completion_tokens: 30, }, finish_reason = 'stop', id = 'test-id', created = 1711115037, model = 'test-model', } = {}) { server.urls['https://api.test.com/language-model'].response = { type: 'json-value', body: { id, created, model, content, finish_reason, usage, }, }; } it('should pass headers correctly', async () => { prepareJsonResponse({ content: { type: 'text', text: 'Hello, World!' } }); await createTestModel().doGenerate({ prompt: TEST_PROMPT, headers: { 'Custom-Header': 'test-value', }, }); const headers = server.calls[0].requestHeaders; expect(headers).toMatchObject({ authorization: 'Bearer test-token', 'custom-header': 'test-value', 'ai-language-model-specification-version': '2', 'ai-language-model-id': 'test-model', 'ai-language-model-streaming': 'false', }); }); it('should extract text response', async () => { prepareJsonResponse({ content: { type: 'text', text: 'Hello, World!' } }); const { content } = await createTestModel().doGenerate({ prompt: TEST_PROMPT, }); expect(content).toEqual({ type: 'text', text: 'Hello, World!' }); }); it('should extract usage information', async () => { prepareJsonResponse({ content: { type: 'text', text: 'Test' }, usage: { prompt_tokens: 10, completion_tokens: 20, }, }); const { usage } = await createTestModel().doGenerate({ prompt: TEST_PROMPT, }); expect(usage).toEqual({ prompt_tokens: 10, completion_tokens: 20, }); }); it('should remove abortSignal from the request body', async () => { prepareJsonResponse({ content: { type: 'text', text: 'Test response' } }); const controller = new AbortController(); const signal = controller.signal; await createTestModel().doGenerate({ prompt: TEST_PROMPT, abortSignal: signal, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody).not.toHaveProperty('abortSignal'); }); it('should pass abortSignal to fetch when provided', async () => { prepareJsonResponse({ content: { type: 'text', text: 'Test response' } }); const mockFetch = vi.fn().mockImplementation(globalThis.fetch); const controller = new AbortController(); const signal = controller.signal; await createTestModel({ fetch: mockFetch, }).doGenerate({ prompt: TEST_PROMPT, abortSignal: signal, }); expect(mockFetch).toHaveBeenCalled(); const fetchCallArgs = mockFetch.mock.calls[0]; expect(fetchCallArgs[1].signal).toBe(signal); }); it('should not pass abortSignal to fetch when not provided', async () => { prepareJsonResponse({ content: { type: 'text', text: 'Test response' } }); const mockFetch = vi.fn().mockImplementation(globalThis.fetch); await createTestModel({ fetch: mockFetch, }).doGenerate({ prompt: TEST_PROMPT, }); expect(mockFetch).toHaveBeenCalled(); const fetchCallArgs = mockFetch.mock.calls[0]; expect(fetchCallArgs[1].signal).toBeUndefined(); }); it('should include o11y headers in the request', async () => { prepareJsonResponse({ content: { type: 'text', text: 'Hello, World!' } }); const o11yHeaders = { 'ai-o11y-deployment-id': 'test-deployment', 'ai-o11y-environment': 'production', 'ai-o11y-region': 'iad1', }; await createTestModel({ o11yHeaders }).doGenerate({ prompt: TEST_PROMPT, }); const headers = server.calls[0].requestHeaders; expect(headers).toMatchObject(o11yHeaders); }); it('should convert API call errors to Gateway errors', async () => { server.urls['https://api.test.com/language-model'].response = { type: 'error', status: 401, body: JSON.stringify({ error: { message: 'Invalid API key provided', type: 'authentication_error', }, }), }; const model = createTestModel(); try { await model.doGenerate({ prompt: TEST_PROMPT }); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayAuthenticationError.isInstance(error)).toBe(true); const authError = error as GatewayAuthenticationError; expect(authError.message).toContain('Invalid API key provided'); expect(authError.statusCode).toBe(401); expect(authError.type).toBe('authentication_error'); } }); it('should handle malformed error responses', async () => { server.urls['https://api.test.com/language-model'].response = { type: 'error', status: 500, body: 'Not JSON', }; const model = createTestModel(); try { await model.doGenerate({ prompt: TEST_PROMPT }); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayResponseError.isInstance(error)).toBe(true); const responseError = error as GatewayResponseError; expect(responseError.statusCode).toBe(500); expect(responseError.type).toBe('response_error'); } }); it('should handle rate limit errors', async () => { server.urls['https://api.test.com/language-model'].response = { type: 'error', status: 429, body: JSON.stringify({ error: { message: 'Rate limit exceeded. Try again later.', type: 'rate_limit_exceeded', }, }), }; const model = createTestModel(); try { await model.doGenerate({ prompt: TEST_PROMPT }); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayRateLimitError.isInstance(error)).toBe(true); const rateLimitError = error as GatewayRateLimitError; expect(rateLimitError.message).toBe( 'Rate limit exceeded. Try again later.', ); expect(rateLimitError.statusCode).toBe(429); expect(rateLimitError.type).toBe('rate_limit_exceeded'); } }); it('should handle invalid request errors', async () => { server.urls['https://api.test.com/language-model'].response = { type: 'error', status: 400, body: JSON.stringify({ error: { message: 'Invalid prompt format', type: 'invalid_request_error', }, }), }; const model = createTestModel(); try { await model.doGenerate({ prompt: TEST_PROMPT }); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayInvalidRequestError.isInstance(error)).toBe(true); const invalidError = error as GatewayInvalidRequestError; expect(invalidError.message).toBe('Invalid prompt format'); expect(invalidError.statusCode).toBe(400); expect(invalidError.type).toBe('invalid_request_error'); } }); describe('Image part encoding', () => { it('should not modify prompt without image parts', async () => { prepareJsonResponse({ content: { type: 'text', text: 'response' } }); await createTestModel().doGenerate({ prompt: TEST_PROMPT, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.prompt).toEqual(TEST_PROMPT); }); it('should encode Uint8Array image part to base64 data URL with default mime type', async () => { prepareJsonResponse({ content: { type: 'text', text: 'response' } }); const imageBytes = new Uint8Array([1, 2, 3, 4]); const expectedBase64 = Buffer.from(imageBytes).toString('base64'); const imagePrompt: LanguageModelV2Prompt = [ { role: 'user', content: [ { type: 'text', text: 'Describe this image:' }, { type: 'file', data: imageBytes, mediaType: 'image/jpeg' }, ], }, ]; await createTestModel().doGenerate({ prompt: imagePrompt, }); const requestBody = await server.calls[0].requestBodyJson; const imagePart = requestBody.prompt[0] .content[1] as LanguageModelV2FilePart; expect(imagePart.type).toBe('file'); expect(imagePart.data).toBe(`data:image/jpeg;base64,${expectedBase64}`); expect(imagePart.mediaType).toBe('image/jpeg'); }); it('should encode Uint8Array image part to base64 data URL with specified mime type', async () => { prepareJsonResponse({ content: { type: 'text', text: 'response' } }); const imageBytes = new Uint8Array([5, 6, 7, 8]); const expectedBase64 = Buffer.from(imageBytes).toString('base64'); const mimeType = 'image/png'; const imagePrompt: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'file', data: imageBytes, mediaType: mimeType }], }, ]; await createTestModel().doGenerate({ prompt: imagePrompt, }); const requestBody = await server.calls[0].requestBodyJson; const imagePart = requestBody.prompt[0] .content[0] as LanguageModelV2FilePart; expect(imagePart.type).toBe('file'); expect(imagePart.data).toBe( `data:${mimeType};base64,${expectedBase64}`, ); expect(imagePart.mediaType).toBe(mimeType); }); it('should not modify image part with URL', async () => { prepareJsonResponse({ content: { type: 'text', text: 'response' } }); const imageUrl = new URL('https://example.com/image.jpg'); const imagePrompt: LanguageModelV2Prompt = [ { role: 'user', content: [ { type: 'text', text: 'Image URL:' }, { type: 'file', data: imageUrl, mediaType: 'image/jpeg' }, ], }, ]; await createTestModel().doGenerate({ prompt: imagePrompt, }); const requestBody = await server.calls[0].requestBodyJson; const imagePart = requestBody.prompt[0] .content[1] as LanguageModelV2FilePart; expect(imagePart.type).toBe('file'); expect(imagePart.data).toBe(imageUrl.toString()); }); it('should handle mixed content types correctly', async () => { prepareJsonResponse({ content: { type: 'text', text: 'response' } }); const imageBytes = new Uint8Array([1, 2, 3, 4]); const expectedBase64 = Buffer.from(imageBytes).toString('base64'); const imageUrl = new URL('https://example.com/image2.png'); const imagePrompt: LanguageModelV2Prompt = [ { role: 'user', content: [ { type: 'text', text: 'First text.' }, { type: 'file', data: imageBytes, mediaType: 'image/gif' }, { type: 'text', text: 'Second text.' }, { type: 'file', data: imageUrl, mediaType: 'image/png' }, ], }, ]; await createTestModel().doGenerate({ prompt: imagePrompt, }); const requestBody = await server.calls[0].requestBodyJson; const content = requestBody.prompt[0].content; expect(content[0]).toEqual({ type: 'text', text: 'First text.' }); expect(content[1]).toEqual({ type: 'file', data: `data:image/gif;base64,${expectedBase64}`, mediaType: 'image/gif', }); expect(content[2]).toEqual({ type: 'text', text: 'Second text.' }); expect(content[3]).toEqual({ type: 'file', data: imageUrl.toString(), mediaType: 'image/png', }); }); }); it('should handle various error types with proper conversion', async () => { const model = createTestModel(); server.urls['https://api.test.com/language-model'].response = { type: 'error', status: 400, body: JSON.stringify({ error: { message: 'Invalid request format', type: 'invalid_request_error', }, }), }; try { await model.doGenerate({ prompt: TEST_PROMPT }); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayInvalidRequestError.isInstance(error)).toBe(true); const invalidError = error as GatewayInvalidRequestError; expect(invalidError.message).toBe('Invalid request format'); expect(invalidError.statusCode).toBe(400); expect(invalidError.type).toBe('invalid_request_error'); } // Test model not found error server.urls['https://api.test.com/language-model'].response = { type: 'error', status: 404, body: JSON.stringify({ error: { message: 'Model xyz not found', type: 'model_not_found', param: { modelId: 'xyz' }, }, }), }; try { await model.doGenerate({ prompt: TEST_PROMPT }); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayModelNotFoundError.isInstance(error)).toBe(true); const modelError = error as GatewayModelNotFoundError; expect(modelError.message).toBe('Model xyz not found'); expect(modelError.statusCode).toBe(404); expect(modelError.type).toBe('model_not_found'); expect(modelError.modelId).toBe('xyz'); } // Test internal server error server.urls['https://api.test.com/language-model'].response = { type: 'error', status: 500, body: JSON.stringify({ error: { message: 'Database connection failed', type: 'internal_server_error', }, }), }; try { await model.doGenerate({ prompt: TEST_PROMPT }); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayInternalServerError.isInstance(error)).toBe(true); const serverError = error as GatewayInternalServerError; expect(serverError.message).toBe('Database connection failed'); expect(serverError.statusCode).toBe(500); expect(serverError.type).toBe('internal_server_error'); } }); describe('Gateway error handling for malformed responses', () => { it('should include actual response body when APICallError has no data', async () => { const malformedResponse = { ferror: { message: 'Model not found', type: 'model_not_found' }, }; // Mock the server to return malformed response that can't be parsed by AI SDK server.urls['https://api.test.com/language-model'].response = { type: 'error', status: 404, body: JSON.stringify(malformedResponse), }; const model = createTestModel(); try { await model.doGenerate({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'test' }] }, ], }); expect.fail('Expected error to be thrown'); } catch (error) { expect(GatewayResponseError.isInstance(error)).toBe(true); const gatewayError = error as GatewayResponseError; expect(gatewayError.response).toEqual(malformedResponse); expect(gatewayError.validationError).toBeDefined(); } }); it('should use raw response body when JSON parsing fails', async () => { const invalidJson = 'invalid json response'; // Mock the server to return invalid JSON server.urls['https://api.test.com/language-model'].response = { type: 'error', status: 500, body: invalidJson, }; const model = createTestModel(); try { await model.doGenerate({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'test' }] }, ], }); expect.fail('Expected error to be thrown'); } catch (error) { expect(GatewayResponseError.isInstance(error)).toBe(true); const gatewayError = error as GatewayResponseError; expect(gatewayError.response).toBe(invalidJson); expect(gatewayError.validationError).toBeDefined(); } }); }); }); describe('doStream', () => { function prepareStreamResponse({ content, finish_reason = 'stop', }: { content: string[]; finish_reason?: string; }) { server.urls['https://api.test.com/language-model'].response = { type: 'stream-chunks', chunks: [ ...content.map( text => `data: ${JSON.stringify({ type: 'text-delta', textDelta: text, })}\n\n`, ), `data: ${JSON.stringify({ type: 'finish', finishReason: finish_reason, usage: { prompt_tokens: 10, completion_tokens: 20, }, })}\n\n`, ], }; } it('should stream text deltas', async () => { prepareStreamResponse({ content: ['Hello', ', ', 'World!'], }); const { stream } = await createTestModel().doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toEqual([ { type: 'text-delta', textDelta: 'Hello' }, { type: 'text-delta', textDelta: ', ' }, { type: 'text-delta', textDelta: 'World!' }, { type: 'finish', finishReason: 'stop', usage: { prompt_tokens: 10, completion_tokens: 20, }, }, ]); }); it('should pass streaming headers', async () => { prepareStreamResponse({ content: ['Test'], }); await createTestModel().doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const headers = server.calls[0].requestHeaders; expect(headers).toMatchObject({ 'ai-language-model-specification-version': '2', 'ai-language-model-id': 'test-model', 'ai-language-model-streaming': 'true', }); }); it('should remove abortSignal from the streaming request body', async () => { prepareStreamResponse({ content: ['Test content'], }); const controller = new AbortController(); const signal = controller.signal; await createTestModel().doStream({ prompt: TEST_PROMPT, abortSignal: signal, includeRawChunks: false, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody).not.toHaveProperty('abortSignal'); }); it('should pass abortSignal to fetch when provided for streaming', async () => { prepareStreamResponse({ content: ['Test content'], }); const mockFetch = vi.fn().mockImplementation(globalThis.fetch); const controller = new AbortController(); const signal = controller.signal; await createTestModel({ fetch: mockFetch, }).doStream({ prompt: TEST_PROMPT, abortSignal: signal, includeRawChunks: false, }); expect(mockFetch).toHaveBeenCalled(); const fetchCallArgs = mockFetch.mock.calls[0]; expect(fetchCallArgs[1].signal).toBe(signal); }); it('should not pass abortSignal to fetch when not provided for streaming', async () => { prepareStreamResponse({ content: ['Test content'], }); const mockFetch = vi.fn().mockImplementation(globalThis.fetch); await createTestModel({ fetch: mockFetch, }).doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(mockFetch).toHaveBeenCalled(); const fetchCallArgs = mockFetch.mock.calls[0]; expect(fetchCallArgs[1].signal).toBeUndefined(); }); it('should include o11y headers in the streaming request', async () => { prepareStreamResponse({ content: ['Test content'], }); const o11yHeaders = { 'ai-o11y-deployment-id': 'test-deployment', 'ai-o11y-environment': 'production', 'ai-o11y-region': 'iad1', }; await createTestModel({ o11yHeaders }).doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const headers = server.calls[0].requestHeaders; expect(headers).toMatchObject(o11yHeaders); }); it('should convert API call errors to Gateway errors in streaming', async () => { server.urls['https://api.test.com/language-model'].response = { type: 'error', status: 429, body: JSON.stringify({ error: { message: 'Rate limit exceeded', type: 'rate_limit_exceeded', }, }), }; const model = createTestModel(); try { await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false }); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayRateLimitError.isInstance(error)).toBe(true); const rateLimitError = error as GatewayRateLimitError; expect(rateLimitError.message).toBe('Rate limit exceeded'); expect(rateLimitError.statusCode).toBe(429); expect(rateLimitError.type).toBe('rate_limit_exceeded'); } }); it('should handle authentication errors in streaming', async () => { server.urls['https://api.test.com/language-model'].response = { type: 'error', status: 401, body: JSON.stringify({ error: { message: 'Authentication failed for streaming', type: 'authentication_error', }, }), }; const model = createTestModel(); try { await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false }); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayAuthenticationError.isInstance(error)).toBe(true); const authError = error as GatewayAuthenticationError; expect(authError.message).toContain('Invalid API key provided'); expect(authError.statusCode).toBe(401); expect(authError.type).toBe('authentication_error'); } }); it('should handle invalid request errors in streaming', async () => { server.urls['https://api.test.com/language-model'].response = { type: 'error', status: 400, body: JSON.stringify({ error: { message: 'Invalid streaming request', type: 'invalid_request_error', }, }), }; const model = createTestModel(); try { await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false }); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayInvalidRequestError.isInstance(error)).toBe(true); const invalidError = error as GatewayInvalidRequestError; expect(invalidError.message).toBe('Invalid streaming request'); expect(invalidError.statusCode).toBe(400); expect(invalidError.type).toBe('invalid_request_error'); } }); it('should handle malformed error responses in streaming', async () => { server.urls['https://api.test.com/language-model'].response = { type: 'error', status: 500, body: 'Invalid JSON for streaming', }; const model = createTestModel(); try { await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false }); expect.fail('Should have thrown an error'); } catch (error) { expect(GatewayResponseError.isInstance(error)).toBe(true); const responseError = error as GatewayResponseError; expect(responseError.statusCode).toBe(500); expect(responseError.type).toBe('response_error'); } }); describe('Image part encoding', () => { it('should not modify prompt without image parts', async () => { prepareStreamResponse({ content: ['response'] }); await createTestModel().doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.prompt).toEqual(TEST_PROMPT); }); it('should encode Uint8Array image part to base64 data URL with default mime type', async () => { prepareStreamResponse({ content: ['response'] }); const imageBytes = new Uint8Array([1, 2, 3, 4]); const expectedBase64 = Buffer.from(imageBytes).toString('base64'); const imagePrompt: LanguageModelV2Prompt = [ { role: 'user', content: [ { type: 'text', text: 'Describe:' }, { type: 'file', data: imageBytes, mediaType: 'image/jpeg' }, ], }, ]; await createTestModel().doStream({ prompt: imagePrompt, includeRawChunks: false, }); const requestBody = await server.calls[0].requestBodyJson; const imagePart = requestBody.prompt[0] .content[1] as LanguageModelV2FilePart; expect(imagePart.type).toBe('file'); expect(imagePart.data).toBe(`data:image/jpeg;base64,${expectedBase64}`); expect(imagePart.mediaType).toBe('image/jpeg'); }); it('should encode Uint8Array image part to base64 data URL with specified mime type', async () => { prepareStreamResponse({ content: ['response'] }); const imageBytes = new Uint8Array([5, 6, 7, 8]); const expectedBase64 = Buffer.from(imageBytes).toString('base64'); const mimeType = 'image/png'; const imagePrompt: LanguageModelV2Prompt = [ { role: 'user', content: [ { type: 'text', text: 'Describe:' }, { type: 'file', data: imageBytes, mediaType: mimeType }, ], }, ]; await createTestModel().doStream({ prompt: imagePrompt, includeRawChunks: false, }); const requestBody = await server.calls[0].requestBodyJson; const imagePart = requestBody.prompt[0] .content[1] as LanguageModelV2FilePart; expect(imagePart.type).toBe('file'); expect(imagePart.data).toBe( `data:${mimeType};base64,${expectedBase64}`, ); expect(imagePart.mediaType).toBe(mimeType); }); it('should not modify image part with URL', async () => { prepareStreamResponse({ content: ['response'] }); const imageUrl = new URL('https://example.com/image.jpg'); const imagePrompt: LanguageModelV2Prompt = [ { role: 'user', content: [ { type: 'text', text: 'URL:' }, { type: 'file', data: imageUrl, mediaType: 'image/jpeg' }, ], }, ]; await createTestModel().doStream({ prompt: imagePrompt, includeRawChunks: false, }); const requestBody = await server.calls[0].requestBodyJson; const imagePart = requestBody.prompt[0] .content[1] as LanguageModelV2FilePart; expect(imagePart.type).toBe('file'); expect(imagePart.data).toBe(imageUrl.toString()); expect(imagePart.mediaType).toBe('image/jpeg'); }); it('should handle mixed content types correctly for streaming', async () => { prepareStreamResponse({ content: ['response'] }); const imageBytes = new Uint8Array([1, 2, 3, 4]); const expectedBase64 = Buffer.from(imageBytes).toString('base64'); const imageUrl = new URL('https://example.com/image2.png'); const imagePrompt: LanguageModelV2Prompt = [ { role: 'user', content: [ { type: 'text', text: 'First text.' }, { type: 'file', data: imageBytes, mediaType: 'image/gif' }, { type: 'text', text: 'Second text.' }, { type: 'file', data: imageUrl, mediaType: 'image/png' }, ], }, ]; await createTestModel().doStream({ prompt: imagePrompt, includeRawChunks: false, }); const requestBody = await server.calls[0].requestBodyJson; const content = requestBody.prompt[0].content; expect(content[0]).toEqual({ type: 'text', text: 'First text.' }); expect(content[1]).toEqual({ type: 'file', data: `data:image/gif;base64,${expectedBase64}`, mediaType: 'image/gif', }); expect(content[2]).toEqual({ type: 'text', text: 'Second text.' }); expect(content[3]).toEqual({ type: 'file', data: imageUrl.toString(), mediaType: 'image/png', }); }); }); describe('Error handling', () => { it('should not double-wrap existing Gateway errors', async () => { // Mock fetch to throw a Gateway error directly const existingGatewayError = new GatewayAuthenticationError({ message: 'Already a Gateway error', statusCode: 401, }); const mockFetch = vi.fn().mockRejectedValue(existingGatewayError); const model = createTestModel({ fetch: mockFetch }); try { await model.doGenerate({ prompt: TEST_PROMPT }); expect.fail('Should have thrown an error'); } catch (error: unknown) { // Should be the same instance, not wrapped expect(error).toBe(existingGatewayError); expect((error as GatewayAuthenticationError).message).toBe( 'Already a Gateway error', ); } }); it('should handle network errors gracefully', async () => { // Mock fetch to throw a network error const networkError = new Error('Network connection failed'); const mockFetch = vi.fn().mockRejectedValue(networkError); const model = createTestModel({ fetch: mockFetch }); try { await model.doGenerate({ prompt: TEST_PROMPT }); expect.fail('Should have thrown an error'); } catch (error: unknown) { expect(GatewayResponseError.isInstance(error)).toBe(true); const responseError = error as GatewayResponseError; expect(responseError.message).toBe( 'Invalid error response format: Gateway request failed: Network connection failed', ); expect(responseError.cause).toBe(networkError); } }); it('should handle network errors gracefully in streaming', async () => { // Mock fetch to throw a network error during streaming const networkError = new Error('Network connection failed'); const mockFetch = vi.fn().mockRejectedValue(networkError); const model = createTestModel({ fetch: mockFetch }); try { await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect.fail('Should have thrown an error'); } catch (error: unknown) { expect(GatewayResponseError.isInstance(error)).toBe(true); const responseError = error as GatewayResponseError; expect(responseError.message).toBe( 'Invalid error response format: Gateway request failed: Network connection failed', ); expect(responseError.cause).toBe(networkError); } }); it('should preserve error cause chain', async () => { server.urls['https://api.test.com/language-model'].response = { type: 'error', status: 401, body: JSON.stringify({ error: { message: 'Token expired', type: 'authentication_error', }, }), }; const model = createTestModel(); try { await model.doGenerate({ prompt: TEST_PROMPT }); expect.fail('Should have thrown an error'); } catch (error: unknown) { expect(GatewayAuthenticationError.isInstance(error)).toBe(true); const authError = error as GatewayAuthenticationError; expect(authError.cause).toBeDefined(); } }); }); }); describe('raw chunks filtering', () => { it('should filter raw chunks based on includeRawChunks option', async () => { server.urls['https://api.test.com/language-model'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"stream-start","warnings":[]}\n\n`, `data: {"type":"raw","rawValue":{"id":"test-chunk","object":"chat.completion.chunk","choices":[{"delta":{"content":"Hello"}}]}}\n\n`, `data: {"type":"text-delta","textDelta":"Hello"}\n\n`, `data: {"type":"raw","rawValue":{"id":"test-chunk-2","object":"chat.completion.chunk","choices":[{"delta":{"content":" world"}}]}}\n\n`, `data: {"type":"text-delta","textDelta":" world"}\n\n`, `data: {"type":"finish","finishReason":"stop","usage":{"prompt_tokens":10,"completion_tokens":5}}\n\n`, ], }; const { stream } = await createTestModel().doStream({ prompt: TEST_PROMPT, includeRawChunks: false, // Raw chunks should be filtered out }); const chunks = await convertReadableStreamToArray(stream); expect(chunks).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "textDelta": "Hello", "type": "text-delta", }, { "textDelta": " world", "type": "text-delta", }, { "finishReason": "stop", "type": "finish", "usage": { "completion_tokens": 5, "prompt_tokens": 10, }, }, ] `); }); it('should include raw chunks when includeRawChunks is true', async () => { server.urls['https://api.test.com/language-model'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"stream-start","warnings":[]}\n\n`, `data: {"type":"raw","rawValue":{"id":"test-chunk","object":"chat.completion.chunk","choices":[{"delta":{"content":"Hello"}}]}}\n\n`, `data: {"type":"text-delta","textDelta":"Hello"}\n\n`, `data: {"type":"finish","finishReason":"stop","usage":{"prompt_tokens":10,"completion_tokens":5}}\n\n`, ], }; const { stream } = await createTestModel().doStream({ prompt: TEST_PROMPT, includeRawChunks: true, // Raw chunks should be included }); const chunks = await convertReadableStreamToArray(stream); expect(chunks).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "rawValue": { "choices": [ { "delta": { "content": "Hello", }, }, ], "id": "test-chunk", "object": "chat.completion.chunk", }, "type": "raw", }, { "textDelta": "Hello", "type": "text-delta", }, { "finishReason": "stop", "type": "finish", "usage": { "completion_tokens": 5, "prompt_tokens": 10, }, }, ] `); }); }); describe('timestamp conversion', () => { it('should convert timestamp strings to Date objects in response-metadata chunks', async () => { const timestampString = '2023-12-07T10:30:00.000Z'; server.urls['https://api.test.com/language-model'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"stream-start","warnings":[]}\n\n`, `data: {"type":"response-metadata","id":"test-id","modelId":"test-model","timestamp":"${timestampString}"}\n\n`, `data: {"type":"text-delta","textDelta":"Hello"}\n\n`, `data: {"type":"finish","finishReason":"stop","usage":{"prompt_tokens":10,"completion_tokens":5}}\n\n`, ], }; const { stream } = await createTestModel().doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks).toHaveLength(4); expect(chunks[0]).toEqual({ type: 'stream-start', warnings: [], }); // Check that the response-metadata chunk has timestamp converted to Date const responseMetadataChunk = chunks[1] as any; expect(responseMetadataChunk).toMatchObject({ type: 'response-metadata', id: 'test-id', modelId: 'test-model', }); expect(responseMetadataChunk.timestamp).toBeInstanceOf(Date); expect(responseMetadataChunk.timestamp.toISOString()).toBe( timestampString, ); }); it('should not modify timestamp if it is already a Date object', async () => { const timestampDate = new Date('2023-12-07T10:30:00.000Z'); // Use standard stream-chunks format with Date serialized as string, then manually parse server.urls['https://api.test.com/language-model'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"stream-start","warnings":[]}\n\n`, `data: {"type":"response-metadata","id":"test-id","modelId":"test-model","timestamp":"${timestampDate.toISOString()}"}\n\n`, `data: {"type":"text-delta","textDelta":"Hello"}\n\n`, `data: {"type":"finish","finishReason":"stop","usage":{"prompt_tokens":10,"completion_tokens":5}}\n\n`, ], }; const { stream } = await createTestModel().doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks).toHaveLength(4); // Check that the response-metadata chunk timestamp is converted to Date const responseMetadataChunk = chunks[1] as any; expect(responseMetadataChunk).toMatchObject({ type: 'response-metadata', id: 'test-id', modelId: 'test-model', }); expect(responseMetadataChunk.timestamp).toBeInstanceOf(Date); }); it('should not modify response-metadata chunks without timestamp', async () => { server.urls['https://api.test.com/language-model'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"stream-start","warnings":[]}\n\n`, `data: {"type":"response-metadata","id":"test-id","modelId":"test-model"}\n\n`, `data: {"type":"text-delta","textDelta":"Hello"}\n\n`, `data: {"type":"finish","finishReason":"stop","usage":{"prompt_tokens":10,"completion_tokens":5}}\n\n`, ], }; const { stream } = await createTestModel().doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks).toHaveLength(4); // Check that the response-metadata chunk without timestamp is unchanged const responseMetadataChunk = chunks[1] as any; expect(responseMetadataChunk).toEqual({ type: 'response-metadata', id: 'test-id', modelId: 'test-model', }); expect(responseMetadataChunk.timestamp).toBeUndefined(); }); it('should handle null timestamp values gracefully', async () => { server.urls['https://api.test.com/language-model'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"stream-start","warnings":[]}\n\n`, `data: {"type":"response-metadata","id":"test-id","modelId":"test-model","timestamp":null}\n\n`, `data: {"type":"text-delta","textDelta":"Hello"}\n\n`, `data: {"type":"finish","finishReason":"stop","usage":{"prompt_tokens":10,"completion_tokens":5}}\n\n`, ], }; const { stream } = await createTestModel().doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks).toHaveLength(4); // Check that null timestamp is left as null const responseMetadataChunk = chunks[1] as any; expect(responseMetadataChunk).toEqual({ type: 'response-metadata', id: 'test-id', modelId: 'test-model', timestamp: null, }); }); it('should only convert timestamps for response-metadata chunks, not other chunk types', async () => { const timestampString = '2023-12-07T10:30:00.000Z'; server.urls['https://api.test.com/language-model'].response = { type: 'stream-chunks', chunks: [ `data: {"type":"stream-start","warnings":[]}\n\n`, `data: {"type":"text-delta","textDelta":"Hello","timestamp":"${timestampString}"}\n\n`, `data: {"type":"finish","finishReason":"stop","usage":{"prompt_tokens":10,"completion_tokens":5},"timestamp":"${timestampString}"}\n\n`, ], }; const { stream } = await createTestModel().doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks).toHaveLength(3); // Check that timestamps in non-response-metadata chunks are left as strings // Note: These chunks don't typically have timestamp properties in the real types, // but this test verifies our conversion logic only affects response-metadata chunks const textDeltaChunk = chunks[1] as any; expect(textDeltaChunk).toEqual({ type: 'text-delta', textDelta: 'Hello', timestamp: timestampString, // Should remain a string }); const finishChunk = chunks[2] as any; expect(finishChunk).toEqual({ type: 'finish', finishReason: 'stop', usage: { prompt_tokens: 10, completion_tokens: 5 }, timestamp: timestampString, // Should remain a string }); }); }); describe('Provider Options', () => { function prepareJsonResponse({ content = { type: 'text', text: '' }, usage = { prompt_tokens: 4, completion_tokens: 30, }, finish_reason = 'stop', id = 'test-id', created = 1711115037, model = 'test-model', } = {}) { server.urls['https://api.test.com/language-model'].response = { type: 'json-value', body: { id, created, model, content, finish_reason, usage, }, }; } function prepareStreamResponse({ content, finish_reason = 'stop', }: { content: string[]; finish_reason?: string; }) { server.urls['https://api.test.com/language-model'].response = { type: 'stream-chunks', chunks: [ ...content.map( text => `data: ${JSON.stringify({ type: 'text-delta', textDelta: text, })}\n\n`, ), `data: ${JSON.stringify({ type: 'finish', finishReason: finish_reason, usage: { prompt_tokens: 10, completion_tokens: 20, }, })}\n\n`, ], }; } it('should pass provider routing order for doGenerate', async () => { prepareJsonResponse({ content: { type: 'text', text: 'Test response' }, }); await createTestModel().doGenerate({ prompt: TEST_PROMPT, providerOptions: { gateway: { order: ['bedrock', 'anthropic'], }, }, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.providerOptions).toEqual({ gateway: { order: ['bedrock', 'anthropic'] }, }); }); it('should pass single provider in order array', async () => { prepareJsonResponse({ content: { type: 'text', text: 'Test response' }, }); await createTestModel().doGenerate({ prompt: TEST_PROMPT, providerOptions: { gateway: { order: ['openai'], }, }, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.providerOptions).toEqual({ gateway: { order: ['openai'] }, }); }); it('should work without provider options', async () => { prepareJsonResponse({ content: { type: 'text', text: 'Test response' }, }); const result = await createTestModel().doGenerate({ prompt: TEST_PROMPT, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.providerOptions).toBeUndefined(); expect(result.content).toEqual({ type: 'text', text: 'Test response', }); }); it('should pass provider routing order for doStream', async () => { prepareStreamResponse({ content: ['Hello', ' world'], }); const { stream } = await createTestModel().doStream({ prompt: TEST_PROMPT, providerOptions: { gateway: { order: ['groq', 'openai'], }, }, }); await convertReadableStreamToArray(stream); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.providerOptions).toEqual({ gateway: { order: ['groq', 'openai'] }, }); }); it('should validate provider options against schema', async () => { prepareJsonResponse({ content: { type: 'text', text: 'Test response' }, }); await createTestModel().doGenerate({ prompt: TEST_PROMPT, providerOptions: { gateway: { order: ['anthropic', 'bedrock', 'openai'], }, }, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.providerOptions).toEqual({ gateway: { order: ['anthropic', 'bedrock', 'openai'] }, }); }); }); }); --- File: /ai/packages/gateway/src/gateway-language-model.ts --- import type { LanguageModelV2, LanguageModelV2CallOptions, LanguageModelV2CallWarning, LanguageModelV2FilePart, LanguageModelV2StreamPart, } from '@ai-sdk/provider'; import { combineHeaders, createEventSourceResponseHandler, createJsonErrorResponseHandler, createJsonResponseHandler, postJsonToApi, resolve, type ParseResult, type Resolvable, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import type { GatewayConfig } from './gateway-config'; import type { GatewayModelId } from './gateway-language-model-settings'; import { asGatewayError } from './errors'; import { parseAuthMethod } from './errors/parse-auth-method'; type GatewayChatConfig = GatewayConfig & { provider: string; o11yHeaders: Resolvable<Record<string, string>>; }; export class GatewayLanguageModel implements LanguageModelV2 { readonly specificationVersion = 'v2'; readonly supportedUrls = { '*/*': [/.*/] }; constructor( readonly modelId: GatewayModelId, private readonly config: GatewayChatConfig, ) {} get provider(): string { return this.config.provider; } private async getArgs(options: Parameters<LanguageModelV2['doGenerate']>[0]) { const { abortSignal: _abortSignal, ...optionsWithoutSignal } = options; return { args: this.maybeEncodeFileParts(optionsWithoutSignal), warnings: [], }; } async doGenerate( options: Parameters<LanguageModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>> { const { args, warnings } = await this.getArgs(options); const { abortSignal } = options; const resolvedHeaders = await resolve(this.config.headers()); try { const { responseHeaders, value: responseBody, rawValue: rawResponse, } = await postJsonToApi({ url: this.getUrl(), headers: combineHeaders( resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, false), await resolve(this.config.o11yHeaders), ), body: args, successfulResponseHandler: createJsonResponseHandler(z.any()), failedResponseHandler: createJsonErrorResponseHandler({ errorSchema: z.any(), errorToMessage: data => data, }), ...(abortSignal && { abortSignal }), fetch: this.config.fetch, }); return { ...responseBody, request: { body: args }, response: { headers: responseHeaders, body: rawResponse }, warnings, }; } catch (error) { throw asGatewayError(error, parseAuthMethod(resolvedHeaders)); } } async doStream( options: Parameters<LanguageModelV2['doStream']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>> { const { args, warnings } = await this.getArgs(options); const { abortSignal } = options; const resolvedHeaders = await resolve(this.config.headers()); try { const { value: response, responseHeaders } = await postJsonToApi({ url: this.getUrl(), headers: combineHeaders( resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, true), await resolve(this.config.o11yHeaders), ), body: args, successfulResponseHandler: createEventSourceResponseHandler(z.any()), failedResponseHandler: createJsonErrorResponseHandler({ errorSchema: z.any(), errorToMessage: data => data, }), ...(abortSignal && { abortSignal }), fetch: this.config.fetch, }); return { stream: response.pipeThrough( new TransformStream< ParseResult<LanguageModelV2StreamPart>, LanguageModelV2StreamPart >({ start(controller) { if (warnings.length > 0) { controller.enqueue({ type: 'stream-start', warnings }); } }, transform(chunk, controller) { if (chunk.success) { const streamPart = chunk.value; // Handle raw chunks: if this is a raw chunk from the gateway API, // only emit it if includeRawChunks is true if (streamPart.type === 'raw' && !options.includeRawChunks) { return; // Skip raw chunks if not requested } if ( streamPart.type === 'response-metadata' && streamPart.timestamp && typeof streamPart.timestamp === 'string' ) { streamPart.timestamp = new Date(streamPart.timestamp); } controller.enqueue(streamPart); } else { controller.error( (chunk as { success: false; error: unknown }).error, ); } }, }), ), request: { body: args }, response: { headers: responseHeaders }, }; } catch (error) { throw asGatewayError(error, parseAuthMethod(resolvedHeaders)); } } private isFilePart(part: unknown) { return ( part && typeof part === 'object' && 'type' in part && part.type === 'file' ); } /** * Encodes file parts in the prompt to base64. Mutates the passed options * instance directly to avoid copying the file data. * @param options - The options to encode. * @returns The options with the file parts encoded. */ private maybeEncodeFileParts(options: LanguageModelV2CallOptions) { for (const message of options.prompt) { for (const part of message.content) { if (this.isFilePart(part)) { const filePart = part as LanguageModelV2FilePart; // If the file part is a URL it will get cleanly converted to a string. // If it's a binary file attachment we convert it to a data url. // In either case, server-side we should only ever see URLs as strings. if (filePart.data instanceof Uint8Array) { const buffer = Uint8Array.from(filePart.data); const base64Data = Buffer.from(buffer).toString('base64'); filePart.data = new URL( `data:${filePart.mediaType || 'application/octet-stream'};base64,${base64Data}`, ); } } } } return options; } private getUrl() { return `${this.config.baseURL}/language-model`; } private getModelConfigHeaders(modelId: string, streaming: boolean) { return { 'ai-language-model-specification-version': '2', 'ai-language-model-id': modelId, 'ai-language-model-streaming': String(streaming), }; } } --- File: /ai/packages/gateway/src/gateway-model-entry.ts --- import type { LanguageModelV2 } from '@ai-sdk/provider'; export interface GatewayLanguageModelEntry { /** * The model id used by the remote provider in model settings and for specifying the * intended model for text generation. */ id: string; /** * The display name of the model for presentation in user-facing contexts. */ name: string; /** * Optional description of the model. */ description?: string | null; /** * Optional pricing information for the model. */ pricing?: { /** * Cost per input token in USD. */ input: string; /** * Cost per output token in USD. */ output: string; } | null; /** * Additional AI SDK language model specifications for the model. */ specification: GatewayLanguageModelSpecification; } export type GatewayLanguageModelSpecification = Pick< LanguageModelV2, 'specificationVersion' | 'provider' | 'modelId' >; --- File: /ai/packages/gateway/src/gateway-provider-options.ts --- import { z } from 'zod/v4'; // https://vercel.com/docs/ai-gateway/provider-options export const gatewayProviderOptions = z.object({ /** * Array of provider slugs that specifies the sequence in which providers should be attempted. * * Example: `['bedrock', 'anthropic']` will try Amazon Bedrock first, then Anthropic as fallback. */ order: z.array(z.string()).optional(), }); export type GatewayProviderOptions = z.infer<typeof gatewayProviderOptions>; --- File: /ai/packages/gateway/src/gateway-provider.test.ts --- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { gateway, createGatewayProvider, getGatewayAuthToken, } from './gateway-provider'; import { GatewayFetchMetadata } from './gateway-fetch-metadata'; import { NoSuchModelError } from '@ai-sdk/provider'; import { GatewayEmbeddingModel } from './gateway-embedding-model'; import { getVercelOidcToken, getVercelRequestId } from './vercel-environment'; import { resolve } from '@ai-sdk/provider-utils'; import { GatewayLanguageModel } from './gateway-language-model'; import { GatewayAuthenticationError, GatewayInternalServerError, } from './errors'; import { fail } from 'node:assert'; vi.mock('./gateway-language-model', () => ({ GatewayLanguageModel: vi.fn(), })); // Mock the gateway fetch metadata to prevent actual network calls // We'll create a more flexible mock that can simulate auth failures const mockGetAvailableModels = vi.fn(); vi.mock('./gateway-fetch-metadata', () => ({ GatewayFetchMetadata: vi.fn().mockImplementation((config: any) => ({ getAvailableModels: async () => { // Call the headers function to trigger authentication logic if (config.headers && typeof config.headers === 'function') { await config.headers(); } return mockGetAvailableModels(); }, })), })); vi.mock('./vercel-environment', () => ({ getVercelOidcToken: vi.fn(), getVercelRequestId: vi.fn(), })); describe('GatewayProvider', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(getVercelOidcToken).mockResolvedValue('mock-oidc-token'); vi.mocked(getVercelRequestId).mockResolvedValue('mock-request-id'); // Set up default mock behavior for getAvailableModels mockGetAvailableModels.mockReturnValue({ models: [] }); if ('AI_GATEWAY_API_KEY' in process.env) { Reflect.deleteProperty(process.env, 'AI_GATEWAY_API_KEY'); } }); describe('createGatewayProvider', () => { it('should create provider with correct configuration', async () => { const options = { baseURL: 'https://api.example.com', apiKey: 'test-api-key', headers: { 'Custom-Header': 'value' }, }; const provider = createGatewayProvider(options); provider('test-model'); expect(GatewayLanguageModel).toHaveBeenCalledWith( 'test-model', expect.objectContaining({ provider: 'gateway', baseURL: 'https://api.example.com', headers: expect.any(Function), fetch: undefined, }), ); // Verify headers function const constructorCall = vi.mocked(GatewayLanguageModel).mock.calls[0]; const config = constructorCall[1]; const headers = await config.headers(); expect(headers).toEqual({ Authorization: 'Bearer test-api-key', 'Custom-Header': 'value', 'ai-gateway-protocol-version': expect.any(String), 'ai-gateway-auth-method': 'api-key', }); }); it('should use OIDC token when no API key is provided', async () => { const options = { baseURL: 'https://api.example.com', headers: { 'Custom-Header': 'value' }, }; const provider = createGatewayProvider(options); provider('test-model'); const constructorCall = vi.mocked(GatewayLanguageModel).mock.calls[0]; const config = constructorCall[1]; const headers = await config.headers(); expect(headers).toEqual({ Authorization: 'Bearer mock-oidc-token', 'Custom-Header': 'value', 'ai-gateway-protocol-version': expect.any(String), 'ai-gateway-auth-method': 'oidc', }); }); it('should throw error when instantiated with new keyword', () => { const provider = createGatewayProvider({ baseURL: 'https://api.example.com', }); expect(() => { new (provider as unknown as { (modelId: string): unknown; new (modelId: string): never; })('test-model'); }).toThrow( 'The Gateway Provider model function cannot be called with the new keyword.', ); }); it('should create GatewayEmbeddingModel for textEmbeddingModel', () => { const provider = createGatewayProvider({ baseURL: 'https://api.example.com', }); const model = provider.textEmbeddingModel( 'openai/text-embedding-3-small', ); expect(model).toBeInstanceOf(GatewayEmbeddingModel); }); it('should fetch available models', async () => { mockGetAvailableModels.mockReturnValue({ models: [] }); const options = { baseURL: 'https://api.example.com', apiKey: 'test-api-key', }; const provider = createGatewayProvider(options); await provider.getAvailableModels(); expect(GatewayFetchMetadata).toHaveBeenCalledWith( expect.objectContaining({ baseURL: 'https://api.example.com', }), ); expect(mockGetAvailableModels).toHaveBeenCalled(); }); describe('metadata caching', () => { it('should cache metadata for the specified refresh interval', async () => { mockGetAvailableModels.mockReturnValue({ models: [{ id: 'test-model', specification: {} }], }); let currentTime = new Date('2024-01-01T00:00:00Z').getTime(); const provider = createGatewayProvider({ baseURL: 'https://api.example.com', metadataCacheRefreshMillis: 10000, // 10 seconds _internal: { currentDate: () => new Date(currentTime), }, }); // First call should fetch metadata await provider.getAvailableModels(); expect(mockGetAvailableModels).toHaveBeenCalledTimes(1); // Second immediate call should use cache await provider.getAvailableModels(); expect(mockGetAvailableModels).toHaveBeenCalledTimes(1); // Advance time by 9 seconds (should still use cache) currentTime += 9000; await provider.getAvailableModels(); expect(mockGetAvailableModels).toHaveBeenCalledTimes(1); // Advance time past 10 seconds (should refresh) currentTime += 2000; await provider.getAvailableModels(); expect(mockGetAvailableModels).toHaveBeenCalledTimes(2); }); it('should use default 5 minute refresh interval when not specified', async () => { mockGetAvailableModels.mockReturnValue({ models: [{ id: 'test-model', specification: {} }], }); let currentTime = new Date('2024-01-01T00:00:00Z').getTime(); const provider = createGatewayProvider({ baseURL: 'https://api.example.com', _internal: { currentDate: () => new Date(currentTime), }, }); // First call should fetch metadata await provider.getAvailableModels(); expect(mockGetAvailableModels).toHaveBeenCalledTimes(1); // Advance time by 4 minutes (should still use cache) currentTime += 4 * 60 * 1000; await provider.getAvailableModels(); expect(mockGetAvailableModels).toHaveBeenCalledTimes(1); // Advance time past 5 minutes (should refresh) currentTime += 2 * 60 * 1000; await provider.getAvailableModels(); expect(mockGetAvailableModels).toHaveBeenCalledTimes(2); }); }); it('should pass o11y headers to GatewayLanguageModel when environment variables are set', async () => { const originalEnv = process.env; process.env = { ...originalEnv, VERCEL_DEPLOYMENT_ID: 'test-deployment', VERCEL_ENV: 'test', VERCEL_REGION: 'iad1', }; vi.mocked(getVercelRequestId).mockResolvedValue('test-request-id'); try { const provider = createGatewayProvider({ baseURL: 'https://api.example.com', apiKey: 'test-api-key', }); provider('test-model'); const constructorCall = vi.mocked(GatewayLanguageModel).mock.calls[0]; const config = constructorCall[1]; expect(config).toEqual( expect.objectContaining({ provider: 'gateway', baseURL: 'https://api.example.com', o11yHeaders: expect.any(Function), }), ); // Test that the o11yHeaders function returns the expected result const o11yHeaders = await resolve(config.o11yHeaders); expect(o11yHeaders).toEqual({ 'ai-o11y-deployment-id': 'test-deployment', 'ai-o11y-environment': 'test', 'ai-o11y-region': 'iad1', 'ai-o11y-request-id': 'test-request-id', }); } finally { process.env = originalEnv; } }); it('should not include undefined o11y headers', async () => { const originalEnv = process.env; process.env = { ...originalEnv }; process.env.VERCEL_DEPLOYMENT_ID = undefined; process.env.VERCEL_ENV = undefined; process.env.VERCEL_REGION = undefined; vi.mocked(getVercelRequestId).mockResolvedValue(undefined); try { const provider = createGatewayProvider({ baseURL: 'https://api.example.com', apiKey: 'test-api-key', }); provider('test-model'); // Get the constructor call to check o11yHeaders const constructorCall = vi.mocked(GatewayLanguageModel).mock.calls[0]; const config = constructorCall[1]; expect(config).toEqual( expect.objectContaining({ provider: 'gateway', baseURL: 'https://api.example.com', o11yHeaders: expect.any(Function), }), ); // Test that the o11yHeaders function returns empty object const o11yHeaders = await resolve(config.o11yHeaders); expect(o11yHeaders).toEqual({}); } finally { process.env = originalEnv; } }); }); describe('default exported provider', () => { it('should export a default provider instance', () => { expect(gateway).toBeDefined(); expect(typeof gateway).toBe('function'); expect(typeof gateway.languageModel).toBe('function'); expect(typeof gateway.getAvailableModels).toBe('function'); }); it('should use the default baseURL when none is provided', async () => { // Set up mock to return empty models mockGetAvailableModels.mockReturnValue({ models: [] }); // Create a provider without specifying baseURL const testProvider = createGatewayProvider({ apiKey: 'test-key', // Provide API key to avoid OIDC token lookup }); // Trigger a request await testProvider.getAvailableModels(); // Check that GatewayFetchMetadata was instantiated with the default baseURL expect(GatewayFetchMetadata).toHaveBeenCalledWith( expect.objectContaining({ baseURL: 'https://ai-gateway.vercel.sh/v1/ai', }), ); }); it('should accept empty options', () => { // This should not throw an error const provider = createGatewayProvider(); expect(provider).toBeDefined(); expect(typeof provider).toBe('function'); expect(typeof provider.languageModel).toBe('function'); }); it('should override default baseURL when provided', async () => { // Reset mocks vi.clearAllMocks(); // Set up mock to return empty models mockGetAvailableModels.mockReturnValue({ models: [] }); const customBaseUrl = 'https://custom-api.example.com'; const testProvider = createGatewayProvider({ baseURL: customBaseUrl, apiKey: 'test-key', }); // Trigger a request await testProvider.getAvailableModels(); // Check that GatewayFetchMetadata was instantiated with the custom baseURL expect(GatewayFetchMetadata).toHaveBeenCalledWith( expect.objectContaining({ baseURL: customBaseUrl, }), ); expect(mockGetAvailableModels).toHaveBeenCalled(); }); it('should use apiKey over OIDC token when provided', async () => { // Reset the mocks vi.clearAllMocks(); // Mock getVercelOidcToken to ensure it's not called vi.mocked(getVercelOidcToken).mockRejectedValue( new Error('Should not be called'), ); // Set up mock to return empty models mockGetAvailableModels.mockReturnValue({ models: [] }); const testApiKey = 'test-api-key-123'; const testProvider = createGatewayProvider({ apiKey: testApiKey, }); // Trigger a request that will use the headers await testProvider.getAvailableModels(); // Get the headers function that was passed to GatewayFetchMetadata const config = vi.mocked(GatewayFetchMetadata).mock.calls[0][0]; const headers = await resolve(config.headers()); // Verify that the API key was used in the Authorization header expect(headers.Authorization).toBe(`Bearer ${testApiKey}`); expect(headers['ai-gateway-auth-method']).toBe('api-key'); // Verify getVercelOidcToken was never called expect(getVercelOidcToken).not.toHaveBeenCalled(); }); }); // Test data for different authentication scenarios const authTestCases = [ { name: 'no auth at all', envOidcToken: undefined, envApiKey: undefined, optionsApiKey: undefined, oidcTokenMock: null, // Will throw error expectSuccess: false, expectedError: 'authentication', description: 'No OIDC token or API key provided', }, { name: 'valid oidc, invalid api key', envOidcToken: 'valid-oidc-token-12345', envApiKey: undefined, optionsApiKey: 'invalid-api-key', oidcTokenMock: 'valid-oidc-token-12345', expectSuccess: true, expectedAuthMethod: 'api-key', // Options API key takes precedence description: 'Valid OIDC in env, but options API key takes precedence', }, { name: 'invalid oidc, valid api key', envOidcToken: 'invalid-oidc-token', envApiKey: undefined, optionsApiKey: 'gw_valid_api_key_12345', oidcTokenMock: null, // Will throw error expectSuccess: true, expectedAuthMethod: 'api-key', description: 'Invalid OIDC, but valid API key should work', }, { name: 'no oidc, invalid api key', envOidcToken: undefined, envApiKey: 'invalid-api-key', optionsApiKey: undefined, oidcTokenMock: null, // Will throw error expectSuccess: true, expectedAuthMethod: 'api-key', description: 'No OIDC, but env API key should be used', }, { name: 'no oidc, valid api key', envOidcToken: undefined, envApiKey: 'gw_valid_api_key_12345', optionsApiKey: undefined, oidcTokenMock: null, // Won't be called expectSuccess: true, expectedAuthMethod: 'api-key', description: 'Valid API key in environment should work', }, { name: 'valid oidc, no api key', envOidcToken: 'valid-oidc-token-12345', envApiKey: undefined, optionsApiKey: undefined, oidcTokenMock: 'valid-oidc-token-12345', expectSuccess: true, expectedAuthMethod: 'oidc', description: 'Valid OIDC token should work when no API key provided', }, { name: 'valid oidc, valid api key', envOidcToken: 'valid-oidc-token-12345', envApiKey: 'gw_valid_api_key_12345', optionsApiKey: undefined, oidcTokenMock: 'valid-oidc-token-12345', expectSuccess: true, expectedAuthMethod: 'api-key', description: 'Both valid credentials - API key should take precedence over OIDC', }, { name: 'valid oidc, valid options api key', envOidcToken: 'valid-oidc-token-12345', envApiKey: undefined, optionsApiKey: 'gw_valid_options_api_key_12345', oidcTokenMock: 'valid-oidc-token-12345', expectSuccess: true, expectedAuthMethod: 'api-key', description: 'Both valid credentials - options API key should take precedence over OIDC', }, { name: 'invalid oidc, no api key', envOidcToken: 'invalid-oidc-token', envApiKey: undefined, optionsApiKey: undefined, oidcTokenMock: null, // Will throw error expectSuccess: false, expectedError: 'authentication', description: 'Invalid OIDC and no API key should fail', }, { name: 'invalid oidc, invalid api key', envOidcToken: 'invalid-oidc-token', envApiKey: 'invalid-api-key', optionsApiKey: undefined, oidcTokenMock: null, // Will throw error for OIDC expectSuccess: true, expectedAuthMethod: 'api-key', // Env API key is still used even if "invalid" description: 'Environment API key takes precedence over OIDC failure', }, ]; describe('Authentication Comprehensive Tests', () => { let originalEnv: NodeJS.ProcessEnv; beforeEach(() => { // Store original environment originalEnv = process.env; }); afterEach(() => { // Restore original environment process.env = originalEnv; }); describe('getGatewayAuthToken function', () => { authTestCases.forEach(testCase => { it(`should handle ${testCase.name}`, async () => { // Set up environment variables for this test case process.env = { ...originalEnv }; // Only set environment variables if they have actual values if (testCase.envOidcToken !== undefined) { process.env.VERCEL_OIDC_TOKEN = testCase.envOidcToken; } else { delete process.env.VERCEL_OIDC_TOKEN; } if (testCase.envApiKey !== undefined) { process.env.AI_GATEWAY_API_KEY = testCase.envApiKey; } else { delete process.env.AI_GATEWAY_API_KEY; } // Mock OIDC token behavior if (testCase.oidcTokenMock) { vi.mocked(getVercelOidcToken).mockResolvedValue( testCase.oidcTokenMock, ); } else { vi.mocked(getVercelOidcToken).mockRejectedValue( new GatewayAuthenticationError({ message: 'OIDC token not available', statusCode: 401, }), ); } const options: any = {}; if (testCase.optionsApiKey) { options.apiKey = testCase.optionsApiKey; } if (testCase.expectSuccess) { // Test successful cases const result = await getGatewayAuthToken(options); expect(result).not.toBeNull(); expect(result?.authMethod).toBe(testCase.expectedAuthMethod); if (testCase.expectedAuthMethod === 'api-key') { const expectedToken = testCase.optionsApiKey || testCase.envApiKey; expect(result?.token).toBe(expectedToken); // If we used options API key, OIDC should not be called if (testCase.optionsApiKey) { expect(getVercelOidcToken).not.toHaveBeenCalled(); } } else if (testCase.expectedAuthMethod === 'oidc') { expect(result?.token).toBe(testCase.oidcTokenMock); expect(getVercelOidcToken).toHaveBeenCalled(); } } else { // Test failure cases const result = await getGatewayAuthToken(options); expect(result).toBeNull(); } }); }); }); describe('createGatewayProvider authentication', () => { authTestCases.forEach(testCase => { it(`should handle provider creation with ${testCase.name}`, async () => { // Set up environment variables for this test case process.env = { ...originalEnv }; // Only set environment variables if they have actual values if (testCase.envOidcToken !== undefined) { process.env.VERCEL_OIDC_TOKEN = testCase.envOidcToken; } else { delete process.env.VERCEL_OIDC_TOKEN; } if (testCase.envApiKey !== undefined) { process.env.AI_GATEWAY_API_KEY = testCase.envApiKey; } else { delete process.env.AI_GATEWAY_API_KEY; } // Mock OIDC token behavior if (testCase.oidcTokenMock) { vi.mocked(getVercelOidcToken).mockResolvedValue( testCase.oidcTokenMock, ); } else { vi.mocked(getVercelOidcToken).mockRejectedValue( new GatewayAuthenticationError({ message: 'OIDC token not available', statusCode: 401, }), ); } const options: any = { baseURL: 'https://test-gateway.example.com', }; if (testCase.optionsApiKey) { options.apiKey = testCase.optionsApiKey; } const provider = createGatewayProvider({ ...options, // Force no caching to ensure headers are called each time metadataCacheRefreshMillis: 0, }); if (testCase.expectSuccess) { // Ensure the mock succeeds for successful test cases mockGetAvailableModels.mockReturnValue({ models: [] }); // Test that provider can get available models (which requires auth) const models = await provider.getAvailableModels(); expect(models).toBeDefined(); // For OIDC tests, we need to verify the auth token function was called // which is indirectly tested by checking if getVercelOidcToken was called if (testCase.expectedAuthMethod === 'oidc') { expect(getVercelOidcToken).toHaveBeenCalled(); } else if ( testCase.expectedAuthMethod === 'api-key' && testCase.optionsApiKey ) { // If we used options API key, OIDC should not be called expect(getVercelOidcToken).not.toHaveBeenCalled(); } } else { // For failure cases, mock the metadata fetch to throw auth error mockGetAvailableModels.mockImplementation(() => { throw new GatewayAuthenticationError({ message: 'Authentication failed', statusCode: 401, }); }); // Test failure cases await expect(provider.getAvailableModels()).rejects.toThrow( /authentication|token/i, ); } }); }); }); describe('Environment variable edge cases', () => { it('should handle empty string environment variables as undefined', async () => { process.env = { ...originalEnv, VERCEL_OIDC_TOKEN: '', AI_GATEWAY_API_KEY: '', }; vi.mocked(getVercelOidcToken).mockRejectedValue( new GatewayAuthenticationError({ message: 'OIDC token not available', statusCode: 401, }), ); const result = await getGatewayAuthToken({}); expect(result).toBeNull(); }); it('should handle whitespace-only environment variables', async () => { process.env = { ...originalEnv, VERCEL_OIDC_TOKEN: ' ', AI_GATEWAY_API_KEY: '\t\n ', }; // The whitespace API key should still be used (it's treated as a valid value) const result = await getGatewayAuthToken({}); expect(result).not.toBeNull(); expect(result?.authMethod).toBe('api-key'); expect(result?.token).toBe('\t\n '); }); it('should prioritize options.apiKey over all environment variables', async () => { process.env = { ...originalEnv, VERCEL_OIDC_TOKEN: 'env-oidc-token', AI_GATEWAY_API_KEY: 'env-api-key', }; const optionsApiKey = 'options-api-key'; const result = await getGatewayAuthToken({ apiKey: optionsApiKey }); expect(result).not.toBeNull(); expect(result?.authMethod).toBe('api-key'); expect(result?.token).toBe(optionsApiKey); expect(getVercelOidcToken).not.toHaveBeenCalled(); }); }); describe('Authentication precedence', () => { it('should prefer options.apiKey over AI_GATEWAY_API_KEY', async () => { process.env = { ...originalEnv, AI_GATEWAY_API_KEY: 'env-api-key', }; const optionsApiKey = 'options-api-key'; const result = await getGatewayAuthToken({ apiKey: optionsApiKey }); expect(result?.authMethod).toBe('api-key'); expect(result?.token).toBe(optionsApiKey); expect(getVercelOidcToken).not.toHaveBeenCalled(); }); it('should prefer AI_GATEWAY_API_KEY over OIDC token', async () => { process.env = { ...originalEnv, VERCEL_OIDC_TOKEN: 'oidc-token', AI_GATEWAY_API_KEY: 'env-api-key', }; const result = await getGatewayAuthToken({}); expect(result?.authMethod).toBe('api-key'); expect(result?.token).toBe('env-api-key'); expect(getVercelOidcToken).not.toHaveBeenCalled(); }); it('should fall back to OIDC when no API keys are available', async () => { process.env = { ...originalEnv, VERCEL_OIDC_TOKEN: 'oidc-token', }; vi.mocked(getVercelOidcToken).mockResolvedValue('oidc-token'); const result = await getGatewayAuthToken({}); expect(result?.authMethod).toBe('oidc'); expect(result?.token).toBe('oidc-token'); expect(getVercelOidcToken).toHaveBeenCalled(); }); }); describe('Real-world usage scenarios', () => { it('should work in Vercel deployment with OIDC', async () => { // Simulate Vercel deployment environment process.env = { ...originalEnv, VERCEL_OIDC_TOKEN: 'vercel-deployment-oidc-token', VERCEL_DEPLOYMENT_ID: 'dpl_12345', VERCEL_ENV: 'production', VERCEL_REGION: 'iad1', }; // Explicitly remove AI_GATEWAY_API_KEY to force OIDC usage delete process.env.AI_GATEWAY_API_KEY; vi.mocked(getVercelOidcToken).mockResolvedValue( 'vercel-deployment-oidc-token', ); const provider = createGatewayProvider(); const models = await provider.getAvailableModels(); expect(models).toBeDefined(); expect(getVercelOidcToken).toHaveBeenCalled(); }); it('should work in local development with API key', async () => { // Simulate local development environment process.env = { ...originalEnv, AI_GATEWAY_API_KEY: 'local-dev-api-key', }; const provider = createGatewayProvider(); const models = await provider.getAvailableModels(); expect(models).toBeDefined(); expect(getVercelOidcToken).not.toHaveBeenCalled(); }); it('should work with explicit API key override', async () => { // User provides explicit API key, should override everything process.env = { ...originalEnv, VERCEL_OIDC_TOKEN: 'should-not-be-used', AI_GATEWAY_API_KEY: 'should-not-be-used-either', }; const explicitApiKey = 'explicit-user-api-key'; const provider = createGatewayProvider({ apiKey: explicitApiKey, }); const models = await provider.getAvailableModels(); expect(models).toBeDefined(); expect(getVercelOidcToken).not.toHaveBeenCalled(); }); }); }); describe('Error handling in metadata fetching', () => { it('should convert metadata fetch errors to Gateway errors', async () => { mockGetAvailableModels.mockImplementation(() => { throw new GatewayInternalServerError({ message: 'Database connection failed', statusCode: 500, }); }); const provider = createGatewayProvider({ baseURL: 'https://api.example.com', apiKey: 'test-key', }); await expect(provider.getAvailableModels()).rejects.toMatchObject({ name: 'GatewayInternalServerError', message: 'Database connection failed', statusCode: 500, }); }); it('should not double-wrap Gateway errors from metadata fetch', async () => { const originalError = new GatewayAuthenticationError({ message: 'Invalid token', statusCode: 401, }); mockGetAvailableModels.mockImplementation(() => { throw originalError; }); const provider = createGatewayProvider({ baseURL: 'https://api.example.com', apiKey: 'test-key', }); try { await provider.getAvailableModels(); fail('Expected error was not thrown'); } catch (error) { expect(error).toBe(originalError); // Same instance expect(error).toBeInstanceOf(GatewayAuthenticationError); expect((error as GatewayAuthenticationError).message).toBe( 'Invalid token', ); } }); it('should handle model specification errors', async () => { // Mock successful metadata fetch with a model mockGetAvailableModels.mockReturnValue({ models: [ { id: 'test-model', specification: { provider: 'test', specificationVersion: 'v2', modelId: 'test-model', }, }, ], }); const provider = createGatewayProvider({ baseURL: 'https://api.example.com', apiKey: 'test-key', }); // Create a language model that should work const model = provider('test-model'); expect(model).toBeDefined(); // Verify the model was created with the correct parameters expect(GatewayLanguageModel).toHaveBeenCalledWith( 'test-model', expect.objectContaining({ provider: 'gateway', baseURL: 'https://api.example.com', headers: expect.any(Function), fetch: undefined, o11yHeaders: expect.any(Function), }), ); }); it('should create language model for any modelId', async () => { // Mock successful metadata fetch with different models mockGetAvailableModels.mockReturnValue({ models: [ { id: 'model-1', specification: { provider: 'test', specificationVersion: 'v2', modelId: 'model-1', }, }, { id: 'model-2', specification: { provider: 'test', specificationVersion: 'v2', modelId: 'model-2', }, }, ], }); const provider = createGatewayProvider({ baseURL: 'https://api.example.com', apiKey: 'test-key', }); // Create a language model for any model ID const model = provider('any-model-id'); // The model should be created successfully expect(GatewayLanguageModel).toHaveBeenCalledWith( 'any-model-id', expect.objectContaining({ provider: 'gateway', baseURL: 'https://api.example.com', headers: expect.any(Function), fetch: undefined, o11yHeaders: expect.any(Function), }), ); expect(model).toBeDefined(); }); it('should handle non-existent model requests', async () => { const provider = createGatewayProvider({ baseURL: 'https://api.example.com', apiKey: 'test-key', }); // Create a language model for a non-existent model const model = provider('non-existent-model'); // The model should be created successfully (validation happens at API call time) expect(GatewayLanguageModel).toHaveBeenCalledWith( 'non-existent-model', expect.objectContaining({ provider: 'gateway', baseURL: 'https://api.example.com', headers: expect.any(Function), fetch: undefined, o11yHeaders: expect.any(Function), }), ); expect(model).toBeDefined(); }); }); }); --- File: /ai/packages/gateway/src/gateway-provider.ts --- import { NoSuchModelError } from '@ai-sdk/provider'; import { loadOptionalSetting, withoutTrailingSlash, type FetchFunction, } from '@ai-sdk/provider-utils'; import { asGatewayError, GatewayAuthenticationError } from './errors'; import { GATEWAY_AUTH_METHOD_HEADER, parseAuthMethod, } from './errors/parse-auth-method'; import { GatewayFetchMetadata, type GatewayFetchMetadataResponse, } from './gateway-fetch-metadata'; import { GatewayLanguageModel } from './gateway-language-model'; import { GatewayEmbeddingModel } from './gateway-embedding-model'; import type { GatewayEmbeddingModelId } from './gateway-embedding-model-settings'; import { getVercelOidcToken, getVercelRequestId } from './vercel-environment'; import type { GatewayModelId } from './gateway-language-model-settings'; import type { LanguageModelV2, EmbeddingModelV2, ProviderV2, } from '@ai-sdk/provider'; export interface GatewayProvider extends ProviderV2 { (modelId: GatewayModelId): LanguageModelV2; /** Creates a model for text generation. */ languageModel(modelId: GatewayModelId): LanguageModelV2; /** Returns available providers and models for use with the remote provider. */ getAvailableModels(): Promise<GatewayFetchMetadataResponse>; /** Creates a model for generating text embeddings. */ textEmbeddingModel( modelId: GatewayEmbeddingModelId, ): EmbeddingModelV2<string>; } export interface GatewayProviderSettings { /** The base URL prefix for API calls. Defaults to `https://ai-gateway.vercel.sh/v1/ai`. */ baseURL?: string; /** API key that is being sent using the `Authorization` header. */ apiKey?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; /** How frequently to refresh the metadata cache in milliseconds. */ metadataCacheRefreshMillis?: number; /** * @internal For testing purposes only */ _internal?: { currentDate?: () => Date; }; } const AI_GATEWAY_PROTOCOL_VERSION = '0.0.1'; /** Create a remote provider instance. */ export function createGatewayProvider( options: GatewayProviderSettings = {}, ): GatewayProvider { let pendingMetadata: Promise<GatewayFetchMetadataResponse> | null = null; let metadataCache: GatewayFetchMetadataResponse | null = null; const cacheRefreshMillis = options.metadataCacheRefreshMillis ?? 1000 * 60 * 5; let lastFetchTime = 0; const baseURL = withoutTrailingSlash(options.baseURL) ?? 'https://ai-gateway.vercel.sh/v1/ai'; const getHeaders = async () => { const auth = await getGatewayAuthToken(options); if (auth) { return { Authorization: `Bearer ${auth.token}`, 'ai-gateway-protocol-version': AI_GATEWAY_PROTOCOL_VERSION, [GATEWAY_AUTH_METHOD_HEADER]: auth.authMethod, ...options.headers, }; } throw GatewayAuthenticationError.createContextualError({ apiKeyProvided: false, oidcTokenProvided: false, statusCode: 401, }); }; const createO11yHeaders = () => { const deploymentId = loadOptionalSetting({ settingValue: undefined, environmentVariableName: 'VERCEL_DEPLOYMENT_ID', }); const environment = loadOptionalSetting({ settingValue: undefined, environmentVariableName: 'VERCEL_ENV', }); const region = loadOptionalSetting({ settingValue: undefined, environmentVariableName: 'VERCEL_REGION', }); return async () => { const requestId = await getVercelRequestId(); return { ...(deploymentId && { 'ai-o11y-deployment-id': deploymentId }), ...(environment && { 'ai-o11y-environment': environment }), ...(region && { 'ai-o11y-region': region }), ...(requestId && { 'ai-o11y-request-id': requestId }), }; }; }; const createLanguageModel = (modelId: GatewayModelId) => { return new GatewayLanguageModel(modelId, { provider: 'gateway', baseURL, headers: getHeaders, fetch: options.fetch, o11yHeaders: createO11yHeaders(), }); }; const getAvailableModels = async () => { const now = options._internal?.currentDate?.().getTime() ?? Date.now(); if (!pendingMetadata || now - lastFetchTime > cacheRefreshMillis) { lastFetchTime = now; pendingMetadata = new GatewayFetchMetadata({ baseURL, headers: getHeaders, fetch: options.fetch, }) .getAvailableModels() .then(metadata => { metadataCache = metadata; return metadata; }) .catch(async (error: unknown) => { throw asGatewayError(error, parseAuthMethod(await getHeaders())); }); } return metadataCache ? Promise.resolve(metadataCache) : pendingMetadata; }; const provider = function (modelId: GatewayModelId) { if (new.target) { throw new Error( 'The Gateway Provider model function cannot be called with the new keyword.', ); } return createLanguageModel(modelId); }; provider.getAvailableModels = getAvailableModels; provider.imageModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'imageModel' }); }; provider.languageModel = createLanguageModel; provider.textEmbeddingModel = (modelId: GatewayEmbeddingModelId) => { return new GatewayEmbeddingModel(modelId, { provider: 'gateway', baseURL, headers: getHeaders, fetch: options.fetch, o11yHeaders: createO11yHeaders(), }); }; return provider; } export const gateway = createGatewayProvider(); export async function getGatewayAuthToken( options: GatewayProviderSettings, ): Promise<{ token: string; authMethod: 'api-key' | 'oidc'; } | null> { const apiKey = loadOptionalSetting({ settingValue: options.apiKey, environmentVariableName: 'AI_GATEWAY_API_KEY', }); if (apiKey) { return { token: apiKey, authMethod: 'api-key', }; } try { const oidcToken = await getVercelOidcToken(); return { token: oidcToken, authMethod: 'oidc', }; } catch { return null; } } --- File: /ai/packages/gateway/src/index.ts --- export type { GatewayModelId } from './gateway-language-model-settings'; export type { GatewayLanguageModelEntry, GatewayLanguageModelSpecification, } from './gateway-model-entry'; export { createGatewayProvider, createGatewayProvider as createGateway, gateway, } from './gateway-provider'; export type { GatewayProvider, GatewayProviderSettings, } from './gateway-provider'; export type { GatewayProviderOptions } from './gateway-provider-options'; export { GatewayError, GatewayAuthenticationError, GatewayInvalidRequestError, GatewayRateLimitError, GatewayModelNotFoundError, GatewayInternalServerError, GatewayResponseError, } from './errors'; export type { GatewayErrorResponse } from './errors'; --- File: /ai/packages/gateway/src/vercel-environment.test.ts --- import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { getVercelOidcToken, getVercelRequestId } from './vercel-environment'; const SYMBOL_FOR_REQ_CONTEXT = Symbol.for('@vercel/request-context'); describe('getVercelOidcToken', () => { const originalEnv = process.env; const originalSymbolValue = (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT]; beforeEach(() => { process.env = { ...originalEnv }; (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = undefined; }); afterEach(() => { process.env = originalEnv; if (originalSymbolValue) { (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = originalSymbolValue; } else { (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = undefined; } }); it('should get token from request headers', async () => { (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = { get: () => ({ headers: { 'x-vercel-oidc-token': 'header-token-value', }, }), }; const token = await getVercelOidcToken(); expect(token).toBe('header-token-value'); }); it('should get token from environment variable when header is not available', async () => { (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = { get: () => ({ headers: {} }), }; process.env.VERCEL_OIDC_TOKEN = 'env-token-value'; const token = await getVercelOidcToken(); expect(token).toBe('env-token-value'); }); it('should prioritize header token over environment variable', async () => { (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = { get: () => ({ headers: { 'x-vercel-oidc-token': 'header-token-value', }, }), }; process.env.VERCEL_OIDC_TOKEN = 'env-token-value'; const token = await getVercelOidcToken(); expect(token).toBe('header-token-value'); }); it('should throw GatewayAuthenticationError when no token is available', async () => { (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = { get: () => ({ headers: {} }), }; process.env.VERCEL_OIDC_TOKEN = undefined; await expect(getVercelOidcToken()).rejects.toMatchObject({ name: 'GatewayAuthenticationError', type: 'authentication_error', statusCode: 401, message: expect.stringContaining('OIDC token not available'), }); }); it('should handle missing request context gracefully', async () => { (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = undefined; process.env.VERCEL_OIDC_TOKEN = 'env-token-value'; const token = await getVercelOidcToken(); expect(token).toBe('env-token-value'); }); it('should handle missing get method in request context', async () => { (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = {}; process.env.VERCEL_OIDC_TOKEN = 'env-token-value'; const token = await getVercelOidcToken(); expect(token).toBe('env-token-value'); }); }); describe('getVercelRequestId', () => { const originalSymbolValue = (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT]; beforeEach(() => { (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = undefined; }); afterEach(() => { if (originalSymbolValue) { (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = originalSymbolValue; } else { (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = undefined; } }); it('should get request ID from request headers when available', async () => { (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = { get: () => ({ headers: { 'x-vercel-id': 'req_1234567890abcdef', }, }), }; const requestId = await getVercelRequestId(); expect(requestId).toBe('req_1234567890abcdef'); }); it('should return undefined when request ID header is not available', async () => { (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = { get: () => ({ headers: {} }), }; const requestId = await getVercelRequestId(); expect(requestId).toBeUndefined(); }); it('should return undefined when no headers are available', async () => { (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = { get: () => ({}), }; const requestId = await getVercelRequestId(); expect(requestId).toBeUndefined(); }); it('should handle missing request context gracefully', async () => { (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = undefined; const requestId = await getVercelRequestId(); expect(requestId).toBeUndefined(); }); it('should handle missing get method in request context', async () => { (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = {}; const requestId = await getVercelRequestId(); expect(requestId).toBeUndefined(); }); }); --- File: /ai/packages/gateway/src/vercel-environment.ts --- import { GatewayAuthenticationError } from './errors'; export async function getVercelOidcToken(): Promise<string> { const token = getContext().headers?.['x-vercel-oidc-token'] ?? process.env.VERCEL_OIDC_TOKEN; if (!token) { throw new GatewayAuthenticationError({ message: 'OIDC token not available', statusCode: 401, }); } return token; } export async function getVercelRequestId(): Promise<string | undefined> { return getContext().headers?.['x-vercel-id']; } type Context = { headers?: Record<string, string>; }; const SYMBOL_FOR_REQ_CONTEXT = Symbol.for('@vercel/request-context'); function getContext(): Context { const fromSymbol: typeof globalThis & { [SYMBOL_FOR_REQ_CONTEXT]?: { get?: () => Context }; } = globalThis; return fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.() ?? {}; } --- File: /ai/packages/gateway/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/gateway/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/gateway/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/gladia/src/gladia-api-types.ts --- export type GladiaTranscriptionInitiateAPITypes = { /** URL to a Gladia file or to an external audio or video file */ audio_url: string; /** [Alpha] Context to feed the transcription model with for possible better accuracy */ context_prompt?: string; /** [Beta] Can be either boolean to enable custom_vocabulary or an array with specific vocabulary */ custom_vocabulary?: boolean | any[]; /** [Beta] Custom vocabulary configuration */ custom_vocabulary_config?: { /** Vocabulary array with string or object containing value, intensity, pronunciations, and language */ vocabulary: Array< | string | { /** Vocabulary value */ value: string; /** Intensity of the vocabulary */ intensity?: number; /** Pronunciation variations */ pronunciations?: string[]; /** Language of the vocabulary */ language?: string; } >; /** Default intensity for vocabulary */ default_intensity?: number; }; /** Detect the language from the given audio */ detect_language?: boolean; /** Detect multiple languages in the given audio */ enable_code_switching?: boolean; /** Configuration for code-switching */ code_switching_config?: { /** Specify the languages you want to use when detecting multiple languages */ languages?: string[]; }; /** The original language in iso639-1 format */ language?: string; /** Enable callback for this transcription */ callback?: boolean; /** Configuration for callback */ callback_config?: { /** The URL to be called with the result of the transcription */ url: string; /** The HTTP method to be used */ method?: 'POST' | 'PUT'; }; /** Enable subtitles generation for this transcription */ subtitles?: boolean; /** Configuration for subtitles */ subtitles_config?: { /** Subtitles formats */ formats?: ('srt' | 'vtt')[]; /** Minimum duration of a subtitle in seconds */ minimum_duration?: number; /** Maximum duration of a subtitle in seconds */ maximum_duration?: number; /** Maximum number of characters per row */ maximum_characters_per_row?: number; /** Maximum number of rows per caption */ maximum_rows_per_caption?: number; /** Style of the subtitles */ style?: 'default' | 'compliance'; }; /** Enable speaker recognition (diarization) for this audio */ diarization?: boolean; /** Configuration for diarization */ diarization_config?: { /** Exact number of speakers in the audio */ number_of_speakers?: number; /** Minimum number of speakers in the audio */ min_speakers?: number; /** Maximum number of speakers in the audio */ max_speakers?: number; /** [Alpha] Use enhanced diarization for this audio */ enhanced?: boolean; }; /** [Beta] Enable translation for this audio */ translation?: boolean; /** Configuration for translation */ translation_config?: { /** The target language in iso639-1 format */ target_languages: string[]; /** Model for translation */ model?: 'base' | 'enhanced'; /** Align translated utterances with the original ones */ match_original_utterances?: boolean; }; /** [Beta] Enable summarization for this audio */ summarization?: boolean; /** Configuration for summarization */ summarization_config?: { /** The type of summarization to apply */ type?: 'general' | 'bullet_points' | 'concise'; }; /** [Alpha] Enable moderation for this audio */ moderation?: boolean; /** [Alpha] Enable named entity recognition for this audio */ named_entity_recognition?: boolean; /** [Alpha] Enable chapterization for this audio */ chapterization?: boolean; /** [Alpha] Enable names consistency for this audio */ name_consistency?: boolean; /** [Alpha] Enable custom spelling for this audio */ custom_spelling?: boolean; /** Configuration for custom spelling */ custom_spelling_config?: { /** The list of spelling applied on the audio transcription */ spelling_dictionary: Record<string, string[]>; }; /** [Alpha] Enable structured data extraction for this audio */ structured_data_extraction?: boolean; /** Configuration for structured data extraction */ structured_data_extraction_config?: { /** The list of classes to extract from the audio transcription */ classes: string[]; }; /** [Alpha] Enable sentiment analysis for this audio */ sentiment_analysis?: boolean; /** [Alpha] Enable audio to llm processing for this audio */ audio_to_llm?: boolean; /** Configuration for audio to llm */ audio_to_llm_config?: { /** The list of prompts applied on the audio transcription */ prompts: string[]; }; /** Custom metadata you can attach to this transcription */ custom_metadata?: Record<string, any>; /** Enable sentences for this audio */ sentences?: boolean; /** [Alpha] Allows to change the output display_mode for this audio */ display_mode?: boolean; /** [Alpha] Use enhanced punctuation for this audio */ punctuation_enhanced?: boolean; }; --- File: /ai/packages/gladia/src/gladia-config.ts --- import { FetchFunction } from '@ai-sdk/provider-utils'; export type GladiaConfig = { provider: string; url: (options: { modelId: string; path: string }) => string; headers: () => Record<string, string | undefined>; fetch?: FetchFunction; generateId?: () => string; }; --- File: /ai/packages/gladia/src/gladia-error.test.ts --- import { safeParseJSON } from '@ai-sdk/provider-utils'; import { gladiaErrorDataSchema } from './gladia-error'; describe('gladiaErrorDataSchema', () => { it('should parse Gladia resource exhausted error', async () => { const error = ` {"error":{"message":"{\\n \\"error\\": {\\n \\"code\\": 429,\\n \\"message\\": \\"Resource has been exhausted (e.g. check quota).\\",\\n \\"status\\": \\"RESOURCE_EXHAUSTED\\"\\n }\\n}\\n","code":429}} `; const result = await safeParseJSON({ text: error, schema: gladiaErrorDataSchema, }); expect(result).toStrictEqual({ success: true, value: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, rawValue: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, }); }); }); --- File: /ai/packages/gladia/src/gladia-error.ts --- import { z } from 'zod/v4'; import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; export const gladiaErrorDataSchema = z.object({ error: z.object({ message: z.string(), code: z.number(), }), }); export type GladiaErrorData = z.infer<typeof gladiaErrorDataSchema>; export const gladiaFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: gladiaErrorDataSchema, errorToMessage: data => data.error.message, }); --- File: /ai/packages/gladia/src/gladia-provider.ts --- import { TranscriptionModelV2, ProviderV2, NoSuchModelError, } from '@ai-sdk/provider'; import { FetchFunction, loadApiKey } from '@ai-sdk/provider-utils'; import { GladiaTranscriptionModel } from './gladia-transcription-model'; export interface GladiaProvider extends ProviderV2 { (): { transcription: GladiaTranscriptionModel; }; /** Creates a model for transcription. */ transcription(): TranscriptionModelV2; } export interface GladiaProviderSettings { /** API key for authenticating requests. */ apiKey?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } /** Create a Gladia provider instance. */ export function createGladia( options: GladiaProviderSettings = {}, ): GladiaProvider { const getHeaders = () => ({ 'x-gladia-key': loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'GLADIA_API_KEY', description: 'Gladia', }), ...options.headers, }); const createTranscriptionModel = () => new GladiaTranscriptionModel('default', { provider: `gladia.transcription`, url: ({ path }) => `https://api.gladia.io${path}`, headers: getHeaders, fetch: options.fetch, }); const provider = function () { return { transcription: createTranscriptionModel(), }; }; provider.transcription = createTranscriptionModel; provider.transcriptionModel = createTranscriptionModel; // Required ProviderV2 methods that are not supported provider.languageModel = () => { throw new NoSuchModelError({ modelId: 'unknown', modelType: 'languageModel', message: 'Gladia does not provide language models', }); }; provider.textEmbeddingModel = () => { throw new NoSuchModelError({ modelId: 'unknown', modelType: 'textEmbeddingModel', message: 'Gladia does not provide text embedding models', }); }; provider.imageModel = () => { throw new NoSuchModelError({ modelId: 'unknown', modelType: 'imageModel', message: 'Gladia does not provide image models', }); }; return provider as GladiaProvider; } /** Default Gladia provider instance. */ export const gladia = createGladia(); --- File: /ai/packages/gladia/src/gladia-transcription-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { GladiaTranscriptionModel } from './gladia-transcription-model'; import { createGladia } from './gladia-provider'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; const audioData = await readFile(path.join(__dirname, 'transcript-test.mp3')); const provider = createGladia({ apiKey: 'test-api-key' }); const model = provider.transcription(); const server = createTestServer({ 'https://api.gladia.io/v2/upload': { response: { type: 'json-value', body: { audio_url: 'https://storage.gladia.io/mock-upload-url', audio_metadata: { id: 'test-id', filename: 'test-file.mp3', extension: 'mp3', size: 1024, audio_duration: 60, number_of_channels: 2, }, }, }, }, 'https://api.gladia.io/v2/pre-recorded': {}, 'https://api.gladia.io/v2/transcription/test-id': {}, }); describe('doGenerate', () => { function prepareJsonResponse({ headers, }: { headers?: Record<string, string>; } = {}) { // No need to set the upload response here as it's already set in the server creation server.urls['https://api.gladia.io/v2/pre-recorded'].response = { type: 'json-value', headers, body: { id: 'test-id', result_url: 'https://api.gladia.io/v2/transcription/test-id', }, }; server.urls['https://api.gladia.io/v2/transcription/test-id'].response = { type: 'json-value', headers, body: { id: '45463597-20b7-4af7-b3b3-f5fb778203ab', request_id: 'G-45463597', version: 2, status: 'done', created_at: '2023-12-28T09:04:17.210Z', completed_at: '2023-12-28T09:04:37.210Z', custom_metadata: {}, error_code: null, kind: 'pre-recorded', file: { id: 'test-id', filename: 'test-file.mp3', source: 'upload', audio_duration: 60, number_of_channels: 2, }, request_params: { audio_url: 'https://storage.gladia.io/mock-upload-url', }, result: { metadata: { audio_duration: 60, number_of_distinct_channels: 2, billing_time: 60, transcription_time: 20, }, transcription: { full_transcript: 'Smoke from hundreds of wildfires.', languages: ['en'], utterances: [ { language: 'en', start: 0, end: 3, confidence: 0.95, channel: 1, speaker: 1, words: [ { word: 'Smoke', start: 0, end: 1, confidence: 0.95, }, { word: 'from', start: 1, end: 2, confidence: 0.95, }, { word: 'hundreds', start: 2, end: 3, confidence: 0.95, }, ], text: 'Smoke from hundreds of wildfires.', }, ], }, }, }, }; } it('should pass the model', async () => { prepareJsonResponse(); await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(await server.calls[1].requestBodyJson).toMatchObject({ audio_url: 'https://storage.gladia.io/mock-upload-url', }); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createGladia({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.transcription().doGenerate({ audio: audioData, mediaType: 'audio/wav', headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[1].requestHeaders).toMatchObject({ 'x-gladia-key': 'test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should extract the transcription text', async () => { prepareJsonResponse(); const result = await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.text).toBe('Smoke from hundreds of wildfires.'); }); it('should include response data with timestamp, modelId and headers', async () => { prepareJsonResponse({ headers: { 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); const testDate = new Date(0); const customModel = new GladiaTranscriptionModel('default', { provider: 'test-provider', url: ({ path }) => `https://api.gladia.io${path}`, headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.response).toMatchObject({ timestamp: testDate, modelId: 'default', headers: { 'content-type': 'application/json', 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); }); it('should use real date when no custom date provider is specified', async () => { prepareJsonResponse(); const testDate = new Date(0); const customModel = new GladiaTranscriptionModel('default', { provider: 'test-provider', url: ({ path }) => `https://api.gladia.io${path}`, headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.response.timestamp.getTime()).toEqual(testDate.getTime()); expect(result.response.modelId).toBe('default'); }); }); --- File: /ai/packages/gladia/src/gladia-transcription-model.ts --- import { AISDKError, TranscriptionModelV2, TranscriptionModelV2CallWarning, } from '@ai-sdk/provider'; import { combineHeaders, convertBase64ToUint8Array, createJsonResponseHandler, delay, getFromApi, parseProviderOptions, postFormDataToApi, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { GladiaConfig } from './gladia-config'; import { gladiaFailedResponseHandler } from './gladia-error'; import { GladiaTranscriptionInitiateAPITypes } from './gladia-api-types'; // https://docs.gladia.io/api-reference/v2/pre-recorded/init const gladiaProviderOptionsSchema = z.object({ /** * Optional context prompt to guide the transcription. */ contextPrompt: z.string().nullish(), /** * Custom vocabulary to improve transcription accuracy. * Can be a boolean or an array of custom terms. */ customVocabulary: z.union([z.boolean(), z.array(z.any())]).nullish(), /** * Configuration for custom vocabulary. */ customVocabularyConfig: z .object({ /** * Array of vocabulary terms or objects with pronunciation details. */ vocabulary: z.array( z.union([ z.string(), z.object({ /** * The vocabulary term. */ value: z.string(), /** * Intensity of the term in recognition (optional). */ intensity: z.number().nullish(), /** * Alternative pronunciations for the term (optional). */ pronunciations: z.array(z.string()).nullish(), /** * Language of the term (optional). */ language: z.string().nullish(), }), ]), ), /** * Default intensity for all vocabulary terms. */ defaultIntensity: z.number().nullish(), }) .nullish(), /** * Whether to automatically detect the language of the audio. */ detectLanguage: z.boolean().nullish(), /** * Whether to enable code switching (multiple languages in the same audio). */ enableCodeSwitching: z.boolean().nullish(), /** * Configuration for code switching. */ codeSwitchingConfig: z .object({ /** * Languages to consider for code switching. */ languages: z.array(z.string()).nullish(), }) .nullish(), /** * Specific language for transcription. */ language: z.string().nullish(), /** * Whether to enable callback when transcription is complete. */ callback: z.boolean().nullish(), /** * Configuration for callback. */ callbackConfig: z .object({ /** * URL to send the callback to. */ url: z.string(), /** * HTTP method for the callback. */ method: z.enum(['POST', 'PUT']).nullish(), }) .nullish(), /** * Whether to generate subtitles. */ subtitles: z.boolean().nullish(), /** * Configuration for subtitles generation. */ subtitlesConfig: z .object({ /** * Subtitle file formats to generate. */ formats: z.array(z.enum(['srt', 'vtt'])).nullish(), /** * Minimum duration for subtitle segments. */ minimumDuration: z.number().nullish(), /** * Maximum duration for subtitle segments. */ maximumDuration: z.number().nullish(), /** * Maximum characters per row in subtitles. */ maximumCharactersPerRow: z.number().nullish(), /** * Maximum rows per caption in subtitles. */ maximumRowsPerCaption: z.number().nullish(), /** * Style of subtitles. */ style: z.enum(['default', 'compliance']).nullish(), }) .nullish(), /** * Whether to enable speaker diarization (speaker identification). */ diarization: z.boolean().nullish(), /** * Configuration for diarization. */ diarizationConfig: z .object({ /** * Exact number of speakers to identify. */ numberOfSpeakers: z.number().nullish(), /** * Minimum number of speakers to identify. */ minSpeakers: z.number().nullish(), /** * Maximum number of speakers to identify. */ maxSpeakers: z.number().nullish(), /** * Whether to use enhanced diarization. */ enhanced: z.boolean().nullish(), }) .nullish(), /** * Whether to translate the transcription. */ translation: z.boolean().nullish(), /** * Configuration for translation. */ translationConfig: z .object({ /** * Target languages for translation. */ targetLanguages: z.array(z.string()), /** * Translation model to use. */ model: z.enum(['base', 'enhanced']).nullish(), /** * Whether to match original utterances in translation. */ matchOriginalUtterances: z.boolean().nullish(), }) .nullish(), /** * Whether to generate a summary of the transcription. */ summarization: z.boolean().nullish(), /** * Configuration for summarization. */ summarizationConfig: z .object({ /** * Type of summary to generate. */ type: z.enum(['general', 'bullet_points', 'concise']).nullish(), }) .nullish(), /** * Whether to enable content moderation. */ moderation: z.boolean().nullish(), /** * Whether to enable named entity recognition. */ namedEntityRecognition: z.boolean().nullish(), /** * Whether to enable automatic chapter creation. */ chapterization: z.boolean().nullish(), /** * Whether to ensure consistent naming of entities. */ nameConsistency: z.boolean().nullish(), /** * Whether to enable custom spelling. */ customSpelling: z.boolean().nullish(), /** * Configuration for custom spelling. */ customSpellingConfig: z .object({ /** * Dictionary of custom spellings. */ spellingDictionary: z.record(z.string(), z.array(z.string())), }) .nullish(), /** * Whether to extract structured data from the transcription. */ structuredDataExtraction: z.boolean().nullish(), /** * Configuration for structured data extraction. */ structuredDataExtractionConfig: z .object({ /** * Classes of data to extract. */ classes: z.array(z.string()), }) .nullish(), /** * Whether to perform sentiment analysis on the transcription. */ sentimentAnalysis: z.boolean().nullish(), /** * Whether to send audio to a language model for processing. */ audioToLlm: z.boolean().nullish(), /** * Configuration for audio to language model processing. */ audioToLlmConfig: z .object({ /** * Prompts to send to the language model. */ prompts: z.array(z.string()), }) .nullish(), /** * Custom metadata to include with the transcription. */ customMetadata: z.record(z.string(), z.any()).nullish(), /** * Whether to include sentence-level segmentation. */ sentences: z.boolean().nullish(), /** * Whether to enable display mode. */ displayMode: z.boolean().nullish(), /** * Whether to enhance punctuation in the transcription. */ punctuationEnhanced: z.boolean().nullish(), }); export type GladiaTranscriptionCallOptions = z.infer< typeof gladiaProviderOptionsSchema >; interface GladiaTranscriptionModelConfig extends GladiaConfig { _internal?: { currentDate?: () => Date; }; } export class GladiaTranscriptionModel implements TranscriptionModelV2 { readonly specificationVersion = 'v2'; get provider(): string { return this.config.provider; } constructor( readonly modelId: 'default', private readonly config: GladiaTranscriptionModelConfig, ) {} private async getArgs({ providerOptions, }: Parameters<TranscriptionModelV2['doGenerate']>[0]) { const warnings: TranscriptionModelV2CallWarning[] = []; // Parse provider options const gladiaOptions = await parseProviderOptions({ provider: 'gladia', providerOptions, schema: gladiaProviderOptionsSchema, }); const body: Omit<GladiaTranscriptionInitiateAPITypes, 'audio_url'> = {}; // Add provider-specific options if (gladiaOptions) { body.context_prompt = gladiaOptions.contextPrompt ?? undefined; body.custom_vocabulary = gladiaOptions.customVocabulary ?? undefined; body.detect_language = gladiaOptions.detectLanguage ?? undefined; body.enable_code_switching = gladiaOptions.enableCodeSwitching ?? undefined; body.language = gladiaOptions.language ?? undefined; body.callback = gladiaOptions.callback ?? undefined; body.subtitles = gladiaOptions.subtitles ?? undefined; body.diarization = gladiaOptions.diarization ?? undefined; body.translation = gladiaOptions.translation ?? undefined; body.summarization = gladiaOptions.summarization ?? undefined; body.moderation = gladiaOptions.moderation ?? undefined; body.named_entity_recognition = gladiaOptions.namedEntityRecognition ?? undefined; body.chapterization = gladiaOptions.chapterization ?? undefined; body.name_consistency = gladiaOptions.nameConsistency ?? undefined; body.custom_spelling = gladiaOptions.customSpelling ?? undefined; body.structured_data_extraction = gladiaOptions.structuredDataExtraction ?? undefined; body.structured_data_extraction_config = gladiaOptions.structuredDataExtractionConfig ?? undefined; body.sentiment_analysis = gladiaOptions.sentimentAnalysis ?? undefined; body.audio_to_llm = gladiaOptions.audioToLlm ?? undefined; body.audio_to_llm_config = gladiaOptions.audioToLlmConfig ?? undefined; body.custom_metadata = gladiaOptions.customMetadata ?? undefined; body.sentences = gladiaOptions.sentences ?? undefined; body.display_mode = gladiaOptions.displayMode ?? undefined; body.punctuation_enhanced = gladiaOptions.punctuationEnhanced ?? undefined; if (gladiaOptions.customVocabularyConfig) { body.custom_vocabulary_config = { vocabulary: gladiaOptions.customVocabularyConfig.vocabulary.map( item => { if (typeof item === 'string') return item; return { value: item.value, intensity: item.intensity ?? undefined, pronunciations: item.pronunciations ?? undefined, language: item.language ?? undefined, }; }, ), default_intensity: gladiaOptions.customVocabularyConfig.defaultIntensity ?? undefined, }; } // Handle code switching config if (gladiaOptions.codeSwitchingConfig) { body.code_switching_config = { languages: gladiaOptions.codeSwitchingConfig.languages ?? undefined, }; } // Handle callback config if (gladiaOptions.callbackConfig) { body.callback_config = { url: gladiaOptions.callbackConfig.url, method: gladiaOptions.callbackConfig.method ?? undefined, }; } // Handle subtitles config if (gladiaOptions.subtitlesConfig) { body.subtitles_config = { formats: gladiaOptions.subtitlesConfig.formats ?? undefined, minimum_duration: gladiaOptions.subtitlesConfig.minimumDuration ?? undefined, maximum_duration: gladiaOptions.subtitlesConfig.maximumDuration ?? undefined, maximum_characters_per_row: gladiaOptions.subtitlesConfig.maximumCharactersPerRow ?? undefined, maximum_rows_per_caption: gladiaOptions.subtitlesConfig.maximumRowsPerCaption ?? undefined, style: gladiaOptions.subtitlesConfig.style ?? undefined, }; } // Handle diarization config if (gladiaOptions.diarizationConfig) { body.diarization_config = { number_of_speakers: gladiaOptions.diarizationConfig.numberOfSpeakers ?? undefined, min_speakers: gladiaOptions.diarizationConfig.minSpeakers ?? undefined, max_speakers: gladiaOptions.diarizationConfig.maxSpeakers ?? undefined, enhanced: gladiaOptions.diarizationConfig.enhanced ?? undefined, }; } // Handle translation config if (gladiaOptions.translationConfig) { body.translation_config = { target_languages: gladiaOptions.translationConfig.targetLanguages, model: gladiaOptions.translationConfig.model ?? undefined, match_original_utterances: gladiaOptions.translationConfig.matchOriginalUtterances ?? undefined, }; } // Handle summarization config if (gladiaOptions.summarizationConfig) { body.summarization_config = { type: gladiaOptions.summarizationConfig.type ?? undefined, }; } // Handle custom spelling config if (gladiaOptions.customSpellingConfig) { body.custom_spelling_config = { spelling_dictionary: gladiaOptions.customSpellingConfig.spellingDictionary, }; } } return { body, warnings, }; } async doGenerate( options: Parameters<TranscriptionModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<TranscriptionModelV2['doGenerate']>>> { const currentDate = this.config._internal?.currentDate?.() ?? new Date(); // Create form data with base fields const formData = new FormData(); const blob = options.audio instanceof Uint8Array ? new Blob([options.audio]) : new Blob([convertBase64ToUint8Array(options.audio)]); formData.append('model', this.modelId); formData.append( 'audio', new File([blob], 'audio', { type: options.mediaType }), ); const { value: uploadResponse } = await postFormDataToApi({ url: this.config.url({ path: '/v2/upload', modelId: 'default', }), headers: combineHeaders(this.config.headers(), options.headers), formData, failedResponseHandler: gladiaFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( gladiaUploadResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const { body, warnings } = await this.getArgs(options); const { value: transcriptionInitResponse } = await postJsonToApi({ url: this.config.url({ path: '/v2/pre-recorded', modelId: 'default', }), headers: combineHeaders(this.config.headers(), options.headers), body: { ...body, audio_url: uploadResponse.audio_url, }, failedResponseHandler: gladiaFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( gladiaTranscriptionInitializeResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); // Poll the result URL until the transcription is done or an error occurs const resultUrl = transcriptionInitResponse.result_url; let transcriptionResult; let transcriptionResultHeaders; const timeoutMs = 60 * 1000; // 60 seconds timeout const startTime = Date.now(); const pollingInterval = 1000; while (true) { // Check if we've exceeded the timeout if (Date.now() - startTime > timeoutMs) { throw new AISDKError({ message: 'Transcription job polling timed out', name: 'TranscriptionJobPollingTimedOut', cause: transcriptionResult, }); } const response = await getFromApi({ url: resultUrl, headers: combineHeaders(this.config.headers(), options.headers), failedResponseHandler: gladiaFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( gladiaTranscriptionResultResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); transcriptionResult = response.value; transcriptionResultHeaders = response.responseHeaders; if (transcriptionResult.status === 'done') { break; } if (transcriptionResult.status === 'error') { throw new AISDKError({ message: 'Transcription job failed', name: 'TranscriptionJobFailed', cause: transcriptionResult, }); } // Wait for the configured polling interval before checking again await delay(pollingInterval); } if (!transcriptionResult.result) { throw new AISDKError({ message: 'Transcription result is empty', name: 'TranscriptionResultEmpty', cause: transcriptionResult, }); } // Process the successful result return { text: transcriptionResult.result.transcription.full_transcript, durationInSeconds: transcriptionResult.result.metadata.audio_duration, language: transcriptionResult.result.transcription.languages.at(0), segments: transcriptionResult.result.transcription.utterances.map( utterance => ({ text: utterance.text, startSecond: utterance.start, endSecond: utterance.end, }), ), response: { timestamp: currentDate, modelId: 'default', headers: transcriptionResultHeaders, }, providerMetadata: { gladia: transcriptionResult, }, warnings, }; } } const gladiaUploadResponseSchema = z.object({ audio_url: z.string(), }); const gladiaTranscriptionInitializeResponseSchema = z.object({ result_url: z.string(), }); const gladiaTranscriptionResultResponseSchema = z.object({ status: z.enum(['queued', 'processing', 'done', 'error']), result: z .object({ metadata: z.object({ audio_duration: z.number(), }), transcription: z.object({ full_transcript: z.string(), languages: z.array(z.string()), utterances: z.array( z.object({ start: z.number(), end: z.number(), text: z.string(), }), ), }), }) .nullish(), }); --- File: /ai/packages/gladia/src/index.ts --- export { createGladia, gladia } from './gladia-provider'; export type { GladiaProvider, GladiaProviderSettings } from './gladia-provider'; --- File: /ai/packages/gladia/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/gladia/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/gladia/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/google/src/internal/index.ts --- export * from '../google-generative-ai-language-model'; export { googleTools } from '../google-tools'; export type { GoogleGenerativeAIModelId } from '../google-generative-ai-options'; --- File: /ai/packages/google/src/tool/code-execution.ts --- import { createProviderDefinedToolFactoryWithOutputSchema } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; /** * A tool that enables the model to generate and run Python code. * * @note Ensure the selected model supports Code Execution. * Multi-tool usage with the code execution tool is typically compatible with Gemini >=2 models. * * @see https://ai.google.dev/gemini-api/docs/code-execution (Google AI) * @see https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/code-execution-api (Vertex AI) */ export const codeExecution = createProviderDefinedToolFactoryWithOutputSchema< { language: string; code: string; }, { outcome: string; output: string; }, {} >({ id: 'google.code_execution', name: 'code_execution', inputSchema: z.object({ language: z.string().describe('The programming language of the code.'), code: z.string().describe('The code to be executed.'), }), outputSchema: z.object({ outcome: z .string() .describe('The outcome of the execution (e.g., "OUTCOME_OK").'), output: z.string().describe('The output from the code execution.'), }), }); --- File: /ai/packages/google/src/tool/google-search.ts --- import { createProviderDefinedToolFactory } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; // https://ai.google.dev/gemini-api/docs/google-search // https://ai.google.dev/api/generate-content#GroundingSupport // https://cloud.google.com/vertex-ai/generative-ai/docs/grounding/grounding-with-google-search export const groundingChunkSchema = z.object({ web: z.object({ uri: z.string(), title: z.string() }).nullish(), retrievedContext: z.object({ uri: z.string(), title: z.string() }).nullish(), }); export const groundingMetadataSchema = z.object({ webSearchQueries: z.array(z.string()).nullish(), retrievalQueries: z.array(z.string()).nullish(), searchEntryPoint: z.object({ renderedContent: z.string() }).nullish(), groundingChunks: z.array(groundingChunkSchema).nullish(), groundingSupports: z .array( z.object({ segment: z.object({ startIndex: z.number().nullish(), endIndex: z.number().nullish(), text: z.string().nullish(), }), segment_text: z.string().nullish(), groundingChunkIndices: z.array(z.number()).nullish(), supportChunkIndices: z.array(z.number()).nullish(), confidenceScores: z.array(z.number()).nullish(), confidenceScore: z.array(z.number()).nullish(), }), ) .nullish(), retrievalMetadata: z .union([ z.object({ webDynamicRetrievalScore: z.number(), }), z.object({}), ]) .nullish(), }); export const googleSearch = createProviderDefinedToolFactory< {}, { /** * The mode of the predictor to be used in dynamic retrieval. The following modes are supported: * - MODE_DYNAMIC: Run retrieval only when system decides it is necessary * - MODE_UNSPECIFIED: Always trigger retrieval * @default MODE_UNSPECIFIED */ mode?: 'MODE_DYNAMIC' | 'MODE_UNSPECIFIED'; /** * The threshold to be used in dynamic retrieval (if not set, a system default value is used). */ dynamicThreshold?: number; } >({ id: 'google.google_search', name: 'google_search', inputSchema: z.object({ mode: z .enum(['MODE_DYNAMIC', 'MODE_UNSPECIFIED']) .default('MODE_UNSPECIFIED'), dynamicThreshold: z.number().default(1), }), }); --- File: /ai/packages/google/src/tool/url-context.ts --- import { createProviderDefinedToolFactory } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; // https://ai.google.dev/api/generate-content#UrlRetrievalMetadata const urlMetadataSchema = z.object({ retrievedUrl: z.string(), urlRetrievalStatus: z.string(), }); export const urlContextMetadataSchema = z.object({ urlMetadata: z.array(urlMetadataSchema), }); export const urlContext = createProviderDefinedToolFactory< { // Url context does not have any input schema, it will directly use the url from the prompt }, {} >({ id: 'google.url_context', name: 'url_context', inputSchema: z.object({}), }); --- File: /ai/packages/google/src/convert-json-schema-to-openapi-schema.test.ts --- import { JSONSchema7 } from '@ai-sdk/provider'; import { convertJSONSchemaToOpenAPISchema } from './convert-json-schema-to-openapi-schema'; it('should remove additionalProperties and $schema', () => { const input: JSONSchema7 = { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { name: { type: 'string' }, age: { type: 'number' }, }, additionalProperties: false, }; const expected = { type: 'object', properties: { name: { type: 'string' }, age: { type: 'number' }, }, }; expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected); }); it('should remove additionalProperties object from nested object schemas', function () { const input: JSONSchema7 = { type: 'object', properties: { keys: { type: 'object', additionalProperties: { type: 'string' }, description: 'Description for the key', }, }, additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }; const expected = { type: 'object', properties: { keys: { type: 'object', description: 'Description for the key', }, }, }; expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected); }); it('should handle nested objects and arrays', () => { const input: JSONSchema7 = { type: 'object', properties: { users: { type: 'array', items: { type: 'object', properties: { id: { type: 'number' }, name: { type: 'string' }, }, additionalProperties: false, }, }, }, additionalProperties: false, }; const expected = { type: 'object', properties: { users: { type: 'array', items: { type: 'object', properties: { id: { type: 'number' }, name: { type: 'string' }, }, }, }, }, }; expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected); }); it('should convert "const" to "enum" with a single value', () => { const input: JSONSchema7 = { type: 'object', properties: { status: { const: 'active' }, }, }; const expected = { type: 'object', properties: { status: { enum: ['active'] }, }, }; expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected); }); it('should handle allOf, anyOf, and oneOf', () => { const input: JSONSchema7 = { type: 'object', properties: { allOfProp: { allOf: [{ type: 'string' }, { minLength: 5 }] }, anyOfProp: { anyOf: [{ type: 'string' }, { type: 'number' }] }, oneOfProp: { oneOf: [{ type: 'boolean' }, { type: 'null' }] }, }, }; const expected = { type: 'object', properties: { allOfProp: { allOf: [{ type: 'string' }, { minLength: 5 }], }, anyOfProp: { anyOf: [{ type: 'string' }, { type: 'number' }], }, oneOfProp: { oneOf: [{ type: 'boolean' }, { type: 'null' }], }, }, }; expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected); }); it('should convert "format: date-time" to "format: date-time"', () => { const input: JSONSchema7 = { type: 'object', properties: { timestamp: { type: 'string', format: 'date-time' }, }, }; const expected = { type: 'object', properties: { timestamp: { type: 'string', format: 'date-time' }, }, }; expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected); }); it('should handle required properties', () => { const input: JSONSchema7 = { type: 'object', properties: { id: { type: 'number' }, name: { type: 'string' }, }, required: ['id'], }; const expected = { type: 'object', properties: { id: { type: 'number' }, name: { type: 'string' }, }, required: ['id'], }; expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected); }); it('should convert deeply nested "const" to "enum"', () => { const input: JSONSchema7 = { type: 'object', properties: { nested: { type: 'object', properties: { deeplyNested: { anyOf: [ { type: 'object', properties: { value: { const: 'specific value', }, }, }, { type: 'string', }, ], }, }, }, }, }; const expected = { type: 'object', properties: { nested: { type: 'object', properties: { deeplyNested: { anyOf: [ { type: 'object', properties: { value: { enum: ['specific value'], }, }, }, { type: 'string', }, ], }, }, }, }, }; expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected); }); it('should correctly convert a complex schema with nested const and anyOf', () => { const input: JSONSchema7 = { type: 'object', properties: { name: { type: 'string', }, age: { type: 'number', }, contact: { anyOf: [ { type: 'object', properties: { type: { type: 'string', const: 'email', }, value: { type: 'string', }, }, required: ['type', 'value'], additionalProperties: false, }, { type: 'object', properties: { type: { type: 'string', const: 'phone', }, value: { type: 'string', }, }, required: ['type', 'value'], additionalProperties: false, }, ], }, occupation: { anyOf: [ { type: 'object', properties: { type: { type: 'string', const: 'employed', }, company: { type: 'string', }, position: { type: 'string', }, }, required: ['type', 'company', 'position'], additionalProperties: false, }, { type: 'object', properties: { type: { type: 'string', const: 'student', }, school: { type: 'string', }, grade: { type: 'number', }, }, required: ['type', 'school', 'grade'], additionalProperties: false, }, { type: 'object', properties: { type: { type: 'string', const: 'unemployed', }, }, required: ['type'], additionalProperties: false, }, ], }, }, required: ['name', 'age', 'contact', 'occupation'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }; const expected = { type: 'object', properties: { name: { type: 'string', }, age: { type: 'number', }, contact: { anyOf: [ { type: 'object', properties: { type: { type: 'string', enum: ['email'], }, value: { type: 'string', }, }, required: ['type', 'value'], }, { type: 'object', properties: { type: { type: 'string', enum: ['phone'], }, value: { type: 'string', }, }, required: ['type', 'value'], }, ], }, occupation: { anyOf: [ { type: 'object', properties: { type: { type: 'string', enum: ['employed'], }, company: { type: 'string', }, position: { type: 'string', }, }, required: ['type', 'company', 'position'], }, { type: 'object', properties: { type: { type: 'string', enum: ['student'], }, school: { type: 'string', }, grade: { type: 'number', }, }, required: ['type', 'school', 'grade'], }, { type: 'object', properties: { type: { type: 'string', enum: ['unemployed'], }, }, required: ['type'], }, ], }, }, required: ['name', 'age', 'contact', 'occupation'], }; expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected); }); it('should handle null type correctly', () => { const input: JSONSchema7 = { type: 'object', properties: { nullableField: { type: ['string', 'null'], }, explicitNullField: { type: 'null', }, }, }; const expected = { type: 'object', properties: { nullableField: { type: 'string', nullable: true, }, explicitNullField: { type: 'null', }, }, }; expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected); }); it('should handle descriptions', () => { const input: JSONSchema7 = { type: 'object', description: 'A user object', properties: { id: { type: 'number', description: 'The user ID', }, name: { type: 'string', description: "The user's full name", }, email: { type: 'string', format: 'email', description: "The user's email address", }, }, required: ['id', 'name'], }; const expected = { type: 'object', description: 'A user object', properties: { id: { type: 'number', description: 'The user ID', }, name: { type: 'string', description: "The user's full name", }, email: { type: 'string', format: 'email', description: "The user's email address", }, }, required: ['id', 'name'], }; expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected); }); it('should return undefined for empty object schemas', () => { const emptyObjectSchemas = [ { type: 'object' }, { type: 'object', properties: {} }, ] as const; emptyObjectSchemas.forEach(schema => { expect(convertJSONSchemaToOpenAPISchema(schema)).toBeUndefined(); }); }); it('should handle non-empty object schemas', () => { const nonEmptySchema: JSONSchema7 = { type: 'object', properties: { name: { type: 'string' }, }, }; expect(convertJSONSchemaToOpenAPISchema(nonEmptySchema)).toEqual({ type: 'object', properties: { name: { type: 'string' }, }, }); }); it('should convert string enum properties', () => { const schemaWithEnumProperty: JSONSchema7 = { type: 'object', properties: { kind: { type: 'string', enum: ['text', 'code', 'image'], }, }, required: ['kind'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }; expect(convertJSONSchemaToOpenAPISchema(schemaWithEnumProperty)).toEqual({ type: 'object', properties: { kind: { type: 'string', enum: ['text', 'code', 'image'], }, }, required: ['kind'], }); }); it('should convert nullable string enum', () => { const schemaWithEnumProperty: JSONSchema7 = { type: 'object', properties: { fieldD: { anyOf: [ { type: 'string', enum: ['a', 'b', 'c'], }, { type: 'null', }, ], }, }, required: ['fieldD'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }; expect(convertJSONSchemaToOpenAPISchema(schemaWithEnumProperty)).toEqual({ required: ['fieldD'], type: 'object', properties: { fieldD: { nullable: true, type: 'string', enum: ['a', 'b', 'c'], }, }, }); }); --- File: /ai/packages/google/src/convert-json-schema-to-openapi-schema.ts --- import { JSONSchema7Definition } from '@ai-sdk/provider'; /** * Converts JSON Schema 7 to OpenAPI Schema 3.0 */ export function convertJSONSchemaToOpenAPISchema( jsonSchema: JSONSchema7Definition | undefined, ): unknown { // parameters need to be undefined if they are empty objects: if (jsonSchema == null || isEmptyObjectSchema(jsonSchema)) { return undefined; } if (typeof jsonSchema === 'boolean') { return { type: 'boolean', properties: {} }; } const { type, description, required, properties, items, allOf, anyOf, oneOf, format, const: constValue, minLength, enum: enumValues, } = jsonSchema; const result: Record<string, unknown> = {}; if (description) result.description = description; if (required) result.required = required; if (format) result.format = format; if (constValue !== undefined) { result.enum = [constValue]; } // Handle type if (type) { if (Array.isArray(type)) { if (type.includes('null')) { result.type = type.filter(t => t !== 'null')[0]; result.nullable = true; } else { result.type = type; } } else if (type === 'null') { result.type = 'null'; } else { result.type = type; } } // Handle enum if (enumValues !== undefined) { result.enum = enumValues; } if (properties != null) { result.properties = Object.entries(properties).reduce( (acc, [key, value]) => { acc[key] = convertJSONSchemaToOpenAPISchema(value); return acc; }, {} as Record<string, unknown>, ); } if (items) { result.items = Array.isArray(items) ? items.map(convertJSONSchemaToOpenAPISchema) : convertJSONSchemaToOpenAPISchema(items); } if (allOf) { result.allOf = allOf.map(convertJSONSchemaToOpenAPISchema); } if (anyOf) { // Handle cases where anyOf includes a null type if ( anyOf.some( schema => typeof schema === 'object' && schema?.type === 'null', ) ) { const nonNullSchemas = anyOf.filter( schema => !(typeof schema === 'object' && schema?.type === 'null'), ); if (nonNullSchemas.length === 1) { // If there's only one non-null schema, convert it and make it nullable const converted = convertJSONSchemaToOpenAPISchema(nonNullSchemas[0]); if (typeof converted === 'object') { result.nullable = true; Object.assign(result, converted); } } else { // If there are multiple non-null schemas, keep them in anyOf result.anyOf = nonNullSchemas.map(convertJSONSchemaToOpenAPISchema); result.nullable = true; } } else { result.anyOf = anyOf.map(convertJSONSchemaToOpenAPISchema); } } if (oneOf) { result.oneOf = oneOf.map(convertJSONSchemaToOpenAPISchema); } if (minLength !== undefined) { result.minLength = minLength; } return result; } function isEmptyObjectSchema(jsonSchema: JSONSchema7Definition): boolean { return ( jsonSchema != null && typeof jsonSchema === 'object' && jsonSchema.type === 'object' && (jsonSchema.properties == null || Object.keys(jsonSchema.properties).length === 0) && !jsonSchema.additionalProperties ); } --- File: /ai/packages/google/src/convert-to-google-generative-ai-messages.test.ts --- import { convertToGoogleGenerativeAIMessages } from './convert-to-google-generative-ai-messages'; describe('system messages', () => { it('should store system message in system instruction', async () => { const result = convertToGoogleGenerativeAIMessages([ { role: 'system', content: 'Test' }, ]); expect(result).toEqual({ systemInstruction: { parts: [{ text: 'Test' }] }, contents: [], }); }); it('should throw error when there was already a user message', async () => { expect(() => convertToGoogleGenerativeAIMessages([ { role: 'user', content: [{ type: 'text', text: 'Test' }] }, { role: 'system', content: 'Test' }, ]), ).toThrow( 'system messages are only supported at the beginning of the conversation', ); }); }); describe('thought signatures', () => { it('should preserve thought signatures in assistant messages', async () => { const result = convertToGoogleGenerativeAIMessages([ { role: 'assistant', content: [ { type: 'text', text: 'Regular text', providerOptions: { google: { thoughtSignature: 'sig1' } }, }, { type: 'reasoning', text: 'Reasoning text', providerOptions: { google: { thoughtSignature: 'sig2' } }, }, { type: 'tool-call', toolCallId: 'call1', toolName: 'test', input: { value: 'test' }, providerOptions: { google: { thoughtSignature: 'sig3' } }, }, ], }, ]); expect(result).toMatchInlineSnapshot(` { "contents": [ { "parts": [ { "text": "Regular text", "thoughtSignature": "sig1", }, { "text": "Reasoning text", "thought": true, "thoughtSignature": "sig2", }, { "functionCall": { "args": { "value": "test", }, "name": "test", }, "thoughtSignature": "sig3", }, ], "role": "model", }, ], "systemInstruction": undefined, } `); }); }); describe('Gemma model system instructions', () => { it('should prepend system instruction to first user message for Gemma models', async () => { const result = convertToGoogleGenerativeAIMessages( [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ], { isGemmaModel: true }, ); expect(result).toMatchInlineSnapshot(` { "contents": [ { "parts": [ { "text": "You are a helpful assistant. ", }, { "text": "Hello", }, ], "role": "user", }, ], "systemInstruction": undefined, } `); }); it('should handle multiple system messages for Gemma models', async () => { const result = convertToGoogleGenerativeAIMessages( [ { role: 'system', content: 'You are helpful.' }, { role: 'system', content: 'Be concise.' }, { role: 'user', content: [{ type: 'text', text: 'Hi' }] }, ], { isGemmaModel: true }, ); expect(result).toMatchInlineSnapshot(` { "contents": [ { "parts": [ { "text": "You are helpful. Be concise. ", }, { "text": "Hi", }, ], "role": "user", }, ], "systemInstruction": undefined, } `); }); it('should not affect non-Gemma models', async () => { const result = convertToGoogleGenerativeAIMessages( [ { role: 'system', content: 'You are helpful.' }, { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ], { isGemmaModel: false }, ); expect(result).toMatchInlineSnapshot(` { "contents": [ { "parts": [ { "text": "Hello", }, ], "role": "user", }, ], "systemInstruction": { "parts": [ { "text": "You are helpful.", }, ], }, } `); }); it('should handle Gemma model with system instruction but no user messages', async () => { const result = convertToGoogleGenerativeAIMessages( [{ role: 'system', content: 'You are helpful.' }], { isGemmaModel: true }, ); expect(result).toMatchInlineSnapshot(` { "contents": [], "systemInstruction": undefined, } `); }); }); describe('user messages', () => { it('should add image parts', async () => { const result = convertToGoogleGenerativeAIMessages([ { role: 'user', content: [ { type: 'file', data: 'AAECAw==', mediaType: 'image/png', }, ], }, ]); expect(result).toEqual({ systemInstruction: undefined, contents: [ { role: 'user', parts: [ { inlineData: { data: 'AAECAw==', mimeType: 'image/png', }, }, ], }, ], }); }); it('should add file parts for base64 encoded files', async () => { const result = convertToGoogleGenerativeAIMessages([ { role: 'user', content: [{ type: 'file', data: 'AAECAw==', mediaType: 'image/png' }], }, ]); expect(result).toEqual({ systemInstruction: undefined, contents: [ { role: 'user', parts: [ { inlineData: { data: 'AAECAw==', mimeType: 'image/png', }, }, ], }, ], }); }); }); describe('tool messages', () => { it('should convert tool result messages to function responses', async () => { const result = convertToGoogleGenerativeAIMessages([ { role: 'tool', content: [ { type: 'tool-result', toolName: 'testFunction', toolCallId: 'testCallId', output: { type: 'json', value: { someData: 'test result' } }, }, ], }, ]); expect(result).toEqual({ systemInstruction: undefined, contents: [ { role: 'user', parts: [ { functionResponse: { name: 'testFunction', response: { name: 'testFunction', content: { someData: 'test result' }, }, }, }, ], }, ], }); }); }); describe('assistant messages', () => { it('should add PNG image parts for base64 encoded files', async () => { const result = convertToGoogleGenerativeAIMessages([ { role: 'assistant', content: [{ type: 'file', data: 'AAECAw==', mediaType: 'image/png' }], }, ]); expect(result).toEqual({ systemInstruction: undefined, contents: [ { role: 'model', parts: [ { inlineData: { data: 'AAECAw==', mimeType: 'image/png', }, }, ], }, ], }); }); it('should throw error for non-PNG images in assistant messages', async () => { expect(() => convertToGoogleGenerativeAIMessages([ { role: 'assistant', content: [ { type: 'file', data: 'AAECAw==', mediaType: 'image/jpeg' }, ], }, ]), ).toThrow('Only PNG images are supported in assistant messages'); }); it('should throw error for URL file data in assistant messages', async () => { expect(() => convertToGoogleGenerativeAIMessages([ { role: 'assistant', content: [ { type: 'file', data: new URL('https://example.com/image.png'), mediaType: 'image/png', }, ], }, ]), ).toThrow('File data URLs in assistant messages are not supported'); }); }); --- File: /ai/packages/google/src/convert-to-google-generative-ai-messages.ts --- import { LanguageModelV2Prompt, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { GoogleGenerativeAIContent, GoogleGenerativeAIContentPart, GoogleGenerativeAIPrompt, } from './google-generative-ai-prompt'; import { convertToBase64 } from '@ai-sdk/provider-utils'; export function convertToGoogleGenerativeAIMessages( prompt: LanguageModelV2Prompt, options?: { isGemmaModel?: boolean }, ): GoogleGenerativeAIPrompt { const systemInstructionParts: Array<{ text: string }> = []; const contents: Array<GoogleGenerativeAIContent> = []; let systemMessagesAllowed = true; const isGemmaModel = options?.isGemmaModel ?? false; for (const { role, content } of prompt) { switch (role) { case 'system': { if (!systemMessagesAllowed) { throw new UnsupportedFunctionalityError({ functionality: 'system messages are only supported at the beginning of the conversation', }); } systemInstructionParts.push({ text: content }); break; } case 'user': { systemMessagesAllowed = false; const parts: GoogleGenerativeAIContentPart[] = []; for (const part of content) { switch (part.type) { case 'text': { parts.push({ text: part.text }); break; } case 'file': { // default to image/jpeg for unknown image/* types const mediaType = part.mediaType === 'image/*' ? 'image/jpeg' : part.mediaType; parts.push( part.data instanceof URL ? { fileData: { mimeType: mediaType, fileUri: part.data.toString(), }, } : { inlineData: { mimeType: mediaType, data: convertToBase64(part.data), }, }, ); break; } } } contents.push({ role: 'user', parts }); break; } case 'assistant': { systemMessagesAllowed = false; contents.push({ role: 'model', parts: content .map(part => { switch (part.type) { case 'text': { return part.text.length === 0 ? undefined : { text: part.text, thoughtSignature: part.providerOptions?.google?.thoughtSignature, }; } case 'reasoning': { return part.text.length === 0 ? undefined : { text: part.text, thought: true, thoughtSignature: part.providerOptions?.google?.thoughtSignature, }; } case 'file': { if (part.mediaType !== 'image/png') { throw new UnsupportedFunctionalityError({ functionality: 'Only PNG images are supported in assistant messages', }); } if (part.data instanceof URL) { throw new UnsupportedFunctionalityError({ functionality: 'File data URLs in assistant messages are not supported', }); } return { inlineData: { mimeType: part.mediaType, data: convertToBase64(part.data), }, }; } case 'tool-call': { return { functionCall: { name: part.toolName, args: part.input, }, thoughtSignature: part.providerOptions?.google?.thoughtSignature, }; } } }) .filter(part => part !== undefined), }); break; } case 'tool': { systemMessagesAllowed = false; contents.push({ role: 'user', parts: content.map(part => ({ functionResponse: { name: part.toolName, response: { name: part.toolName, content: part.output.value, }, }, })), }); break; } } } if ( isGemmaModel && systemInstructionParts.length > 0 && contents.length > 0 && contents[0].role === 'user' ) { const systemText = systemInstructionParts .map(part => part.text) .join('\n\n'); contents[0].parts.unshift({ text: systemText + '\n\n' }); } return { systemInstruction: systemInstructionParts.length > 0 && !isGemmaModel ? { parts: systemInstructionParts } : undefined, contents, }; } --- File: /ai/packages/google/src/get-model-path.test.ts --- import { getModelPath } from './get-model-path'; it('should pass through model path for models/*', async () => { expect(getModelPath('models/some-model')).toEqual('models/some-model'); }); it('should pass through model path for tunedModels/*', async () => { expect(getModelPath('tunedModels/some-model')).toEqual( 'tunedModels/some-model', ); }); it('should add model path prefix to models without slash', async () => { expect(getModelPath('some-model')).toEqual('models/some-model'); }); --- File: /ai/packages/google/src/get-model-path.ts --- export function getModelPath(modelId: string): string { return modelId.includes('/') ? modelId : `models/${modelId}`; } --- File: /ai/packages/google/src/google-error.ts --- import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; const googleErrorDataSchema = z.object({ error: z.object({ code: z.number().nullable(), message: z.string(), status: z.string(), }), }); export type GoogleErrorData = z.infer<typeof googleErrorDataSchema>; export const googleFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: googleErrorDataSchema, errorToMessage: data => data.error.message, }); --- File: /ai/packages/google/src/google-generative-ai-embedding-model.test.ts --- import { EmbeddingModelV2Embedding } from '@ai-sdk/provider'; import { createTestServer } from '@ai-sdk/provider-utils/test'; import { GoogleGenerativeAIEmbeddingModel } from './google-generative-ai-embedding-model'; import { createGoogleGenerativeAI } from './google-provider'; const dummyEmbeddings = [ [0.1, 0.2, 0.3, 0.4, 0.5], [0.6, 0.7, 0.8, 0.9, 1.0], ]; const testValues = ['sunny day at the beach', 'rainy day in the city']; const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key' }); const model = provider.textEmbeddingModel('gemini-embedding-001'); const URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:something'; const server = createTestServer({ [URL]: {}, }); describe('GoogleGenerativeAIEmbeddingModel', () => { function prepareBatchJsonResponse({ embeddings = dummyEmbeddings, headers, }: { embeddings?: EmbeddingModelV2Embedding[]; headers?: Record<string, string>; } = {}) { server.urls[URL].response = { type: 'json-value', headers, body: { embeddings: embeddings.map(embedding => ({ values: embedding })), }, }; } function prepareSingleJsonResponse({ embeddings = dummyEmbeddings, headers, }: { embeddings?: EmbeddingModelV2Embedding[]; headers?: Record<string, string>; } = {}) { server.urls[URL].response = { type: 'json-value', headers, body: { embedding: { values: embeddings[0] }, }, }; } it('should extract embedding', async () => { prepareBatchJsonResponse(); const { embeddings } = await model.doEmbed({ values: testValues }); expect(embeddings).toStrictEqual(dummyEmbeddings); }); it('should expose the raw response', async () => { prepareBatchJsonResponse({ headers: { 'test-header': 'test-value', }, }); const { response } = await model.doEmbed({ values: testValues }); expect(response?.headers).toStrictEqual({ // default headers: 'content-length': '80', 'content-type': 'application/json', // custom header 'test-header': 'test-value', }); expect(response).toMatchSnapshot(); }); it('should pass the model and the values', async () => { prepareBatchJsonResponse(); await model.doEmbed({ values: testValues }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ requests: testValues.map(value => ({ model: 'models/gemini-embedding-001', content: { role: 'user', parts: [{ text: value }] }, })), }); }); it('should pass the outputDimensionality setting', async () => { prepareBatchJsonResponse(); await provider.embedding('gemini-embedding-001').doEmbed({ values: testValues, providerOptions: { google: { outputDimensionality: 64 }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ requests: testValues.map(value => ({ model: 'models/gemini-embedding-001', content: { role: 'user', parts: [{ text: value }] }, outputDimensionality: 64, })), }); }); it('should pass the taskType setting', async () => { prepareBatchJsonResponse(); await provider.embedding('gemini-embedding-001').doEmbed({ values: testValues, providerOptions: { google: { taskType: 'SEMANTIC_SIMILARITY' } }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ requests: testValues.map(value => ({ model: 'models/gemini-embedding-001', content: { role: 'user', parts: [{ text: value }] }, taskType: 'SEMANTIC_SIMILARITY', })), }); }); it('should pass headers', async () => { prepareBatchJsonResponse(); const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.embedding('gemini-embedding-001').doEmbed({ values: testValues, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'x-goog-api-key': 'test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should throw an error if too many values are provided', async () => { const model = new GoogleGenerativeAIEmbeddingModel('gemini-embedding-001', { provider: 'google.generative-ai', baseURL: 'https://generativelanguage.googleapis.com/v1beta', headers: () => ({}), }); const tooManyValues = Array(2049).fill('test'); await expect(model.doEmbed({ values: tooManyValues })).rejects.toThrow( 'Too many values for a single embedding call. The google.generative-ai model "gemini-embedding-001" can only embed up to 2048 values per call, but 2049 values were provided.', ); }); it('should use the batch embeddings endpoint', async () => { prepareBatchJsonResponse(); const model = provider.textEmbeddingModel('gemini-embedding-001'); await model.doEmbed({ values: testValues, }); expect(server.calls[0].requestUrl).toBe( 'https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:batchEmbedContents', ); }); it('should use the single embeddings endpoint', async () => { prepareSingleJsonResponse(); const model = provider.textEmbeddingModel('gemini-embedding-001'); await model.doEmbed({ values: [testValues[0]], }); expect(server.calls[0].requestUrl).toBe( 'https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent', ); }); }); --- File: /ai/packages/google/src/google-generative-ai-embedding-model.ts --- import { EmbeddingModelV2, TooManyEmbeddingValuesForCallError, } from '@ai-sdk/provider'; import { combineHeaders, createJsonResponseHandler, FetchFunction, parseProviderOptions, postJsonToApi, resolve, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { googleFailedResponseHandler } from './google-error'; import { GoogleGenerativeAIEmbeddingModelId, googleGenerativeAIEmbeddingProviderOptions, } from './google-generative-ai-embedding-options'; type GoogleGenerativeAIEmbeddingConfig = { provider: string; baseURL: string; headers: () => Record<string, string | undefined>; fetch?: FetchFunction; }; export class GoogleGenerativeAIEmbeddingModel implements EmbeddingModelV2<string> { readonly specificationVersion = 'v2'; readonly modelId: GoogleGenerativeAIEmbeddingModelId; readonly maxEmbeddingsPerCall = 2048; readonly supportsParallelCalls = true; private readonly config: GoogleGenerativeAIEmbeddingConfig; get provider(): string { return this.config.provider; } constructor( modelId: GoogleGenerativeAIEmbeddingModelId, config: GoogleGenerativeAIEmbeddingConfig, ) { this.modelId = modelId; this.config = config; } async doEmbed({ values, headers, abortSignal, providerOptions, }: Parameters<EmbeddingModelV2<string>['doEmbed']>[0]): Promise< Awaited<ReturnType<EmbeddingModelV2<string>['doEmbed']>> > { // Parse provider options const googleOptions = await parseProviderOptions({ provider: 'google', providerOptions, schema: googleGenerativeAIEmbeddingProviderOptions, }); if (values.length > this.maxEmbeddingsPerCall) { throw new TooManyEmbeddingValuesForCallError({ provider: this.provider, modelId: this.modelId, maxEmbeddingsPerCall: this.maxEmbeddingsPerCall, values, }); } const mergedHeaders = combineHeaders( await resolve(this.config.headers), headers, ); // For single embeddings, use the single endpoint (ratelimits, etc.) if (values.length === 1) { const { responseHeaders, value: response, rawValue, } = await postJsonToApi({ url: `${this.config.baseURL}/models/${this.modelId}:embedContent`, headers: mergedHeaders, body: { model: `models/${this.modelId}`, content: { parts: [{ text: values[0] }], }, outputDimensionality: googleOptions?.outputDimensionality, taskType: googleOptions?.taskType, }, failedResponseHandler: googleFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( googleGenerativeAISingleEmbeddingResponseSchema, ), abortSignal, fetch: this.config.fetch, }); return { embeddings: [response.embedding.values], usage: undefined, response: { headers: responseHeaders, body: rawValue }, }; } const { responseHeaders, value: response, rawValue, } = await postJsonToApi({ url: `${this.config.baseURL}/models/${this.modelId}:batchEmbedContents`, headers: mergedHeaders, body: { requests: values.map(value => ({ model: `models/${this.modelId}`, content: { role: 'user', parts: [{ text: value }] }, outputDimensionality: googleOptions?.outputDimensionality, taskType: googleOptions?.taskType, })), }, failedResponseHandler: googleFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( googleGenerativeAITextEmbeddingResponseSchema, ), abortSignal, fetch: this.config.fetch, }); return { embeddings: response.embeddings.map(item => item.values), usage: undefined, response: { headers: responseHeaders, body: rawValue }, }; } } // minimal version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const googleGenerativeAITextEmbeddingResponseSchema = z.object({ embeddings: z.array(z.object({ values: z.array(z.number()) })), }); // Schema for single embedding response const googleGenerativeAISingleEmbeddingResponseSchema = z.object({ embedding: z.object({ values: z.array(z.number()) }), }); --- File: /ai/packages/google/src/google-generative-ai-embedding-options.ts --- import { z } from 'zod/v4'; export type GoogleGenerativeAIEmbeddingModelId = | 'gemini-embedding-001' | 'text-embedding-004' | (string & {}); export const googleGenerativeAIEmbeddingProviderOptions = z.object({ /** * Optional. Optional reduced dimension for the output embedding. * If set, excessive values in the output embedding are truncated from the end. */ outputDimensionality: z.number().optional(), /** * Optional. Specifies the task type for generating embeddings. * Supported task types: * - SEMANTIC_SIMILARITY: Optimized for text similarity. * - CLASSIFICATION: Optimized for text classification. * - CLUSTERING: Optimized for clustering texts based on similarity. * - RETRIEVAL_DOCUMENT: Optimized for document retrieval. * - RETRIEVAL_QUERY: Optimized for query-based retrieval. * - QUESTION_ANSWERING: Optimized for answering questions. * - FACT_VERIFICATION: Optimized for verifying factual information. * - CODE_RETRIEVAL_QUERY: Optimized for retrieving code blocks based on natural language queries. */ taskType: z .enum([ 'SEMANTIC_SIMILARITY', 'CLASSIFICATION', 'CLUSTERING', 'RETRIEVAL_DOCUMENT', 'RETRIEVAL_QUERY', 'QUESTION_ANSWERING', 'FACT_VERIFICATION', 'CODE_RETRIEVAL_QUERY', ]) .optional(), }); export type GoogleGenerativeAIEmbeddingProviderOptions = z.infer< typeof googleGenerativeAIEmbeddingProviderOptions >; --- File: /ai/packages/google/src/google-generative-ai-image-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { GoogleGenerativeAIImageModel } from './google-generative-ai-image-model'; const prompt = 'A cute baby sea otter'; const model = new GoogleGenerativeAIImageModel( 'imagen-3.0-generate-002', {}, { provider: 'google.generative-ai', baseURL: 'https://api.example.com/v1beta', headers: () => ({ 'api-key': 'test-api-key' }), }, ); const server = createTestServer({ 'https://api.example.com/v1beta/models/imagen-3.0-generate-002:predict': { response: { type: 'json-value', body: { predictions: [ { bytesBase64Encoded: 'base64-image-1' }, { bytesBase64Encoded: 'base64-image-2' }, ], }, }, }, }); describe('GoogleGenerativeAIImageModel', () => { describe('doGenerate', () => { function prepareJsonResponse({ headers, }: { headers?: Record<string, string>; } = {}) { const url = 'https://api.example.com/v1beta/models/imagen-3.0-generate-002:predict'; server.urls[url].response = { type: 'json-value', headers, body: { predictions: [ { bytesBase64Encoded: 'base64-image-1' }, { bytesBase64Encoded: 'base64-image-2' }, ], }, }; } it('should pass headers', async () => { prepareJsonResponse(); const modelWithHeaders = new GoogleGenerativeAIImageModel( 'imagen-3.0-generate-002', {}, { provider: 'google.generative-ai', baseURL: 'https://api.example.com/v1beta', headers: () => ({ 'Custom-Provider-Header': 'provider-header-value', }), }, ); await modelWithHeaders.doGenerate({ prompt, n: 2, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should respect maxImagesPerCall setting', () => { const customModel = new GoogleGenerativeAIImageModel( 'imagen-3.0-generate-002', { maxImagesPerCall: 2 }, { provider: 'google.generative-ai', baseURL: 'https://api.example.com/v1beta', headers: () => ({ 'api-key': 'test-api-key' }), }, ); expect(customModel.maxImagesPerCall).toBe(2); }); it('should use default maxImagesPerCall when not specified', () => { const defaultModel = new GoogleGenerativeAIImageModel( 'imagen-3.0-generate-002', {}, { provider: 'google.generative-ai', baseURL: 'https://api.example.com/v1beta', headers: () => ({ 'api-key': 'test-api-key' }), }, ); expect(defaultModel.maxImagesPerCall).toBe(4); }); it('should extract the generated images', async () => { prepareJsonResponse(); const result = await model.doGenerate({ prompt, n: 2, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.images).toStrictEqual(['base64-image-1', 'base64-image-2']); }); it('sends aspect ratio in the request', async () => { prepareJsonResponse(); await model.doGenerate({ prompt: 'test prompt', n: 1, size: undefined, aspectRatio: '16:9', seed: undefined, providerOptions: {}, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ instances: [{ prompt: 'test prompt' }], parameters: { sampleCount: 1, aspectRatio: '16:9', }, }); }); it('should pass aspect ratio directly when specified', async () => { prepareJsonResponse(); await model.doGenerate({ prompt: 'test prompt', n: 1, size: undefined, aspectRatio: '16:9', seed: undefined, providerOptions: {}, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ instances: [{ prompt: 'test prompt' }], parameters: { sampleCount: 1, aspectRatio: '16:9', }, }); }); it('should combine aspectRatio and provider options', async () => { prepareJsonResponse(); await model.doGenerate({ prompt: 'test prompt', n: 1, size: undefined, aspectRatio: '1:1', seed: undefined, providerOptions: { google: { personGeneration: 'dont_allow', }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ instances: [{ prompt: 'test prompt' }], parameters: { sampleCount: 1, personGeneration: 'dont_allow', aspectRatio: '1:1', }, }); }); it('should return warnings for unsupported settings', async () => { prepareJsonResponse(); const result = await model.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: '1:1', seed: 123, providerOptions: {}, }); expect(result.warnings).toStrictEqual([ { type: 'unsupported-setting', setting: 'size', details: 'This model does not support the `size` option. Use `aspectRatio` instead.', }, { type: 'unsupported-setting', setting: 'seed', details: 'This model does not support the `seed` option through this provider.', }, ]); }); it('should include response data with timestamp, modelId and headers', async () => { prepareJsonResponse({ headers: { 'request-id': 'test-request-id', 'x-goog-quota-remaining': '123', }, }); const testDate = new Date('2024-03-15T12:00:00Z'); const customModel = new GoogleGenerativeAIImageModel( 'imagen-3.0-generate-002', {}, { provider: 'google.generative-ai', baseURL: 'https://api.example.com/v1beta', headers: () => ({ 'api-key': 'test-api-key' }), _internal: { currentDate: () => testDate, }, }, ); const result = await customModel.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.response).toStrictEqual({ timestamp: testDate, modelId: 'imagen-3.0-generate-002', headers: { 'content-length': '97', 'content-type': 'application/json', 'request-id': 'test-request-id', 'x-goog-quota-remaining': '123', }, }); }); it('should use real date when no custom date provider is specified', async () => { prepareJsonResponse(); const beforeDate = new Date(); const result = await model.doGenerate({ prompt, n: 2, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); const afterDate = new Date(); expect(result.response.timestamp.getTime()).toBeGreaterThanOrEqual( beforeDate.getTime(), ); expect(result.response.timestamp.getTime()).toBeLessThanOrEqual( afterDate.getTime(), ); expect(result.response.modelId).toBe('imagen-3.0-generate-002'); }); it('should only pass valid provider options', async () => { prepareJsonResponse(); await model.doGenerate({ prompt, n: 2, size: undefined, aspectRatio: '16:9', seed: undefined, providerOptions: { google: { addWatermark: false, personGeneration: 'allow_all', foo: 'bar', // Invalid option negativePrompt: 'negative prompt', // Invalid option }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ instances: [{ prompt }], parameters: { sampleCount: 2, personGeneration: 'allow_all', aspectRatio: '16:9', }, }); }); }); }); --- File: /ai/packages/google/src/google-generative-ai-image-model.ts --- import { ImageModelV2, ImageModelV2CallWarning } from '@ai-sdk/provider'; import { combineHeaders, createJsonResponseHandler, parseProviderOptions, postJsonToApi, resolve, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { googleFailedResponseHandler } from './google-error'; import { GoogleGenerativeAIImageModelId, GoogleGenerativeAIImageSettings, } from './google-generative-ai-image-settings'; import { FetchFunction, Resolvable } from '@ai-sdk/provider-utils'; interface GoogleGenerativeAIImageModelConfig { provider: string; baseURL: string; headers?: Resolvable<Record<string, string | undefined>>; fetch?: FetchFunction; generateId?: () => string; _internal?: { currentDate?: () => Date; }; } export class GoogleGenerativeAIImageModel implements ImageModelV2 { readonly specificationVersion = 'v2'; get maxImagesPerCall(): number { // https://ai.google.dev/gemini-api/docs/imagen#imagen-model return this.settings.maxImagesPerCall ?? 4; } get provider(): string { return this.config.provider; } constructor( readonly modelId: GoogleGenerativeAIImageModelId, private readonly settings: GoogleGenerativeAIImageSettings, private readonly config: GoogleGenerativeAIImageModelConfig, ) {} async doGenerate( options: Parameters<ImageModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<ImageModelV2['doGenerate']>>> { const { prompt, n = 1, size = '1024x1024', aspectRatio = '1:1', seed, providerOptions, headers, abortSignal, } = options; const warnings: Array<ImageModelV2CallWarning> = []; if (size != null) { warnings.push({ type: 'unsupported-setting', setting: 'size', details: 'This model does not support the `size` option. Use `aspectRatio` instead.', }); } if (seed != null) { warnings.push({ type: 'unsupported-setting', setting: 'seed', details: 'This model does not support the `seed` option through this provider.', }); } const googleOptions = await parseProviderOptions({ provider: 'google', providerOptions, schema: googleImageProviderOptionsSchema, }); const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const parameters: Record<string, unknown> = { sampleCount: n, }; if (aspectRatio != null) { parameters.aspectRatio = aspectRatio; } if (googleOptions) { Object.assign(parameters, googleOptions); } const body = { instances: [{ prompt }], parameters, }; const { responseHeaders, value: response } = await postJsonToApi<{ predictions: Array<{ bytesBase64Encoded: string }>; }>({ url: `${this.config.baseURL}/models/${this.modelId}:predict`, headers: combineHeaders(await resolve(this.config.headers), headers), body, failedResponseHandler: googleFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( googleImageResponseSchema, ), abortSignal, fetch: this.config.fetch, }); return { images: response.predictions.map( (p: { bytesBase64Encoded: string }) => p.bytesBase64Encoded, ), warnings: warnings ?? [], providerMetadata: { google: { images: response.predictions.map(prediction => ({ // Add any prediction-specific metadata here })), }, }, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, }, }; } } // minimal version of the schema const googleImageResponseSchema = z.object({ predictions: z .array(z.object({ bytesBase64Encoded: z.string() })) .default([]), }); // Note: For the initial GA launch of Imagen 3, safety filters are not configurable. // https://ai.google.dev/gemini-api/docs/imagen#imagen-model const googleImageProviderOptionsSchema = z.object({ personGeneration: z .enum(['dont_allow', 'allow_adult', 'allow_all']) .nullish(), aspectRatio: z.enum(['1:1', '3:4', '4:3', '9:16', '16:9']).nullish(), }); export type GoogleGenerativeAIImageProviderOptions = z.infer< typeof googleImageProviderOptionsSchema >; --- File: /ai/packages/google/src/google-generative-ai-image-settings.ts --- export type GoogleGenerativeAIImageModelId = | 'imagen-3.0-generate-002' | (string & {}); export interface GoogleGenerativeAIImageSettings { /** Override the maximum number of images per call (default 4) */ maxImagesPerCall?: number; } --- File: /ai/packages/google/src/google-generative-ai-language-model.test.ts --- import { LanguageModelV2Prompt, LanguageModelV2ProviderDefinedTool, } from '@ai-sdk/provider'; import { convertReadableStreamToArray, createTestServer, } from '@ai-sdk/provider-utils/test'; import { GoogleGenerativeAILanguageModel } from './google-generative-ai-language-model'; import { GoogleGenerativeAIGroundingMetadata, GoogleGenerativeAIUrlContextMetadata, } from './google-generative-ai-prompt'; import { createGoogleGenerativeAI } from './google-provider'; import { groundingMetadataSchema } from './tool/google-search'; import { urlContextMetadataSchema } from './tool/url-context'; const TEST_PROMPT: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; const SAFETY_RATINGS = [ { category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', probability: 'NEGLIGIBLE', }, { category: 'HARM_CATEGORY_HATE_SPEECH', probability: 'NEGLIGIBLE', }, { category: 'HARM_CATEGORY_HARASSMENT', probability: 'NEGLIGIBLE', }, { category: 'HARM_CATEGORY_DANGEROUS_CONTENT', probability: 'NEGLIGIBLE', }, ]; const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key', generateId: () => 'test-id', }); const model = provider.chat('gemini-pro'); describe('groundingMetadataSchema', () => { it('validates complete grounding metadata with web search results', () => { const metadata = { webSearchQueries: ["What's the weather in Chicago this weekend?"], searchEntryPoint: { renderedContent: 'Sample rendered content for search results', }, groundingChunks: [ { web: { uri: 'https://example.com/weather', title: 'Chicago Weather Forecast', }, }, ], groundingSupports: [ { segment: { startIndex: 0, endIndex: 65, text: 'Chicago weather changes rapidly, so layers let you adjust easily.', }, groundingChunkIndices: [0], confidenceScores: [0.99], }, ], retrievalMetadata: { webDynamicRetrievalScore: 0.96879, }, }; const result = groundingMetadataSchema.safeParse(metadata); expect(result.success).toBe(true); }); it('validates complete grounding metadata with Vertex AI Search results', () => { const metadata = { retrievalQueries: ['How to make appointment to renew driving license?'], groundingChunks: [ { retrievedContext: { uri: 'https://vertexaisearch.cloud.google.com/grounding-api-redirect/AXiHM.....QTN92V5ePQ==', title: 'dmv', }, }, ], groundingSupports: [ { segment: { startIndex: 25, endIndex: 147, }, segment_text: 'ipsum lorem ...', supportChunkIndices: [1, 2], confidenceScore: [0.9541752, 0.97726375], }, ], }; const result = groundingMetadataSchema.safeParse(metadata); expect(result.success).toBe(true); }); it('validates partial grounding metadata', () => { const metadata = { webSearchQueries: ['sample query'], // Missing other optional fields }; const result = groundingMetadataSchema.safeParse(metadata); expect(result.success).toBe(true); }); it('validates empty grounding metadata', () => { const metadata = {}; const result = groundingMetadataSchema.safeParse(metadata); expect(result.success).toBe(true); }); it('validates metadata with empty retrievalMetadata', () => { const metadata = { webSearchQueries: ['sample query'], retrievalMetadata: {}, }; const result = groundingMetadataSchema.safeParse(metadata); expect(result.success).toBe(true); }); it('rejects invalid data types', () => { const metadata = { webSearchQueries: 'not an array', // Should be an array groundingSupports: [ { confidenceScores: 'not an array', // Should be an array of numbers }, ], }; const result = groundingMetadataSchema.safeParse(metadata); expect(result.success).toBe(false); }); }); describe('urlContextMetadata', () => { it('validates complete url context output', () => { const output = { urlMetadata: [ { retrievedUrl: 'https://example.com/weather', urlRetrievalStatus: 'URL_RETRIEVAL_STATUS_SUCCESS', }, ], }; const result = urlContextMetadataSchema.safeParse(output); expect(result.success).toBe(true); }); it('validates empty url context output', () => { const output = { urlMetadata: [], }; const result = urlContextMetadataSchema.safeParse(output); expect(result.success).toBe(true); }); }); describe('doGenerate', () => { const TEST_URL_GEMINI_PRO = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent'; const TEST_URL_GEMINI_2_0_PRO = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-pro:generateContent'; const TEST_URL_GEMINI_2_0_FLASH_EXP = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent'; const TEST_URL_GEMINI_1_0_PRO = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.0-pro:generateContent'; const TEST_URL_GEMINI_1_5_FLASH = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent'; const server = createTestServer({ [TEST_URL_GEMINI_PRO]: {}, [TEST_URL_GEMINI_2_0_PRO]: {}, [TEST_URL_GEMINI_2_0_FLASH_EXP]: {}, [TEST_URL_GEMINI_1_0_PRO]: {}, [TEST_URL_GEMINI_1_5_FLASH]: {}, }); const prepareJsonResponse = ({ content = '', usage = { promptTokenCount: 1, candidatesTokenCount: 2, totalTokenCount: 3, }, headers, groundingMetadata, url = TEST_URL_GEMINI_PRO, }: { content?: string; usage?: { promptTokenCount: number; candidatesTokenCount: number; totalTokenCount: number; }; headers?: Record<string, string>; groundingMetadata?: GoogleGenerativeAIGroundingMetadata; url?: | typeof TEST_URL_GEMINI_PRO | typeof TEST_URL_GEMINI_2_0_PRO | typeof TEST_URL_GEMINI_2_0_FLASH_EXP | typeof TEST_URL_GEMINI_1_0_PRO | typeof TEST_URL_GEMINI_1_5_FLASH; }) => { server.urls[url].response = { type: 'json-value', headers, body: { candidates: [ { content: { parts: [{ text: content }], role: 'model', }, finishReason: 'STOP', index: 0, safetyRatings: SAFETY_RATINGS, ...(groundingMetadata && { groundingMetadata }), }, ], promptFeedback: { safetyRatings: SAFETY_RATINGS }, usageMetadata: usage, }, }; }; it('should extract text response', async () => { prepareJsonResponse({ content: 'Hello, World!' }); const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "providerMetadata": undefined, "text": "Hello, World!", "type": "text", }, ] `); }); it('should extract usage', async () => { prepareJsonResponse({ usage: { promptTokenCount: 20, candidatesTokenCount: 5, totalTokenCount: 25, }, }); const { usage } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(usage).toMatchInlineSnapshot(` { "cachedInputTokens": undefined, "inputTokens": 20, "outputTokens": 5, "reasoningTokens": undefined, "totalTokens": 25, } `); }); it('should handle MALFORMED_FUNCTION_CALL finish reason and empty content object', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'json-value', body: { candidates: [ { content: {}, finishReason: 'MALFORMED_FUNCTION_CALL', }, ], usageMetadata: { promptTokenCount: 9056, totalTokenCount: 9056, promptTokensDetails: [ { modality: 'TEXT', tokenCount: 9056, }, ], }, modelVersion: 'gemini-2.0-flash-lite', }, }; const { content, finishReason } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(`[]`); expect(finishReason).toStrictEqual('error'); }); it('should extract tool calls', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'json-value', body: { candidates: [ { content: { parts: [ { functionCall: { name: 'test-tool', args: { value: 'example value' }, }, }, ], role: 'model', }, finishReason: 'STOP', index: 0, safetyRatings: SAFETY_RATINGS, }, ], promptFeedback: { safetyRatings: SAFETY_RATINGS }, }, }; const { content, finishReason } = await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "input": "{"value":"example value"}", "providerMetadata": undefined, "toolCallId": "test-id", "toolName": "test-tool", "type": "tool-call", }, ] `); expect(finishReason).toStrictEqual('tool-calls'); }); it('should expose the raw response headers', async () => { prepareJsonResponse({ headers: { 'test-header': 'test-value' } }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-length': '804', 'content-type': 'application/json', // custom header 'test-header': 'test-value', }); }); it('should pass the model, messages, and options', async () => { prepareJsonResponse({}); await model.doGenerate({ prompt: [ { role: 'system', content: 'test system instruction' }, { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ], seed: 123, temperature: 0.5, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ contents: [ { role: 'user', parts: [{ text: 'Hello' }], }, ], systemInstruction: { parts: [{ text: 'test system instruction' }] }, generationConfig: { seed: 123, temperature: 0.5, }, }); }); it('should only pass valid provider options', async () => { prepareJsonResponse({}); await model.doGenerate({ prompt: [ { role: 'system', content: 'test system instruction' }, { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ], seed: 123, temperature: 0.5, providerOptions: { google: { foo: 'bar', responseModalities: ['TEXT', 'IMAGE'] }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ contents: [ { role: 'user', parts: [{ text: 'Hello' }], }, ], systemInstruction: { parts: [{ text: 'test system instruction' }] }, generationConfig: { seed: 123, temperature: 0.5, responseModalities: ['TEXT', 'IMAGE'], }, }); }); it('should pass tools and toolChoice', async () => { prepareJsonResponse({}); await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], toolChoice: { type: 'tool', toolName: 'test-tool', }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ generationConfig: {}, contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], tools: { functionDeclarations: [ { name: 'test-tool', description: '', parameters: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], }, }, ], }, toolConfig: { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: ['test-tool'], }, }, }); }); it('should set response mime type with responseFormat', async () => { prepareJsonResponse({}); await model.doGenerate({ responseFormat: { type: 'json', schema: { type: 'object', properties: { location: { type: 'string' } }, }, }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ contents: [ { role: 'user', parts: [{ text: 'Hello' }], }, ], generationConfig: { responseMimeType: 'application/json', responseSchema: { type: 'object', properties: { location: { type: 'string', }, }, }, }, }); }); it('should pass specification with responseFormat and structuredOutputs = true (default)', async () => { prepareJsonResponse({}); await provider.languageModel('gemini-pro').doGenerate({ responseFormat: { type: 'json', schema: { type: 'object', properties: { property1: { type: 'string' }, property2: { type: 'number' }, }, required: ['property1', 'property2'], additionalProperties: false, }, }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], generationConfig: { responseMimeType: 'application/json', responseSchema: { properties: { property1: { type: 'string' }, property2: { type: 'number' }, }, required: ['property1', 'property2'], type: 'object', }, }, }); }); it('should not pass specification with responseFormat and structuredOutputs = false', async () => { prepareJsonResponse({}); await provider.languageModel('gemini-pro').doGenerate({ providerOptions: { google: { structuredOutputs: false, }, }, responseFormat: { type: 'json', schema: { type: 'object', properties: { property1: { type: 'string' }, property2: { type: 'number' }, }, required: ['property1', 'property2'], additionalProperties: false, }, }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], generationConfig: { responseMimeType: 'application/json', }, }); }); it('should pass tools and toolChoice', async () => { prepareJsonResponse({}); await provider.languageModel('gemini-pro').doGenerate({ tools: [ { name: 'test-tool', type: 'function', inputSchema: { type: 'object', properties: { property1: { type: 'string' }, property2: { type: 'number' }, }, required: ['property1', 'property2'], additionalProperties: false, }, }, ], toolChoice: { type: 'required' }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], generationConfig: {}, toolConfig: { functionCallingConfig: { mode: 'ANY' } }, tools: { functionDeclarations: [ { name: 'test-tool', description: '', parameters: { properties: { property1: { type: 'string' }, property2: { type: 'number' }, }, required: ['property1', 'property2'], type: 'object', }, }, ], }, }); }); it('should pass headers', async () => { prepareJsonResponse({}); const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.chat('gemini-pro').doGenerate({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, }); const requestHeaders = server.calls[0].requestHeaders; expect(requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', 'x-goog-api-key': 'test-api-key', }); }); it('should pass response format', async () => { prepareJsonResponse({}); await model.doGenerate({ prompt: TEST_PROMPT, responseFormat: { type: 'json', schema: { type: 'object', properties: { text: { type: 'string' }, }, required: ['text'], }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], generationConfig: { responseMimeType: 'application/json', responseSchema: { type: 'object', properties: { text: { type: 'string' }, }, required: ['text'], }, }, }); }); it('should send request body', async () => { prepareJsonResponse({}); await model.doGenerate({ prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], generationConfig: {}, }); }); it('should extract sources from grounding metadata', async () => { prepareJsonResponse({ content: 'test response', groundingMetadata: { groundingChunks: [ { web: { uri: 'https://source.example.com', title: 'Source Title' }, }, { retrievedContext: { uri: 'https://not-a-source.example.com', title: 'Not a Source', }, }, ], }, }); const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "providerMetadata": undefined, "text": "test response", "type": "text", }, { "id": "test-id", "sourceType": "url", "title": "Source Title", "type": "source", "url": "https://source.example.com", }, ] `); }); describe('async headers handling', () => { it('merges async config headers with sync request headers', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'json-value', body: { candidates: [ { content: { parts: [{ text: '' }], role: 'model', }, finishReason: 'STOP', index: 0, safetyRatings: SAFETY_RATINGS, }, ], promptFeedback: { safetyRatings: SAFETY_RATINGS }, usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 2, totalTokenCount: 3, }, }, }; const model = new GoogleGenerativeAILanguageModel('gemini-pro', { provider: 'google.generative-ai', baseURL: 'https://generativelanguage.googleapis.com/v1beta', headers: async () => ({ 'X-Async-Config': 'async-config-value', 'X-Common': 'config-value', }), generateId: () => 'test-id', supportedUrls: () => ({ '*': [/^https?:\/\/.*$/], }), }); await model.doGenerate({ prompt: TEST_PROMPT, headers: { 'X-Sync-Request': 'sync-request-value', 'X-Common': 'request-value', // Should override config value }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'x-async-config': 'async-config-value', 'x-sync-request': 'sync-request-value', 'x-common': 'request-value', // Request headers take precedence }); }); it('handles Promise-based headers', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'json-value', body: { candidates: [ { content: { parts: [{ text: '' }], role: 'model', }, finishReason: 'STOP', index: 0, safetyRatings: SAFETY_RATINGS, }, ], promptFeedback: { safetyRatings: SAFETY_RATINGS }, usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 2, totalTokenCount: 3, }, }, }; const model = new GoogleGenerativeAILanguageModel('gemini-pro', { provider: 'google.generative-ai', baseURL: 'https://generativelanguage.googleapis.com/v1beta', headers: async () => ({ 'X-Promise-Header': 'promise-value', }), generateId: () => 'test-id', }); await model.doGenerate({ prompt: TEST_PROMPT, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'x-promise-header': 'promise-value', }); }); it('handles async function headers from config', async () => { prepareJsonResponse({}); const model = new GoogleGenerativeAILanguageModel('gemini-pro', { provider: 'google.generative-ai', baseURL: 'https://generativelanguage.googleapis.com/v1beta', headers: async () => ({ 'X-Async-Header': 'async-value', }), generateId: () => 'test-id', }); await model.doGenerate({ prompt: TEST_PROMPT, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'x-async-header': 'async-value', }); }); }); it('should expose safety ratings in provider metadata', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'json-value', body: { candidates: [ { content: { parts: [{ text: 'test response' }], role: 'model', }, finishReason: 'STOP', index: 0, safetyRatings: [ { category: 'HARM_CATEGORY_DANGEROUS_CONTENT', probability: 'NEGLIGIBLE', probabilityScore: 0.1, severity: 'LOW', severityScore: 0.2, blocked: false, }, ], }, ], promptFeedback: { safetyRatings: SAFETY_RATINGS }, }, }; const { providerMetadata } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(providerMetadata?.google.safetyRatings).toStrictEqual([ { category: 'HARM_CATEGORY_DANGEROUS_CONTENT', probability: 'NEGLIGIBLE', probabilityScore: 0.1, severity: 'LOW', severityScore: 0.2, blocked: false, }, ]); }); it('should expose grounding metadata in provider metadata', async () => { prepareJsonResponse({ content: 'test response', groundingMetadata: { webSearchQueries: ["What's the weather in Chicago this weekend?"], searchEntryPoint: { renderedContent: 'Sample rendered content for search results', }, groundingChunks: [ { web: { uri: 'https://example.com/weather', title: 'Chicago Weather Forecast', }, }, ], groundingSupports: [ { segment: { startIndex: 0, endIndex: 65, text: 'Chicago weather changes rapidly, so layers let you adjust easily.', }, groundingChunkIndices: [0], confidenceScores: [0.99], }, ], retrievalMetadata: { webDynamicRetrievalScore: 0.96879, }, }, }); const { providerMetadata } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(providerMetadata?.google.groundingMetadata).toStrictEqual({ webSearchQueries: ["What's the weather in Chicago this weekend?"], searchEntryPoint: { renderedContent: 'Sample rendered content for search results', }, groundingChunks: [ { web: { uri: 'https://example.com/weather', title: 'Chicago Weather Forecast', }, }, ], groundingSupports: [ { segment: { startIndex: 0, endIndex: 65, text: 'Chicago weather changes rapidly, so layers let you adjust easily.', }, groundingChunkIndices: [0], confidenceScores: [0.99], }, ], retrievalMetadata: { webDynamicRetrievalScore: 0.96879, }, }); }); it('should handle code execution tool calls', async () => { server.urls[TEST_URL_GEMINI_2_0_PRO].response = { type: 'json-value', body: { candidates: [ { content: { parts: [ { executableCode: { language: 'PYTHON', code: 'print(1+1)', }, }, { codeExecutionResult: { outcome: 'OUTCOME_OK', output: '2', }, }, ], role: 'model', }, finishReason: 'STOP', }, ], }, }; const model = provider.languageModel('gemini-2.0-pro'); const { content } = await model.doGenerate({ tools: [ provider.tools.codeExecution({}) as LanguageModelV2ProviderDefinedTool, ], prompt: TEST_PROMPT, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.tools).toEqual({ codeExecution: {} }); expect(content).toEqual([ { type: 'tool-call', toolCallId: 'test-id', toolName: 'code_execution', input: '{"language":"PYTHON","code":"print(1+1)"}', providerExecuted: true, }, { type: 'tool-result', toolCallId: 'test-id', toolName: 'code_execution', result: { outcome: 'OUTCOME_OK', output: '2', }, providerExecuted: true, }, ]); }); describe('search tool selection', () => { const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key', generateId: () => 'test-id', }); it('should use googleSearch for gemini-2.0-pro', async () => { prepareJsonResponse({ url: TEST_URL_GEMINI_2_0_PRO, }); const gemini2Pro = provider.languageModel('gemini-2.0-pro'); await gemini2Pro.doGenerate({ prompt: TEST_PROMPT, tools: [ { type: 'provider-defined', id: 'google.google_search', name: 'google_search', args: {}, }, ], }); expect(await server.calls[0].requestBodyJson).toMatchObject({ tools: { googleSearch: {} }, }); }); it('should use googleSearch for gemini-2.0-flash-exp', async () => { prepareJsonResponse({ url: TEST_URL_GEMINI_2_0_FLASH_EXP, }); const gemini2Flash = provider.languageModel('gemini-2.0-flash-exp'); await gemini2Flash.doGenerate({ prompt: TEST_PROMPT, tools: [ { type: 'provider-defined', id: 'google.google_search', name: 'google_search', args: {}, }, ], }); expect(await server.calls[0].requestBodyJson).toMatchObject({ tools: { googleSearch: {} }, }); }); it('should use googleSearchRetrieval for non-gemini-2 models', async () => { prepareJsonResponse({ url: TEST_URL_GEMINI_1_0_PRO, }); const geminiPro = provider.languageModel('gemini-1.0-pro'); await geminiPro.doGenerate({ prompt: TEST_PROMPT, tools: [ { type: 'provider-defined', id: 'google.google_search', name: 'google_search', args: {}, }, ], }); expect(await server.calls[0].requestBodyJson).toMatchObject({ tools: { googleSearchRetrieval: {} }, }); }); it('should use dynamic retrieval for gemini-1-5', async () => { prepareJsonResponse({ url: TEST_URL_GEMINI_1_5_FLASH, }); const geminiPro = provider.languageModel('gemini-1.5-flash'); await geminiPro.doGenerate({ prompt: TEST_PROMPT, tools: [ { type: 'provider-defined', id: 'google.google_search', name: 'google_search', args: { mode: 'MODE_DYNAMIC', dynamicThreshold: 1, }, }, ], }); expect(await server.calls[0].requestBodyJson).toMatchObject({ tools: { googleSearchRetrieval: { dynamicRetrievalConfig: { mode: 'MODE_DYNAMIC', dynamicThreshold: 1, }, }, }, }); }); it('should use urlContextTool for gemini-2.0-pro', async () => { prepareJsonResponse({ url: TEST_URL_GEMINI_2_0_PRO, }); const gemini2Pro = provider.languageModel('gemini-2.0-pro'); await gemini2Pro.doGenerate({ prompt: TEST_PROMPT, tools: [ { type: 'provider-defined', id: 'google.url_context', name: 'url_context', args: {}, }, ], }); expect(await server.calls[0].requestBodyJson).toMatchObject({ tools: { urlContext: {} }, }); }); }); it('should extract image file outputs', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'json-value', body: { candidates: [ { content: { parts: [ { text: 'Here is an image:' }, { inlineData: { mimeType: 'image/jpeg', data: 'base64encodedimagedata', }, }, { text: 'And another image:' }, { inlineData: { mimeType: 'image/png', data: 'anotherbase64encodedimagedata', }, }, ], role: 'model', }, finishReason: 'STOP', index: 0, safetyRatings: SAFETY_RATINGS, }, ], promptFeedback: { safetyRatings: SAFETY_RATINGS }, usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 20, totalTokenCount: 30, }, }, }; const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "providerMetadata": undefined, "text": "Here is an image:", "type": "text", }, { "data": "base64encodedimagedata", "mediaType": "image/jpeg", "type": "file", }, { "providerMetadata": undefined, "text": "And another image:", "type": "text", }, { "data": "anotherbase64encodedimagedata", "mediaType": "image/png", "type": "file", }, ] `); }); it('should handle responses with only images and no text', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'json-value', body: { candidates: [ { content: { parts: [ { inlineData: { mimeType: 'image/jpeg', data: 'imagedata1', }, }, { inlineData: { mimeType: 'image/png', data: 'imagedata2', }, }, ], role: 'model', }, finishReason: 'STOP', index: 0, safetyRatings: SAFETY_RATINGS, }, ], promptFeedback: { safetyRatings: SAFETY_RATINGS }, usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 20, totalTokenCount: 30, }, }, }; const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "data": "imagedata1", "mediaType": "image/jpeg", "type": "file", }, { "data": "imagedata2", "mediaType": "image/png", "type": "file", }, ] `); }); it('should pass responseModalities in provider options', async () => { prepareJsonResponse({}); await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { google: { responseModalities: ['TEXT', 'IMAGE'], }, }, }); expect(await server.calls[0].requestBodyJson).toMatchObject({ generationConfig: { responseModalities: ['TEXT', 'IMAGE'], }, }); }); it('should include non-image inlineData parts', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'json-value', body: { candidates: [ { content: { parts: [ { text: 'Here is content:' }, { inlineData: { mimeType: 'image/jpeg', data: 'validimagedata', }, }, { inlineData: { mimeType: 'application/pdf', data: 'pdfdata', }, }, ], role: 'model', }, finishReason: 'STOP', index: 0, safetyRatings: SAFETY_RATINGS, }, ], promptFeedback: { safetyRatings: SAFETY_RATINGS }, }, }; const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "providerMetadata": undefined, "text": "Here is content:", "type": "text", }, { "data": "validimagedata", "mediaType": "image/jpeg", "type": "file", }, { "data": "pdfdata", "mediaType": "application/pdf", "type": "file", }, ] `); }); it('should correctly parse and separate reasoning parts from text output', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'json-value', body: { candidates: [ { content: { parts: [ { text: 'Visible text part 1. ' }, { text: 'This is a thought process.', thought: true }, { text: 'Visible text part 2.' }, { text: 'Another internal thought.', thought: true }, ], role: 'model', }, finishReason: 'STOP', index: 0, safetyRatings: SAFETY_RATINGS, }, ], usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 20, totalTokenCount: 30, }, }, }; const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "providerMetadata": undefined, "text": "Visible text part 1. ", "type": "text", }, { "providerMetadata": undefined, "text": "This is a thought process.", "type": "reasoning", }, { "providerMetadata": undefined, "text": "Visible text part 2.", "type": "text", }, { "providerMetadata": undefined, "text": "Another internal thought.", "type": "reasoning", }, ] `); }); it('should correctly parse thought signatures with reasoning parts', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'json-value', body: { candidates: [ { content: { parts: [ { text: 'Visible text part 1. ', thoughtSignature: 'sig1' }, { text: 'This is a thought process.', thought: true, thoughtSignature: 'sig2', }, { text: 'Visible text part 2.', thoughtSignature: 'sig3' }, ], role: 'model', }, finishReason: 'STOP', index: 0, safetyRatings: SAFETY_RATINGS, }, ], usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 20, totalTokenCount: 30, }, }, }; const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "providerMetadata": { "google": { "thoughtSignature": "sig1", }, }, "text": "Visible text part 1. ", "type": "text", }, { "providerMetadata": { "google": { "thoughtSignature": "sig2", }, }, "text": "This is a thought process.", "type": "reasoning", }, { "providerMetadata": { "google": { "thoughtSignature": "sig3", }, }, "text": "Visible text part 2.", "type": "text", }, ] `); }); it('should correctly parse thought signatures with function calls', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'json-value', body: { candidates: [ { content: { parts: [ { functionCall: { name: 'test-tool', args: { value: 'test' }, }, thoughtSignature: 'func_sig1', }, ], role: 'model', }, finishReason: 'STOP', index: 0, safetyRatings: SAFETY_RATINGS, }, ], usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 20, totalTokenCount: 30, }, }, }; const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "input": "{"value":"test"}", "providerMetadata": { "google": { "thoughtSignature": "func_sig1", }, }, "toolCallId": "test-id", "toolName": "test-tool", "type": "tool-call", }, ] `); }); describe('warnings for includeThoughts option', () => { it('should generate a warning if includeThoughts is true for a non-Vertex provider', async () => { prepareJsonResponse({ content: 'test' }); // Mock API response // Manually create a model instance to control the provider string const nonVertexModel = new GoogleGenerativeAILanguageModel('gemini-pro', { provider: 'google.generative-ai.chat', // Simulate non-Vertex provider baseURL: 'https://generativelanguage.googleapis.com/v1beta', headers: {}, generateId: () => 'test-id', supportedUrls: () => ({}), // Dummy implementation }); const { warnings } = await nonVertexModel.doGenerate({ prompt: TEST_PROMPT, providerOptions: { google: { thinkingConfig: { includeThoughts: true, thinkingBudget: 500, }, }, }, }); expect(warnings).toMatchInlineSnapshot(` [ { "message": "The 'includeThoughts' option is only supported with the Google Vertex provider and might not be supported or could behave unexpectedly with the current Google provider (google.generative-ai.chat).", "type": "other", }, ] `); }); it('should NOT generate a warning if includeThoughts is true for a Vertex provider', async () => { prepareJsonResponse({ content: 'test' }); // Mock API response const vertexModel = new GoogleGenerativeAILanguageModel('gemini-pro', { provider: 'google.vertex.chat', // Simulate Vertex provider baseURL: 'https://generativelanguage.googleapis.com/v1beta', headers: {}, generateId: () => 'test-id', supportedUrls: () => ({}), }); const { warnings } = await vertexModel.doGenerate({ prompt: TEST_PROMPT, providerOptions: { google: { thinkingConfig: { includeThoughts: true, thinkingBudget: 500, }, }, }, }); expect(warnings).toMatchInlineSnapshot(`[]`); }); it('should NOT generate a warning if includeThoughts is false for a non-Vertex provider', async () => { prepareJsonResponse({ content: 'test' }); // Mock API response const nonVertexModel = new GoogleGenerativeAILanguageModel('gemini-pro', { provider: 'google.generative-ai.chat', // Simulate non-Vertex provider baseURL: 'https://generativelanguage.googleapis.com/v1beta', headers: {}, generateId: () => 'test-id', supportedUrls: () => ({}), }); const { warnings } = await nonVertexModel.doGenerate({ prompt: TEST_PROMPT, providerOptions: { google: { thinkingConfig: { includeThoughts: false, thinkingBudget: 500, }, }, }, }); expect(warnings).toMatchInlineSnapshot(`[]`); }); it('should NOT generate a warning if thinkingConfig is not provided for a non-Vertex provider', async () => { prepareJsonResponse({ content: 'test' }); // Mock API response const nonVertexModel = new GoogleGenerativeAILanguageModel('gemini-pro', { provider: 'google.generative-ai.chat', // Simulate non-Vertex provider baseURL: 'https://generativelanguage.googleapis.com/v1beta', headers: {}, generateId: () => 'test-id', supportedUrls: () => ({}), }); const { warnings } = await nonVertexModel.doGenerate({ prompt: TEST_PROMPT, providerOptions: { google: { // No thinkingConfig }, }, }); expect(warnings).toMatchInlineSnapshot(`[]`); }); }); }); describe('doStream', () => { const TEST_URL_GEMINI_PRO = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:streamGenerateContent'; const TEST_URL_GEMINI_2_0_PRO = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-pro:streamGenerateContent'; const TEST_URL_GEMINI_2_0_FLASH_EXP = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:streamGenerateContent'; const TEST_URL_GEMINI_1_0_PRO = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.0-pro:streamGenerateContent'; const TEST_URL_GEMINI_1_5_FLASH = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:streamGenerateContent'; const server = createTestServer({ [TEST_URL_GEMINI_PRO]: {}, [TEST_URL_GEMINI_2_0_PRO]: {}, [TEST_URL_GEMINI_2_0_FLASH_EXP]: {}, [TEST_URL_GEMINI_1_0_PRO]: {}, [TEST_URL_GEMINI_1_5_FLASH]: {}, }); const prepareStreamResponse = ({ content, headers, groundingMetadata, urlContextMetadata, url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:streamGenerateContent', }: { content: string[]; headers?: Record<string, string>; groundingMetadata?: GoogleGenerativeAIGroundingMetadata; urlContextMetadata?: GoogleGenerativeAIUrlContextMetadata; url?: | typeof TEST_URL_GEMINI_PRO | typeof TEST_URL_GEMINI_2_0_PRO | typeof TEST_URL_GEMINI_2_0_FLASH_EXP | typeof TEST_URL_GEMINI_1_0_PRO | typeof TEST_URL_GEMINI_1_5_FLASH; }) => { server.urls[url].response = { headers, type: 'stream-chunks', chunks: content.map( (text, index) => `data: ${JSON.stringify({ candidates: [ { content: { parts: [{ text }], role: 'model' }, finishReason: 'STOP', index: 0, safetyRatings: SAFETY_RATINGS, ...(groundingMetadata && { groundingMetadata }), ...(urlContextMetadata && { urlContextMetadata }), }, ], // Include usage metadata only in the last chunk ...(index === content.length - 1 && { usageMetadata: { promptTokenCount: 294, candidatesTokenCount: 233, totalTokenCount: 527, }, }), })}\n\n`, ), }; }; it('should expose grounding metadata in provider metadata on finish', async () => { prepareStreamResponse({ content: ['test'], groundingMetadata: { webSearchQueries: ["What's the weather in Chicago this weekend?"], searchEntryPoint: { renderedContent: 'Sample rendered content for search results', }, groundingChunks: [ { web: { uri: 'https://example.com/weather', title: 'Chicago Weather Forecast', }, }, ], groundingSupports: [ { segment: { startIndex: 0, endIndex: 65, text: 'Chicago weather changes rapidly, so layers let you adjust easily.', }, groundingChunkIndices: [0], confidenceScores: [0.99], }, ], retrievalMetadata: { webDynamicRetrievalScore: 0.96879, }, }, }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const events = await convertReadableStreamToArray(stream); const finishEvent = events.find(event => event.type === 'finish'); expect( finishEvent?.type === 'finish' && finishEvent.providerMetadata?.google.groundingMetadata, ).toStrictEqual({ webSearchQueries: ["What's the weather in Chicago this weekend?"], searchEntryPoint: { renderedContent: 'Sample rendered content for search results', }, groundingChunks: [ { web: { uri: 'https://example.com/weather', title: 'Chicago Weather Forecast', }, }, ], groundingSupports: [ { segment: { startIndex: 0, endIndex: 65, text: 'Chicago weather changes rapidly, so layers let you adjust easily.', }, groundingChunkIndices: [0], confidenceScores: [0.99], }, ], retrievalMetadata: { webDynamicRetrievalScore: 0.96879, }, }); }); it('should expose url context metadata in provider metadata on finish', async () => { prepareStreamResponse({ content: ['test'], urlContextMetadata: { urlMetadata: [ { retrievedUrl: 'https://example.com/weather', urlRetrievalStatus: 'URL_RETRIEVAL_STATUS_SUCCESS', }, ], }, }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const events = await convertReadableStreamToArray(stream); const finishEvent = events.find(event => event.type === 'finish'); expect( finishEvent?.type === 'finish' && finishEvent.providerMetadata?.google.urlContextMetadata, ).toStrictEqual({ urlMetadata: [ { retrievedUrl: 'https://example.com/weather', urlRetrievalStatus: 'URL_RETRIEVAL_STATUS_SUCCESS', }, ], }); }); it('should stream text deltas', async () => { prepareStreamResponse({ content: ['Hello', ', ', 'world!'] }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "0", "providerMetadata": undefined, "type": "text-start", }, { "delta": "Hello", "id": "0", "providerMetadata": undefined, "type": "text-delta", }, { "delta": ", ", "id": "0", "providerMetadata": undefined, "type": "text-delta", }, { "delta": "world!", "id": "0", "providerMetadata": undefined, "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "google": { "groundingMetadata": null, "safetyRatings": [ { "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability": "NEGLIGIBLE", }, { "category": "HARM_CATEGORY_HATE_SPEECH", "probability": "NEGLIGIBLE", }, { "category": "HARM_CATEGORY_HARASSMENT", "probability": "NEGLIGIBLE", }, { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "probability": "NEGLIGIBLE", }, ], "urlContextMetadata": null, "usageMetadata": { "candidatesTokenCount": 233, "promptTokenCount": 294, "totalTokenCount": 527, }, }, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 294, "outputTokens": 233, "reasoningTokens": undefined, "totalTokens": 527, }, }, ] `); }); it('should expose safety ratings in provider metadata on finish', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'stream-chunks', chunks: [ `data: {"candidates": [{"content": {"parts": [{"text": "test"}],"role": "model"},` + `"finishReason": "STOP","index": 0,"safetyRatings": [` + `{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE",` + `"probabilityScore": 0.1,"severity": "LOW","severityScore": 0.2,"blocked": false}]}]}\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const events = await convertReadableStreamToArray(stream); const finishEvent = events.find(event => event.type === 'finish'); expect( finishEvent?.type === 'finish' && finishEvent.providerMetadata?.google.safetyRatings, ).toStrictEqual([ { category: 'HARM_CATEGORY_DANGEROUS_CONTENT', probability: 'NEGLIGIBLE', probabilityScore: 0.1, severity: 'LOW', severityScore: 0.2, blocked: false, }, ]); }); it('should stream code execution tool calls and results', async () => { server.urls[TEST_URL_GEMINI_2_0_PRO].response = { type: 'stream-chunks', chunks: [ `data: ${JSON.stringify({ candidates: [ { content: { parts: [ { executableCode: { language: 'PYTHON', code: 'print("hello")', }, }, ], }, }, ], })}\n\n`, `data: ${JSON.stringify({ candidates: [ { content: { parts: [ { codeExecutionResult: { outcome: 'OUTCOME_OK', output: 'hello\n', }, }, ], }, finishReason: 'STOP', }, ], })}\n\n`, ], }; const model = provider.languageModel('gemini-2.0-pro'); const { stream } = await model.doStream({ tools: [ provider.tools.codeExecution({}) as LanguageModelV2ProviderDefinedTool, ], prompt: TEST_PROMPT, }); const events = await convertReadableStreamToArray(stream); const toolEvents = events.filter( e => e.type === 'tool-call' || e.type === 'tool-result', ); expect(toolEvents).toEqual([ { type: 'tool-call', toolCallId: 'test-id', toolName: 'code_execution', input: '{"language":"PYTHON","code":"print(\\"hello\\")"}', providerExecuted: true, }, { type: 'tool-result', toolCallId: 'test-id', toolName: 'code_execution', result: { outcome: 'OUTCOME_OK', output: 'hello\n', }, providerExecuted: true, }, ]); }); describe('search tool selection', () => { const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key', generateId: () => 'test-id', }); it('should use googleSearch for gemini-2.0-pro', async () => { prepareStreamResponse({ content: [''], url: TEST_URL_GEMINI_2_0_PRO, }); const gemini2Pro = provider.languageModel('gemini-2.0-pro'); await gemini2Pro.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, tools: [ { type: 'provider-defined', id: 'google.google_search', name: 'google_search', args: {}, }, ], }); expect(await server.calls[0].requestBodyJson).toMatchObject({ tools: { googleSearch: {} }, }); }); it('should use googleSearch for gemini-2.0-flash-exp', async () => { prepareStreamResponse({ content: [''], url: TEST_URL_GEMINI_2_0_FLASH_EXP, }); const gemini2Flash = provider.languageModel('gemini-2.0-flash-exp'); await gemini2Flash.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, tools: [ { type: 'provider-defined', id: 'google.google_search', name: 'google_search', args: {}, }, ], }); expect(await server.calls[0].requestBodyJson).toMatchObject({ tools: { googleSearch: {} }, }); }); it('should use googleSearchRetrieval for non-gemini-2 models', async () => { prepareStreamResponse({ content: [''], url: TEST_URL_GEMINI_1_0_PRO, }); const geminiPro = provider.languageModel('gemini-1.0-pro'); await geminiPro.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, tools: [ { type: 'provider-defined', id: 'google.google_search', name: 'google_search', args: {}, }, ], }); expect(await server.calls[0].requestBodyJson).toMatchObject({ tools: { googleSearchRetrieval: {} }, }); }); it('should use dynamic retrieval for gemini-1-5', async () => { prepareStreamResponse({ content: [''], url: TEST_URL_GEMINI_1_5_FLASH, }); const geminiPro = provider.languageModel('gemini-1.5-flash'); await geminiPro.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, tools: [ { type: 'provider-defined', id: 'google.google_search', name: 'google_search', args: { mode: 'MODE_DYNAMIC', dynamicThreshold: 1, }, }, ], }); expect(await server.calls[0].requestBodyJson).toMatchObject({ tools: { googleSearchRetrieval: { dynamicRetrievalConfig: { mode: 'MODE_DYNAMIC', dynamicThreshold: 1, }, }, }, }); }); }); it('should stream source events', async () => { prepareStreamResponse({ content: ['Some initial text'], groundingMetadata: { groundingChunks: [ { web: { uri: 'https://source.example.com', title: 'Source Title', }, }, ], }, }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const events = await convertReadableStreamToArray(stream); const sourceEvents = events.filter(event => event.type === 'source'); expect(sourceEvents).toMatchInlineSnapshot(` [ { "id": "test-id", "sourceType": "url", "title": "Source Title", "type": "source", "url": "https://source.example.com", }, ] `); }); it('should stream sources during intermediate chunks', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'stream-chunks', chunks: [ `data: ${JSON.stringify({ candidates: [ { content: { parts: [{ text: 'text' }], role: 'model' }, index: 0, safetyRatings: SAFETY_RATINGS, groundingMetadata: { groundingChunks: [ { web: { uri: 'https://a.com', title: 'A' } }, { web: { uri: 'https://b.com', title: 'B' } }, ], }, }, ], })}\n\n`, `data: ${JSON.stringify({ candidates: [ { content: { parts: [{ text: 'more' }], role: 'model' }, finishReason: 'STOP', index: 0, safetyRatings: SAFETY_RATINGS, }, ], })}\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const events = await convertReadableStreamToArray(stream); const sourceEvents = events.filter(event => event.type === 'source'); expect(sourceEvents).toMatchInlineSnapshot(` [ { "id": "test-id", "sourceType": "url", "title": "A", "type": "source", "url": "https://a.com", }, { "id": "test-id", "sourceType": "url", "title": "B", "type": "source", "url": "https://b.com", }, ] `); }); it('should deduplicate sources across chunks', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'stream-chunks', chunks: [ `data: ${JSON.stringify({ candidates: [ { content: { parts: [{ text: 'first chunk' }], role: 'model' }, index: 0, safetyRatings: SAFETY_RATINGS, groundingMetadata: { groundingChunks: [ { web: { uri: 'https://example.com', title: 'Example' } }, { web: { uri: 'https://unique.com', title: 'Unique' } }, ], }, }, ], })}\n\n`, `data: ${JSON.stringify({ candidates: [ { content: { parts: [{ text: 'second chunk' }], role: 'model' }, index: 0, safetyRatings: SAFETY_RATINGS, groundingMetadata: { groundingChunks: [ { web: { uri: 'https://example.com', title: 'Example Duplicate', }, }, { web: { uri: 'https://another.com', title: 'Another' } }, ], }, }, ], })}\n\n`, `data: ${JSON.stringify({ candidates: [ { content: { parts: [{ text: 'final chunk' }], role: 'model' }, finishReason: 'STOP', index: 0, safetyRatings: SAFETY_RATINGS, }, ], })}\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const events = await convertReadableStreamToArray(stream); const sourceEvents = events.filter(event => event.type === 'source'); expect(sourceEvents).toMatchInlineSnapshot(` [ { "id": "test-id", "sourceType": "url", "title": "Example", "type": "source", "url": "https://example.com", }, { "id": "test-id", "sourceType": "url", "title": "Unique", "type": "source", "url": "https://unique.com", }, { "id": "test-id", "sourceType": "url", "title": "Another", "type": "source", "url": "https://another.com", }, ] `); }); it('should stream files', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'stream-chunks', chunks: [ `data: {"candidates": [{"content": {"parts": [{"inlineData": {"data": "test","mimeType": "text/plain"}}]` + `,"role": "model"},` + `"finishReason": "STOP","index": 0,"safetyRatings": [` + `{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},` + `{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},` + `{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},` + `{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]}\n\n`, `data: {"usageMetadata": {"promptTokenCount": 294,"candidatesTokenCount": 233,"totalTokenCount": 527}}\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const events = await convertReadableStreamToArray(stream); expect(events).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "data": "test", "mediaType": "text/plain", "type": "file", }, { "finishReason": "stop", "providerMetadata": { "google": { "groundingMetadata": null, "safetyRatings": [ { "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability": "NEGLIGIBLE", }, { "category": "HARM_CATEGORY_HATE_SPEECH", "probability": "NEGLIGIBLE", }, { "category": "HARM_CATEGORY_HARASSMENT", "probability": "NEGLIGIBLE", }, { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "probability": "NEGLIGIBLE", }, ], "urlContextMetadata": null, }, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 294, "outputTokens": 233, "reasoningTokens": undefined, "totalTokens": 527, }, }, ] `); }); it('should set finishReason to tool-calls when chunk contains functionCall', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'stream-chunks', chunks: [ `data: ${JSON.stringify({ candidates: [ { content: { parts: [{ text: 'Initial text response' }], role: 'model', }, index: 0, safetyRatings: SAFETY_RATINGS, }, ], })}\n\n`, `data: ${JSON.stringify({ candidates: [ { content: { parts: [ { functionCall: { name: 'test-tool', args: { value: 'example value' }, }, }, ], role: 'model', }, finishReason: 'STOP', index: 0, safetyRatings: SAFETY_RATINGS, }, ], usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 20, totalTokenCount: 30, }, })}\n\n`, ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, includeRawChunks: false, }); const events = await convertReadableStreamToArray(stream); const finishEvent = events.find(event => event.type === 'finish'); expect(finishEvent?.type === 'finish' && finishEvent.finishReason).toEqual( 'tool-calls', ); }); it('should only pass valid provider options', async () => { prepareStreamResponse({ content: [''] }); await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, providerOptions: { google: { foo: 'bar', responseModalities: ['TEXT', 'IMAGE'] }, }, }); expect(await server.calls[0].requestBodyJson).toMatchObject({ contents: [ { role: 'user', parts: [{ text: 'Hello' }], }, ], generationConfig: { responseModalities: ['TEXT', 'IMAGE'], }, }); }); it('should stream reasoning parts separately from text parts', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'stream-chunks', chunks: [ `data: ${JSON.stringify({ candidates: [ { content: { parts: [ { text: 'I need to think about this carefully. The user wants a simple explanation.', thought: true, }, ], role: 'model', }, index: 0, }, ], usageMetadata: { promptTokenCount: 14, totalTokenCount: 84, thoughtsTokenCount: 70, }, })}\n\n`, `data: ${JSON.stringify({ candidates: [ { content: { parts: [ { text: 'Let me organize my thoughts and provide a clear answer.', thought: true, }, ], role: 'model', }, index: 0, }, ], usageMetadata: { promptTokenCount: 14, totalTokenCount: 156, thoughtsTokenCount: 142, }, })}\n\n`, `data: ${JSON.stringify({ candidates: [ { content: { parts: [ { text: 'Here is a simple explanation: ', }, ], role: 'model', }, index: 0, }, ], usageMetadata: { promptTokenCount: 14, candidatesTokenCount: 8, totalTokenCount: 164, thoughtsTokenCount: 142, }, })}\n\n`, `data: ${JSON.stringify({ candidates: [ { content: { parts: [ { text: 'The concept works because of basic principles.', }, ], role: 'model', }, finishReason: 'STOP', index: 0, }, ], usageMetadata: { promptTokenCount: 14, candidatesTokenCount: 18, totalTokenCount: 174, thoughtsTokenCount: 142, }, })}\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const allEvents = await convertReadableStreamToArray(stream); expect(allEvents).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "0", "providerMetadata": undefined, "type": "reasoning-start", }, { "delta": "I need to think about this carefully. The user wants a simple explanation.", "id": "0", "providerMetadata": undefined, "type": "reasoning-delta", }, { "delta": "Let me organize my thoughts and provide a clear answer.", "id": "0", "providerMetadata": undefined, "type": "reasoning-delta", }, { "id": "0", "type": "reasoning-end", }, { "id": "1", "providerMetadata": undefined, "type": "text-start", }, { "delta": "Here is a simple explanation: ", "id": "1", "providerMetadata": undefined, "type": "text-delta", }, { "delta": "The concept works because of basic principles.", "id": "1", "providerMetadata": undefined, "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "google": { "groundingMetadata": null, "safetyRatings": null, "urlContextMetadata": null, "usageMetadata": { "candidatesTokenCount": 18, "promptTokenCount": 14, "thoughtsTokenCount": 142, "totalTokenCount": 174, }, }, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 14, "outputTokens": 18, "reasoningTokens": 142, "totalTokens": 174, }, }, ] `); }); it('should stream thought signatures with reasoning and text parts', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'stream-chunks', chunks: [ `data: ${JSON.stringify({ candidates: [ { content: { parts: [ { text: 'I need to think about this.', thought: true, thoughtSignature: 'reasoning_sig1', }, ], role: 'model', }, index: 0, }, ], })}\n\n`, `data: ${JSON.stringify({ candidates: [ { content: { parts: [ { text: 'Here is the answer.', thoughtSignature: 'text_sig1', }, ], role: 'model', }, index: 0, finishReason: 'STOP', safetyRatings: SAFETY_RATINGS, }, ], })}\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "0", "providerMetadata": { "google": { "thoughtSignature": "reasoning_sig1", }, }, "type": "reasoning-start", }, { "delta": "I need to think about this.", "id": "0", "providerMetadata": { "google": { "thoughtSignature": "reasoning_sig1", }, }, "type": "reasoning-delta", }, { "id": "0", "type": "reasoning-end", }, { "id": "1", "providerMetadata": { "google": { "thoughtSignature": "text_sig1", }, }, "type": "text-start", }, { "delta": "Here is the answer.", "id": "1", "providerMetadata": { "google": { "thoughtSignature": "text_sig1", }, }, "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "google": { "groundingMetadata": null, "safetyRatings": [ { "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability": "NEGLIGIBLE", }, { "category": "HARM_CATEGORY_HATE_SPEECH", "probability": "NEGLIGIBLE", }, { "category": "HARM_CATEGORY_HARASSMENT", "probability": "NEGLIGIBLE", }, { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "probability": "NEGLIGIBLE", }, ], "urlContextMetadata": null, }, }, "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); describe('raw chunks', () => { it('should include raw chunks when includeRawChunks is enabled', async () => { prepareStreamResponse({ content: ['Hello', ' World!'], }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: true, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks.filter(chunk => chunk.type === 'raw')) .toMatchInlineSnapshot(` [ { "rawValue": { "candidates": [ { "content": { "parts": [ { "text": "Hello", }, ], "role": "model", }, "finishReason": "STOP", "index": 0, "safetyRatings": [ { "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability": "NEGLIGIBLE", }, { "category": "HARM_CATEGORY_HATE_SPEECH", "probability": "NEGLIGIBLE", }, { "category": "HARM_CATEGORY_HARASSMENT", "probability": "NEGLIGIBLE", }, { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "probability": "NEGLIGIBLE", }, ], }, ], }, "type": "raw", }, { "rawValue": { "candidates": [ { "content": { "parts": [ { "text": " World!", }, ], "role": "model", }, "finishReason": "STOP", "index": 0, "safetyRatings": [ { "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability": "NEGLIGIBLE", }, { "category": "HARM_CATEGORY_HATE_SPEECH", "probability": "NEGLIGIBLE", }, { "category": "HARM_CATEGORY_HARASSMENT", "probability": "NEGLIGIBLE", }, { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "probability": "NEGLIGIBLE", }, ], }, ], "usageMetadata": { "candidatesTokenCount": 233, "promptTokenCount": 294, "totalTokenCount": 527, }, }, "type": "raw", }, ] `); }); it('should not include raw chunks when includeRawChunks is false', async () => { prepareStreamResponse({ content: ['Hello', ' World!'], }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks.filter(chunk => chunk.type === 'raw')).toHaveLength(0); }); }); }); describe('GEMMA Model System Instruction Fix', () => { const TEST_PROMPT_WITH_SYSTEM: LanguageModelV2Prompt = [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; const TEST_URL_GEMMA_3_12B_IT = 'https://generativelanguage.googleapis.com/v1beta/models/gemma-3-12b-it:generateContent'; const TEST_URL_GEMMA_3_27B_IT = 'https://generativelanguage.googleapis.com/v1beta/models/gemma-3-27b-it:generateContent'; const TEST_URL_GEMINI_PRO = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent'; const server = createTestServer({ [TEST_URL_GEMMA_3_12B_IT]: {}, [TEST_URL_GEMMA_3_27B_IT]: {}, [TEST_URL_GEMINI_PRO]: {}, }); it('should NOT send systemInstruction for GEMMA-3-12b-it model', async () => { server.urls[TEST_URL_GEMMA_3_12B_IT].response = { type: 'json-value', body: { candidates: [ { content: { parts: [{ text: 'Hello!' }], role: 'model' }, finishReason: 'STOP', index: 0, }, ], }, }; const model = new GoogleGenerativeAILanguageModel('gemma-3-12b-it', { provider: 'google.generative-ai', baseURL: 'https://generativelanguage.googleapis.com/v1beta', headers: { 'x-goog-api-key': 'test-api-key' }, generateId: () => 'test-id', }); await model.doGenerate({ prompt: TEST_PROMPT_WITH_SYSTEM, }); // Verify that systemInstruction was NOT sent for GEMMA model const lastCall = server.calls[server.calls.length - 1]; const requestBody = await lastCall.requestBodyJson; expect(requestBody).not.toHaveProperty('systemInstruction'); }); it('should NOT send systemInstruction for GEMMA-3-27b-it model', async () => { server.urls[TEST_URL_GEMMA_3_27B_IT].response = { type: 'json-value', body: { candidates: [ { content: { parts: [{ text: 'Hello!' }], role: 'model' }, finishReason: 'STOP', index: 0, }, ], }, }; const model = new GoogleGenerativeAILanguageModel('gemma-3-27b-it', { provider: 'google.generative-ai', baseURL: 'https://generativelanguage.googleapis.com/v1beta', headers: { 'x-goog-api-key': 'test-api-key' }, generateId: () => 'test-id', }); await model.doGenerate({ prompt: TEST_PROMPT_WITH_SYSTEM, }); const lastCall = server.calls[server.calls.length - 1]; const requestBody = await lastCall.requestBodyJson; expect(requestBody).not.toHaveProperty('systemInstruction'); }); it('should still send systemInstruction for Gemini models (regression test)', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'json-value', body: { candidates: [ { content: { parts: [{ text: 'Hello!' }], role: 'model' }, finishReason: 'STOP', index: 0, }, ], }, }; const model = new GoogleGenerativeAILanguageModel('gemini-pro', { provider: 'google.generative-ai', baseURL: 'https://generativelanguage.googleapis.com/v1beta', headers: { 'x-goog-api-key': 'test-api-key' }, generateId: () => 'test-id', }); await model.doGenerate({ prompt: TEST_PROMPT_WITH_SYSTEM, }); const lastCall = server.calls[server.calls.length - 1]; const requestBody = await lastCall.requestBodyJson; expect(requestBody).toHaveProperty('systemInstruction'); expect(requestBody.systemInstruction).toEqual({ parts: [{ text: 'You are a helpful assistant.' }], }); }); it('should NOT generate warning when GEMMA model is used without system instructions', async () => { server.urls[TEST_URL_GEMMA_3_12B_IT].response = { type: 'json-value', body: { candidates: [ { content: { parts: [{ text: 'Hello!' }], role: 'model' }, finishReason: 'STOP', index: 0, }, ], }, }; const model = new GoogleGenerativeAILanguageModel('gemma-3-12b-it', { provider: 'google.generative-ai', baseURL: 'https://generativelanguage.googleapis.com/v1beta', headers: { 'x-goog-api-key': 'test-api-key' }, generateId: () => 'test-id', }); const TEST_PROMPT_WITHOUT_SYSTEM: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; const { warnings } = await model.doGenerate({ prompt: TEST_PROMPT_WITHOUT_SYSTEM, }); expect(warnings).toHaveLength(0); }); it('should NOT generate warning when Gemini model is used with system instructions', async () => { server.urls[TEST_URL_GEMINI_PRO].response = { type: 'json-value', body: { candidates: [ { content: { parts: [{ text: 'Hello!' }], role: 'model' }, finishReason: 'STOP', index: 0, }, ], }, }; const model = new GoogleGenerativeAILanguageModel('gemini-pro', { provider: 'google.generative-ai', baseURL: 'https://generativelanguage.googleapis.com/v1beta', headers: { 'x-goog-api-key': 'test-api-key' }, generateId: () => 'test-id', }); const { warnings } = await model.doGenerate({ prompt: TEST_PROMPT_WITH_SYSTEM, }); expect(warnings).toHaveLength(0); }); it('should prepend system instruction to first user message for GEMMA models', async () => { server.urls[TEST_URL_GEMMA_3_12B_IT].response = { type: 'json-value', body: { candidates: [ { content: { parts: [{ text: 'Hello!' }], role: 'model' }, finishReason: 'STOP', index: 0, }, ], }, }; const model = new GoogleGenerativeAILanguageModel('gemma-3-12b-it', { provider: 'google.generative-ai', baseURL: 'https://generativelanguage.googleapis.com/v1beta', headers: { 'x-goog-api-key': 'test-api-key' }, generateId: () => 'test-id', }); await model.doGenerate({ prompt: TEST_PROMPT_WITH_SYSTEM, }); const lastCall = server.calls[server.calls.length - 1]; const requestBody = await lastCall.requestBodyJson; expect(requestBody).toMatchInlineSnapshot(` { "contents": [ { "parts": [ { "text": "You are a helpful assistant. ", }, { "text": "Hello", }, ], "role": "user", }, ], "generationConfig": {}, } `); }); }); --- File: /ai/packages/google/src/google-generative-ai-language-model.ts --- import { LanguageModelV2, LanguageModelV2CallWarning, LanguageModelV2Content, LanguageModelV2FinishReason, LanguageModelV2Source, LanguageModelV2StreamPart, LanguageModelV2Usage, SharedV2ProviderMetadata, } from '@ai-sdk/provider'; import { FetchFunction, ParseResult, Resolvable, combineHeaders, createEventSourceResponseHandler, createJsonResponseHandler, generateId, parseProviderOptions, postJsonToApi, resolve, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { convertJSONSchemaToOpenAPISchema } from './convert-json-schema-to-openapi-schema'; import { convertToGoogleGenerativeAIMessages } from './convert-to-google-generative-ai-messages'; import { getModelPath } from './get-model-path'; import { googleFailedResponseHandler } from './google-error'; import { GoogleGenerativeAIContentPart } from './google-generative-ai-prompt'; import { GoogleGenerativeAIModelId, googleGenerativeAIProviderOptions, } from './google-generative-ai-options'; import { prepareTools } from './google-prepare-tools'; import { mapGoogleGenerativeAIFinishReason } from './map-google-generative-ai-finish-reason'; import { groundingChunkSchema, groundingMetadataSchema, } from './tool/google-search'; import { urlContextMetadataSchema } from './tool/url-context'; type GoogleGenerativeAIConfig = { provider: string; baseURL: string; headers: Resolvable<Record<string, string | undefined>>; fetch?: FetchFunction; generateId: () => string; /** * The supported URLs for the model. */ supportedUrls?: () => LanguageModelV2['supportedUrls']; }; export class GoogleGenerativeAILanguageModel implements LanguageModelV2 { readonly specificationVersion = 'v2'; readonly modelId: GoogleGenerativeAIModelId; private readonly config: GoogleGenerativeAIConfig; private readonly generateId: () => string; constructor( modelId: GoogleGenerativeAIModelId, config: GoogleGenerativeAIConfig, ) { this.modelId = modelId; this.config = config; this.generateId = config.generateId ?? generateId; } get provider(): string { return this.config.provider; } get supportedUrls() { return this.config.supportedUrls?.() ?? {}; } private async getArgs({ prompt, maxOutputTokens, temperature, topP, topK, frequencyPenalty, presencePenalty, stopSequences, responseFormat, seed, tools, toolChoice, providerOptions, }: Parameters<LanguageModelV2['doGenerate']>[0]) { const warnings: LanguageModelV2CallWarning[] = []; const googleOptions = await parseProviderOptions({ provider: 'google', providerOptions, schema: googleGenerativeAIProviderOptions, }); // Add warning if includeThoughts is used with a non-Vertex Google provider if ( googleOptions?.thinkingConfig?.includeThoughts === true && !this.config.provider.startsWith('google.vertex.') ) { warnings.push({ type: 'other', message: "The 'includeThoughts' option is only supported with the Google Vertex provider " + 'and might not be supported or could behave unexpectedly with the current Google provider ' + `(${this.config.provider}).`, }); } const isGemmaModel = this.modelId.toLowerCase().startsWith('gemma-'); const { contents, systemInstruction } = convertToGoogleGenerativeAIMessages( prompt, { isGemmaModel }, ); const { tools: googleTools, toolConfig: googleToolConfig, toolWarnings, } = prepareTools({ tools, toolChoice, modelId: this.modelId, }); return { args: { generationConfig: { // standardized settings: maxOutputTokens, temperature, topK, topP, frequencyPenalty, presencePenalty, stopSequences, seed, // response format: responseMimeType: responseFormat?.type === 'json' ? 'application/json' : undefined, responseSchema: responseFormat?.type === 'json' && responseFormat.schema != null && // Google GenAI does not support all OpenAPI Schema features, // so this is needed as an escape hatch: // TODO convert into provider option (googleOptions?.structuredOutputs ?? true) ? convertJSONSchemaToOpenAPISchema(responseFormat.schema) : undefined, ...(googleOptions?.audioTimestamp && { audioTimestamp: googleOptions.audioTimestamp, }), // provider options: responseModalities: googleOptions?.responseModalities, thinkingConfig: googleOptions?.thinkingConfig, }, contents, systemInstruction: isGemmaModel ? undefined : systemInstruction, safetySettings: googleOptions?.safetySettings, tools: googleTools, toolConfig: googleToolConfig, cachedContent: googleOptions?.cachedContent, }, warnings: [...warnings, ...toolWarnings], }; } async doGenerate( options: Parameters<LanguageModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>> { const { args, warnings } = await this.getArgs(options); const body = JSON.stringify(args); const mergedHeaders = combineHeaders( await resolve(this.config.headers), options.headers, ); const { responseHeaders, value: response, rawValue: rawResponse, } = await postJsonToApi({ url: `${this.config.baseURL}/${getModelPath( this.modelId, )}:generateContent`, headers: mergedHeaders, body: args, failedResponseHandler: googleFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler(responseSchema), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const candidate = response.candidates[0]; const content: Array<LanguageModelV2Content> = []; // map ordered parts to content: const parts = candidate.content?.parts ?? []; const usageMetadata = response.usageMetadata; // Associates a code execution result with its preceding call. let lastCodeExecutionToolCallId: string | undefined; // Build content array from all parts for (const part of parts) { if ('executableCode' in part && part.executableCode?.code) { const toolCallId = this.config.generateId(); lastCodeExecutionToolCallId = toolCallId; content.push({ type: 'tool-call', toolCallId, toolName: 'code_execution', input: JSON.stringify(part.executableCode), providerExecuted: true, }); } else if ('codeExecutionResult' in part && part.codeExecutionResult) { content.push({ type: 'tool-result', // Assumes a result directly follows its corresponding call part. toolCallId: lastCodeExecutionToolCallId!, toolName: 'code_execution', result: { outcome: part.codeExecutionResult.outcome, output: part.codeExecutionResult.output, }, providerExecuted: true, }); // Clear the ID after use to avoid accidental reuse. lastCodeExecutionToolCallId = undefined; } else if ('text' in part && part.text != null && part.text.length > 0) { content.push({ type: part.thought === true ? 'reasoning' : 'text', text: part.text, providerMetadata: part.thoughtSignature ? { google: { thoughtSignature: part.thoughtSignature } } : undefined, }); } else if ('functionCall' in part) { content.push({ type: 'tool-call' as const, toolCallId: this.config.generateId(), toolName: part.functionCall.name, input: JSON.stringify(part.functionCall.args), providerMetadata: part.thoughtSignature ? { google: { thoughtSignature: part.thoughtSignature } } : undefined, }); } else if ('inlineData' in part) { content.push({ type: 'file' as const, data: part.inlineData.data, mediaType: part.inlineData.mimeType, }); } } const sources = extractSources({ groundingMetadata: candidate.groundingMetadata, generateId: this.config.generateId, }) ?? []; for (const source of sources) { content.push(source); } return { content, finishReason: mapGoogleGenerativeAIFinishReason({ finishReason: candidate.finishReason, hasToolCalls: content.some(part => part.type === 'tool-call'), }), usage: { inputTokens: usageMetadata?.promptTokenCount ?? undefined, outputTokens: usageMetadata?.candidatesTokenCount ?? undefined, totalTokens: usageMetadata?.totalTokenCount ?? undefined, reasoningTokens: usageMetadata?.thoughtsTokenCount ?? undefined, cachedInputTokens: usageMetadata?.cachedContentTokenCount ?? undefined, }, warnings, providerMetadata: { google: { groundingMetadata: candidate.groundingMetadata ?? null, urlContextMetadata: candidate.urlContextMetadata ?? null, safetyRatings: candidate.safetyRatings ?? null, usageMetadata: usageMetadata ?? null, }, }, request: { body }, response: { // TODO timestamp, model id, id headers: responseHeaders, body: rawResponse, }, }; } async doStream( options: Parameters<LanguageModelV2['doStream']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>> { const { args, warnings } = await this.getArgs(options); const body = JSON.stringify(args); const headers = combineHeaders( await resolve(this.config.headers), options.headers, ); const { responseHeaders, value: response } = await postJsonToApi({ url: `${this.config.baseURL}/${getModelPath( this.modelId, )}:streamGenerateContent?alt=sse`, headers, body: args, failedResponseHandler: googleFailedResponseHandler, successfulResponseHandler: createEventSourceResponseHandler(chunkSchema), abortSignal: options.abortSignal, fetch: this.config.fetch, }); let finishReason: LanguageModelV2FinishReason = 'unknown'; const usage: LanguageModelV2Usage = { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, }; let providerMetadata: SharedV2ProviderMetadata | undefined = undefined; const generateId = this.config.generateId; let hasToolCalls = false; // Track active blocks to group consecutive parts of same type let currentTextBlockId: string | null = null; let currentReasoningBlockId: string | null = null; let blockCounter = 0; // Track emitted sources to prevent duplicates const emittedSourceUrls = new Set<string>(); // Associates a code execution result with its preceding call. let lastCodeExecutionToolCallId: string | undefined; return { stream: response.pipeThrough( new TransformStream< ParseResult<z.infer<typeof chunkSchema>>, LanguageModelV2StreamPart >({ start(controller) { controller.enqueue({ type: 'stream-start', warnings }); }, transform(chunk, controller) { if (options.includeRawChunks) { controller.enqueue({ type: 'raw', rawValue: chunk.rawValue }); } if (!chunk.success) { controller.enqueue({ type: 'error', error: chunk.error }); return; } const value = chunk.value; const usageMetadata = value.usageMetadata; if (usageMetadata != null) { usage.inputTokens = usageMetadata.promptTokenCount ?? undefined; usage.outputTokens = usageMetadata.candidatesTokenCount ?? undefined; usage.totalTokens = usageMetadata.totalTokenCount ?? undefined; usage.reasoningTokens = usageMetadata.thoughtsTokenCount ?? undefined; usage.cachedInputTokens = usageMetadata.cachedContentTokenCount ?? undefined; } const candidate = value.candidates?.[0]; // sometimes the API returns an empty candidates array if (candidate == null) { return; } const content = candidate.content; const sources = extractSources({ groundingMetadata: candidate.groundingMetadata, generateId, }); if (sources != null) { for (const source of sources) { if ( source.sourceType === 'url' && !emittedSourceUrls.has(source.url) ) { emittedSourceUrls.add(source.url); controller.enqueue(source); } } } // Process tool call's parts before determining finishReason to ensure hasToolCalls is properly set if (content != null) { // Process text parts individually to handle reasoning parts const parts = content.parts ?? []; for (const part of parts) { if ('executableCode' in part && part.executableCode?.code) { const toolCallId = generateId(); lastCodeExecutionToolCallId = toolCallId; controller.enqueue({ type: 'tool-call', toolCallId, toolName: 'code_execution', input: JSON.stringify(part.executableCode), providerExecuted: true, }); hasToolCalls = true; } else if ( 'codeExecutionResult' in part && part.codeExecutionResult ) { // Assumes a result directly follows its corresponding call part. const toolCallId = lastCodeExecutionToolCallId; if (toolCallId) { controller.enqueue({ type: 'tool-result', toolCallId, toolName: 'code_execution', result: { outcome: part.codeExecutionResult.outcome, output: part.codeExecutionResult.output, }, providerExecuted: true, }); // Clear the ID after use. lastCodeExecutionToolCallId = undefined; } } else if ( 'text' in part && part.text != null && part.text.length > 0 ) { if (part.thought === true) { // End any active text block before starting reasoning if (currentTextBlockId !== null) { controller.enqueue({ type: 'text-end', id: currentTextBlockId, }); currentTextBlockId = null; } // Start new reasoning block if not already active if (currentReasoningBlockId === null) { currentReasoningBlockId = String(blockCounter++); controller.enqueue({ type: 'reasoning-start', id: currentReasoningBlockId, providerMetadata: part.thoughtSignature ? { google: { thoughtSignature: part.thoughtSignature, }, } : undefined, }); } controller.enqueue({ type: 'reasoning-delta', id: currentReasoningBlockId, delta: part.text, providerMetadata: part.thoughtSignature ? { google: { thoughtSignature: part.thoughtSignature }, } : undefined, }); } else { // End any active reasoning block before starting text if (currentReasoningBlockId !== null) { controller.enqueue({ type: 'reasoning-end', id: currentReasoningBlockId, }); currentReasoningBlockId = null; } // Start new text block if not already active if (currentTextBlockId === null) { currentTextBlockId = String(blockCounter++); controller.enqueue({ type: 'text-start', id: currentTextBlockId, providerMetadata: part.thoughtSignature ? { google: { thoughtSignature: part.thoughtSignature, }, } : undefined, }); } controller.enqueue({ type: 'text-delta', id: currentTextBlockId, delta: part.text, providerMetadata: part.thoughtSignature ? { google: { thoughtSignature: part.thoughtSignature }, } : undefined, }); } } } const inlineDataParts = getInlineDataParts(content.parts); if (inlineDataParts != null) { for (const part of inlineDataParts) { controller.enqueue({ type: 'file', mediaType: part.inlineData.mimeType, data: part.inlineData.data, }); } } const toolCallDeltas = getToolCallsFromParts({ parts: content.parts, generateId, }); if (toolCallDeltas != null) { for (const toolCall of toolCallDeltas) { controller.enqueue({ type: 'tool-input-start', id: toolCall.toolCallId, toolName: toolCall.toolName, providerMetadata: toolCall.providerMetadata, }); controller.enqueue({ type: 'tool-input-delta', id: toolCall.toolCallId, delta: toolCall.args, providerMetadata: toolCall.providerMetadata, }); controller.enqueue({ type: 'tool-input-end', id: toolCall.toolCallId, providerMetadata: toolCall.providerMetadata, }); controller.enqueue({ type: 'tool-call', toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, input: toolCall.args, providerMetadata: toolCall.providerMetadata, }); hasToolCalls = true; } } } if (candidate.finishReason != null) { finishReason = mapGoogleGenerativeAIFinishReason({ finishReason: candidate.finishReason, hasToolCalls, }); providerMetadata = { google: { groundingMetadata: candidate.groundingMetadata ?? null, urlContextMetadata: candidate.urlContextMetadata ?? null, safetyRatings: candidate.safetyRatings ?? null, }, }; if (usageMetadata != null) { providerMetadata.google.usageMetadata = usageMetadata; } } }, flush(controller) { // Close any open blocks before finishing if (currentTextBlockId !== null) { controller.enqueue({ type: 'text-end', id: currentTextBlockId, }); } if (currentReasoningBlockId !== null) { controller.enqueue({ type: 'reasoning-end', id: currentReasoningBlockId, }); } controller.enqueue({ type: 'finish', finishReason, usage, providerMetadata, }); }, }), ), response: { headers: responseHeaders }, request: { body }, }; } } function getToolCallsFromParts({ parts, generateId, }: { parts: z.infer<typeof contentSchema>['parts']; generateId: () => string; }) { const functionCallParts = parts?.filter( part => 'functionCall' in part, ) as Array< GoogleGenerativeAIContentPart & { functionCall: { name: string; args: unknown }; thoughtSignature?: string | null; } >; return functionCallParts == null || functionCallParts.length === 0 ? undefined : functionCallParts.map(part => ({ type: 'tool-call' as const, toolCallId: generateId(), toolName: part.functionCall.name, args: JSON.stringify(part.functionCall.args), providerMetadata: part.thoughtSignature ? { google: { thoughtSignature: part.thoughtSignature } } : undefined, })); } function getInlineDataParts(parts: z.infer<typeof contentSchema>['parts']) { return parts?.filter( ( part, ): part is { inlineData: { mimeType: string; data: string }; } => 'inlineData' in part, ); } function extractSources({ groundingMetadata, generateId, }: { groundingMetadata: z.infer<typeof groundingMetadataSchema> | undefined | null; generateId: () => string; }): undefined | LanguageModelV2Source[] { return groundingMetadata?.groundingChunks ?.filter( ( chunk, ): chunk is z.infer<typeof groundingChunkSchema> & { web: { uri: string; title?: string }; } => chunk.web != null, ) .map(chunk => ({ type: 'source', sourceType: 'url', id: generateId(), url: chunk.web.uri, title: chunk.web.title, })); } const contentSchema = z.object({ parts: z .array( z.union([ // note: order matters since text can be fully empty z.object({ functionCall: z.object({ name: z.string(), args: z.unknown(), }), thoughtSignature: z.string().nullish(), }), z.object({ inlineData: z.object({ mimeType: z.string(), data: z.string(), }), }), z.object({ executableCode: z .object({ language: z.string(), code: z.string(), }) .nullish(), codeExecutionResult: z .object({ outcome: z.string(), output: z.string(), }) .nullish(), text: z.string().nullish(), thought: z.boolean().nullish(), thoughtSignature: z.string().nullish(), }), ]), ) .nullish(), }); // https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/configure-safety-filters export const safetyRatingSchema = z.object({ category: z.string().nullish(), probability: z.string().nullish(), probabilityScore: z.number().nullish(), severity: z.string().nullish(), severityScore: z.number().nullish(), blocked: z.boolean().nullish(), }); const usageSchema = z.object({ cachedContentTokenCount: z.number().nullish(), thoughtsTokenCount: z.number().nullish(), promptTokenCount: z.number().nullish(), candidatesTokenCount: z.number().nullish(), totalTokenCount: z.number().nullish(), }); const responseSchema = z.object({ candidates: z.array( z.object({ content: contentSchema.nullish().or(z.object({}).strict()), finishReason: z.string().nullish(), safetyRatings: z.array(safetyRatingSchema).nullish(), groundingMetadata: groundingMetadataSchema.nullish(), urlContextMetadata: urlContextMetadataSchema.nullish(), }), ), usageMetadata: usageSchema.nullish(), }); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const chunkSchema = z.object({ candidates: z .array( z.object({ content: contentSchema.nullish(), finishReason: z.string().nullish(), safetyRatings: z.array(safetyRatingSchema).nullish(), groundingMetadata: groundingMetadataSchema.nullish(), urlContextMetadata: urlContextMetadataSchema.nullish(), }), ) .nullish(), usageMetadata: usageSchema.nullish(), }); --- File: /ai/packages/google/src/google-generative-ai-options.ts --- import { z } from 'zod/v4'; export type GoogleGenerativeAIModelId = // Stable models // https://ai.google.dev/gemini-api/docs/models/gemini | 'gemini-1.5-flash' | 'gemini-1.5-flash-latest' | 'gemini-1.5-flash-001' | 'gemini-1.5-flash-002' | 'gemini-1.5-flash-8b' | 'gemini-1.5-flash-8b-latest' | 'gemini-1.5-flash-8b-001' | 'gemini-1.5-pro' | 'gemini-1.5-pro-latest' | 'gemini-1.5-pro-001' | 'gemini-1.5-pro-002' | 'gemini-2.0-flash' | 'gemini-2.0-flash-001' | 'gemini-2.0-flash-live-001' | 'gemini-2.0-flash-lite' | 'gemini-2.0-pro-exp-02-05' | 'gemini-2.0-flash-thinking-exp-01-21' | 'gemini-2.0-flash-exp' | 'gemini-2.5-pro' | 'gemini-2.5-flash' | 'gemini-2.5-flash-lite' // Experimental models // https://ai.google.dev/gemini-api/docs/models/experimental-models | 'gemini-2.5-pro-exp-03-25' | 'gemini-2.5-flash-preview-04-17' | 'gemini-exp-1206' | 'gemma-3-12b-it' | 'gemma-3-27b-it' | (string & {}); export const googleGenerativeAIProviderOptions = z.object({ responseModalities: z.array(z.enum(['TEXT', 'IMAGE'])).optional(), thinkingConfig: z .object({ thinkingBudget: z.number().optional(), includeThoughts: z.boolean().optional(), }) .optional(), /** Optional. The name of the cached content used as context to serve the prediction. Format: cachedContents/{cachedContent} */ cachedContent: z.string().optional(), /** * Optional. Enable structured output. Default is true. * * This is useful when the JSON Schema contains elements that are * not supported by the OpenAPI schema version that * Google Generative AI uses. You can use this to disable * structured outputs if you need to. */ structuredOutputs: z.boolean().optional(), /** Optional. A list of unique safety settings for blocking unsafe content. */ safetySettings: z .array( z.object({ category: z.enum([ 'HARM_CATEGORY_UNSPECIFIED', 'HARM_CATEGORY_HATE_SPEECH', 'HARM_CATEGORY_DANGEROUS_CONTENT', 'HARM_CATEGORY_HARASSMENT', 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'HARM_CATEGORY_CIVIC_INTEGRITY', ]), threshold: z.enum([ 'HARM_BLOCK_THRESHOLD_UNSPECIFIED', 'BLOCK_LOW_AND_ABOVE', 'BLOCK_MEDIUM_AND_ABOVE', 'BLOCK_ONLY_HIGH', 'BLOCK_NONE', 'OFF', ]), }), ) .optional(), threshold: z .enum([ 'HARM_BLOCK_THRESHOLD_UNSPECIFIED', 'BLOCK_LOW_AND_ABOVE', 'BLOCK_MEDIUM_AND_ABOVE', 'BLOCK_ONLY_HIGH', 'BLOCK_NONE', 'OFF', ]) .optional(), /** * Optional. Enables timestamp understanding for audio-only files. * * https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/audio-understanding */ audioTimestamp: z.boolean().optional(), }); export type GoogleGenerativeAIProviderOptions = z.infer< typeof googleGenerativeAIProviderOptions >; --- File: /ai/packages/google/src/google-generative-ai-prompt.ts --- import { z } from 'zod/v4'; import { groundingMetadataSchema } from './tool/google-search'; import { urlContextMetadataSchema } from './tool/url-context'; import { safetyRatingSchema } from './google-generative-ai-language-model'; export type GoogleGenerativeAIPrompt = { systemInstruction?: GoogleGenerativeAISystemInstruction; contents: Array<GoogleGenerativeAIContent>; }; export type GoogleGenerativeAISystemInstruction = { parts: Array<{ text: string }>; }; export type GoogleGenerativeAIContent = { role: 'user' | 'model'; parts: Array<GoogleGenerativeAIContentPart>; }; export type GoogleGenerativeAIContentPart = | { text: string } | { inlineData: { mimeType: string; data: string } } | { functionCall: { name: string; args: unknown } } | { functionResponse: { name: string; response: unknown } } | { fileData: { mimeType: string; fileUri: string } }; export type GoogleGenerativeAIGroundingMetadata = z.infer< typeof groundingMetadataSchema >; export type GoogleGenerativeAIUrlContextMetadata = z.infer< typeof urlContextMetadataSchema >; export type GoogleGenerativeAISafetyRating = z.infer<typeof safetyRatingSchema>; export interface GoogleGenerativeAIProviderMetadata { groundingMetadata: GoogleGenerativeAIGroundingMetadata | null; urlContextMetadata: GoogleGenerativeAIUrlContextMetadata | null; safetyRatings: GoogleGenerativeAISafetyRating[] | null; } --- File: /ai/packages/google/src/google-prepare-tools.test.ts --- import { prepareTools } from './google-prepare-tools'; it('should return undefined tools and tool_choice when tools are null', () => { const result = prepareTools({ tools: undefined, modelId: 'gemini-2.5-flash', }); expect(result).toEqual({ tools: undefined, tool_choice: undefined, toolWarnings: [], }); }); it('should return undefined tools and tool_choice when tools are empty', () => { const result = prepareTools({ tools: [], modelId: 'gemini-2.5-flash' }); expect(result).toEqual({ tools: undefined, tool_choice: undefined, toolWarnings: [], }); }); it('should correctly prepare function tools', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'A test function', inputSchema: { type: 'object', properties: {} }, }, ], modelId: 'gemini-2.5-flash', }); expect(result.tools).toEqual({ functionDeclarations: [ { name: 'testFunction', description: 'A test function', parameters: undefined, }, ], }); expect(result.toolConfig).toBeUndefined(); expect(result.toolWarnings).toEqual([]); }); it('should correctly prepare provider-defined tools', () => { const result = prepareTools({ tools: [ { type: 'provider-defined', id: 'google.google_search', name: 'google_search', args: {}, }, { type: 'provider-defined', id: 'google.url_context', name: 'url_context', args: {}, }, ], modelId: 'gemini-2.5-flash', }); expect(result.tools).toEqual({ googleSearch: {}, urlContext: {}, }); expect(result.toolConfig).toBeUndefined(); expect(result.toolWarnings).toEqual([]); }); it('should add warnings for unsupported tools', () => { const result = prepareTools({ tools: [ { type: 'provider-defined', id: 'unsupported.tool', name: 'unsupported_tool', args: {}, }, ], modelId: 'gemini-2.5-flash', }); expect(result.tools).toBeUndefined(); expect(result.toolConfig).toBeUndefined(); expect(result.toolWarnings).toMatchInlineSnapshot(` [ { "tool": { "args": {}, "id": "unsupported.tool", "name": "unsupported_tool", "type": "provider-defined", }, "type": "unsupported-tool", }, ] `); }); it('should handle tool choice "auto"', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'Test', inputSchema: {}, }, ], toolChoice: { type: 'auto' }, modelId: 'gemini-2.5-flash', }); expect(result.toolConfig).toEqual({ functionCallingConfig: { mode: 'AUTO' }, }); }); it('should handle tool choice "required"', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'Test', inputSchema: {}, }, ], toolChoice: { type: 'required' }, modelId: 'gemini-2.5-flash', }); expect(result.toolConfig).toEqual({ functionCallingConfig: { mode: 'ANY' }, }); }); it('should handle tool choice "none"', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'Test', inputSchema: {}, }, ], toolChoice: { type: 'none' }, modelId: 'gemini-2.5-flash', }); expect(result.tools).toEqual({ functionDeclarations: [ { name: 'testFunction', description: 'Test', parameters: {}, }, ], }); expect(result.toolConfig).toEqual({ functionCallingConfig: { mode: 'NONE' }, }); }); it('should handle tool choice "tool"', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'Test', inputSchema: {}, }, ], toolChoice: { type: 'tool', toolName: 'testFunction' }, modelId: 'gemini-2.5-flash', }); expect(result.toolConfig).toEqual({ functionCallingConfig: { mode: 'ANY', allowedFunctionNames: ['testFunction'], }, }); }); it('should warn when mixing function and provider-defined tools', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'A test function', inputSchema: { type: 'object', properties: {} }, }, { type: 'provider-defined', id: 'google.google_search', name: 'google_search', args: {}, }, ], modelId: 'gemini-2.5-flash', }); // Should only include provider-defined tools expect(result.tools).toEqual({ googleSearch: {}, }); // Should have warning about mixed tool types expect(result.toolWarnings).toEqual([ { type: 'unsupported-tool', tool: { type: 'function', name: 'testFunction', description: 'A test function', inputSchema: { type: 'object', properties: {} }, }, details: 'Cannot mix function tools with provider-defined tools in the same request. Please use either function tools or provider-defined tools, but not both.', }, ]); expect(result.toolConfig).toBeUndefined(); }); it('should handle tool choice with mixed tools (provider-defined tools only)', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'A test function', inputSchema: { type: 'object', properties: {} }, }, { type: 'provider-defined', id: 'google.google_search', name: 'google_search', args: {}, }, ], toolChoice: { type: 'auto' }, modelId: 'gemini-2.5-flash', }); // Should only include provider-defined tools expect(result.tools).toEqual({ googleSearch: {}, }); // Should apply tool choice to provider-defined tools expect(result.toolConfig).toEqual(undefined); // Should have warning about mixed tool types expect(result.toolWarnings).toEqual([ { type: 'unsupported-tool', tool: { type: 'function', name: 'testFunction', description: 'A test function', inputSchema: { type: 'object', properties: {} }, }, details: 'Cannot mix function tools with provider-defined tools in the same request. Please use either function tools or provider-defined tools, but not both.', }, ]); }); --- File: /ai/packages/google/src/google-prepare-tools.ts --- import { LanguageModelV2CallOptions, LanguageModelV2CallWarning, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { convertJSONSchemaToOpenAPISchema } from './convert-json-schema-to-openapi-schema'; import { GoogleGenerativeAIModelId } from './google-generative-ai-options'; export function prepareTools({ tools, toolChoice, modelId, }: { tools: LanguageModelV2CallOptions['tools']; toolChoice?: LanguageModelV2CallOptions['toolChoice']; modelId: GoogleGenerativeAIModelId; }): { tools: | { functionDeclarations: Array<{ name: string; description: string; parameters: unknown; }>; } | Record<string, any> | undefined; toolConfig: | undefined | { functionCallingConfig: { mode: 'AUTO' | 'NONE' | 'ANY'; allowedFunctionNames?: string[]; }; }; toolWarnings: LanguageModelV2CallWarning[]; } { // when the tools array is empty, change it to undefined to prevent errors: tools = tools?.length ? tools : undefined; const toolWarnings: LanguageModelV2CallWarning[] = []; const isGemini2 = modelId.includes('gemini-2'); const supportsDynamicRetrieval = modelId.includes('gemini-1.5-flash') && !modelId.includes('-8b'); if (tools == null) { return { tools: undefined, toolConfig: undefined, toolWarnings }; } // Check for mixed tool types and add warnings const hasFunctionTools = tools.some(tool => tool.type === 'function'); const hasProviderDefinedTools = tools.some( tool => tool.type === 'provider-defined', ); if (hasFunctionTools && hasProviderDefinedTools) { toolWarnings.push({ type: 'unsupported-tool', tool: tools.find(tool => tool.type === 'function')!, details: 'Cannot mix function tools with provider-defined tools in the same request. Please use either function tools or provider-defined tools, but not both.', }); } if (hasProviderDefinedTools) { const googleTools: Record<string, any> = {}; const providerDefinedTools = tools.filter( tool => tool.type === 'provider-defined', ); providerDefinedTools.forEach(tool => { switch (tool.id) { case 'google.google_search': if (isGemini2) { googleTools.googleSearch = {}; } else if (supportsDynamicRetrieval) { // For non-Gemini-2 models that don't support dynamic retrieval, use basic googleSearchRetrieval googleTools.googleSearchRetrieval = { dynamicRetrievalConfig: { mode: tool.args.mode as | 'MODE_DYNAMIC' | 'MODE_UNSPECIFIED' | undefined, dynamicThreshold: tool.args.dynamicThreshold as | number | undefined, }, }; } else { googleTools.googleSearchRetrieval = {}; } break; case 'google.url_context': if (isGemini2) { googleTools.urlContext = {}; } else { toolWarnings.push({ type: 'unsupported-tool', tool, details: 'The URL context tool is not supported with other Gemini models than Gemini 2.', }); } break; case 'google.code_execution': if (isGemini2) { googleTools.codeExecution = {}; } else { toolWarnings.push({ type: 'unsupported-tool', tool, details: 'The code execution tools is not supported with other Gemini models than Gemini 2.', }); } break; default: toolWarnings.push({ type: 'unsupported-tool', tool }); break; } }); return { tools: Object.keys(googleTools).length > 0 ? googleTools : undefined, toolConfig: undefined, toolWarnings, }; } const functionDeclarations = []; for (const tool of tools) { switch (tool.type) { case 'function': functionDeclarations.push({ name: tool.name, description: tool.description ?? '', parameters: convertJSONSchemaToOpenAPISchema(tool.inputSchema), }); break; default: toolWarnings.push({ type: 'unsupported-tool', tool }); break; } } if (toolChoice == null) { return { tools: { functionDeclarations }, toolConfig: undefined, toolWarnings, }; } const type = toolChoice.type; switch (type) { case 'auto': return { tools: { functionDeclarations }, toolConfig: { functionCallingConfig: { mode: 'AUTO' } }, toolWarnings, }; case 'none': return { tools: { functionDeclarations }, toolConfig: { functionCallingConfig: { mode: 'NONE' } }, toolWarnings, }; case 'required': return { tools: { functionDeclarations }, toolConfig: { functionCallingConfig: { mode: 'ANY' } }, toolWarnings, }; case 'tool': return { tools: { functionDeclarations }, toolConfig: { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: [toolChoice.toolName], }, }, toolWarnings, }; default: { const _exhaustiveCheck: never = type; throw new UnsupportedFunctionalityError({ functionality: `tool choice type: ${_exhaustiveCheck}`, }); } } } --- File: /ai/packages/google/src/google-provider.test.ts --- import { describe, it, expect, beforeEach, vi } from 'vitest'; import { createGoogleGenerativeAI } from './google-provider'; import { GoogleGenerativeAILanguageModel } from './google-generative-ai-language-model'; import { GoogleGenerativeAIEmbeddingModel } from './google-generative-ai-embedding-model'; import { GoogleGenerativeAIImageModel } from './google-generative-ai-image-model'; // Mock the imported modules using a partial mock to preserve original exports vi.mock('@ai-sdk/provider-utils', async importOriginal => { const mod = await importOriginal<typeof import('@ai-sdk/provider-utils')>(); return { ...mod, loadApiKey: vi.fn().mockImplementation(({ apiKey }) => apiKey), generateId: vi.fn().mockReturnValue('mock-id'), withoutTrailingSlash: vi.fn().mockImplementation(url => url), }; }); vi.mock('./google-generative-ai-language-model', () => ({ GoogleGenerativeAILanguageModel: vi.fn(), })); vi.mock('./google-generative-ai-embedding-model', () => ({ GoogleGenerativeAIEmbeddingModel: vi.fn(), })); vi.mock('./google-generative-ai-image-model', () => ({ GoogleGenerativeAIImageModel: vi.fn(), })); describe('google-provider', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should create a language model with default settings', () => { const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key', }); provider('gemini-pro'); expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith( 'gemini-pro', expect.objectContaining({ provider: 'google.generative-ai', baseURL: 'https://generativelanguage.googleapis.com/v1beta', headers: expect.any(Function), generateId: expect.any(Function), supportedUrls: expect.any(Function), }), ); }); it('should throw an error when using new keyword', () => { const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key' }); expect(() => new (provider as any)('gemini-pro')).toThrow( 'The Google Generative AI model function cannot be called with the new keyword.', ); }); it('should create an embedding model with correct settings', () => { const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key', }); provider.textEmbeddingModel('embedding-001'); expect(GoogleGenerativeAIEmbeddingModel).toHaveBeenCalledWith( 'embedding-001', expect.objectContaining({ provider: 'google.generative-ai', headers: expect.any(Function), baseURL: 'https://generativelanguage.googleapis.com/v1beta', }), ); }); it('should pass custom headers to the model constructor', () => { const customHeaders = { 'Custom-Header': 'custom-value' }; const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key', headers: customHeaders, }); provider('gemini-pro'); expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ headers: expect.any(Function), }), ); const options = (GoogleGenerativeAILanguageModel as any).mock.calls[0][1]; const headers = options.headers(); expect(headers).toEqual({ 'x-goog-api-key': 'test-api-key', 'Custom-Header': 'custom-value', }); }); it('should pass custom generateId function to the model constructor', () => { const customGenerateId = () => 'custom-id'; const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key', generateId: customGenerateId, }); provider('gemini-pro'); expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ generateId: customGenerateId, }), ); }); it('should use chat method to create a model', () => { const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key', }); provider.chat('gemini-pro'); expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith( 'gemini-pro', expect.any(Object), ); }); it('should use custom baseURL when provided', () => { const customBaseURL = 'https://custom-endpoint.example.com'; const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key', baseURL: customBaseURL, }); provider('gemini-pro'); expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith( 'gemini-pro', expect.objectContaining({ baseURL: customBaseURL, }), ); }); it('should create an image model with default settings', () => { const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key', }); provider.image('imagen-3.0-generate-002'); expect(GoogleGenerativeAIImageModel).toHaveBeenCalledWith( 'imagen-3.0-generate-002', {}, expect.objectContaining({ provider: 'google.generative-ai', headers: expect.any(Function), baseURL: 'https://generativelanguage.googleapis.com/v1beta', }), ); }); it('should create an image model with custom maxImagesPerCall', () => { const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key', }); const imageSettings = { maxImagesPerCall: 3, }; provider.image('imagen-3.0-generate-002', imageSettings); expect(GoogleGenerativeAIImageModel).toHaveBeenCalledWith( 'imagen-3.0-generate-002', imageSettings, expect.objectContaining({ provider: 'google.generative-ai', headers: expect.any(Function), baseURL: 'https://generativelanguage.googleapis.com/v1beta', }), ); }); it('should support deprecated methods', () => { const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key', }); provider.generativeAI('gemini-pro'); provider.embedding('embedding-001'); provider.textEmbedding('embedding-001'); expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledTimes(1); expect(GoogleGenerativeAIEmbeddingModel).toHaveBeenCalledTimes(2); }); it('should include YouTube URLs in supportedUrls', () => { const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key', }); provider('gemini-pro'); const call = vi.mocked(GoogleGenerativeAILanguageModel).mock.calls[0]; const supportedUrlsFunction = call[1].supportedUrls; expect(supportedUrlsFunction).toBeDefined(); const supportedUrls = supportedUrlsFunction!() as Record<string, RegExp[]>; const patterns = supportedUrls['*']; expect(patterns).toBeDefined(); expect(Array.isArray(patterns)).toBe(true); const testResults = { supportedUrls: [ 'https://generativelanguage.googleapis.com/v1beta/files/test123', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'https://youtube.com/watch?v=dQw4w9WgXcQ', 'https://youtu.be/dQw4w9WgXcQ', ].map(url => ({ url, isSupported: patterns.some((pattern: RegExp) => pattern.test(url)), })), unsupportedUrls: [ 'https://example.com', 'https://vimeo.com/123456789', 'https://youtube.com/channel/UCdQw4w9WgXcQ', ].map(url => ({ url, isSupported: patterns.some((pattern: RegExp) => pattern.test(url)), })), }; expect(testResults).toMatchInlineSnapshot(` { "supportedUrls": [ { "isSupported": true, "url": "https://generativelanguage.googleapis.com/v1beta/files/test123", }, { "isSupported": true, "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", }, { "isSupported": true, "url": "https://youtube.com/watch?v=dQw4w9WgXcQ", }, { "isSupported": true, "url": "https://youtu.be/dQw4w9WgXcQ", }, ], "unsupportedUrls": [ { "isSupported": false, "url": "https://example.com", }, { "isSupported": false, "url": "https://vimeo.com/123456789", }, { "isSupported": false, "url": "https://youtube.com/channel/UCdQw4w9WgXcQ", }, ], } `); }); }); --- File: /ai/packages/google/src/google-provider.ts --- import { EmbeddingModelV2, LanguageModelV2, ProviderV2, ImageModelV2, } from '@ai-sdk/provider'; import { FetchFunction, generateId, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { GoogleGenerativeAIEmbeddingModel } from './google-generative-ai-embedding-model'; import { GoogleGenerativeAIEmbeddingModelId } from './google-generative-ai-embedding-options'; import { GoogleGenerativeAILanguageModel } from './google-generative-ai-language-model'; import { GoogleGenerativeAIModelId } from './google-generative-ai-options'; import { googleTools } from './google-tools'; import { GoogleGenerativeAIImageSettings, GoogleGenerativeAIImageModelId, } from './google-generative-ai-image-settings'; import { GoogleGenerativeAIImageModel } from './google-generative-ai-image-model'; export interface GoogleGenerativeAIProvider extends ProviderV2 { (modelId: GoogleGenerativeAIModelId): LanguageModelV2; languageModel(modelId: GoogleGenerativeAIModelId): LanguageModelV2; chat(modelId: GoogleGenerativeAIModelId): LanguageModelV2; /** Creates a model for image generation. */ image( modelId: GoogleGenerativeAIImageModelId, settings?: GoogleGenerativeAIImageSettings, ): ImageModelV2; /** * @deprecated Use `chat()` instead. */ generativeAI(modelId: GoogleGenerativeAIModelId): LanguageModelV2; /** @deprecated Use `textEmbedding()` instead. */ embedding( modelId: GoogleGenerativeAIEmbeddingModelId, ): EmbeddingModelV2<string>; textEmbedding( modelId: GoogleGenerativeAIEmbeddingModelId, ): EmbeddingModelV2<string>; textEmbeddingModel( modelId: GoogleGenerativeAIEmbeddingModelId, ): EmbeddingModelV2<string>; tools: typeof googleTools; } export interface GoogleGenerativeAIProviderSettings { /** Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://generativelanguage.googleapis.com/v1beta`. */ baseURL?: string; /** API key that is being send using the `x-goog-api-key` header. It defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable. */ apiKey?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string | undefined>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; /** Optional function to generate a unique ID for each request. */ generateId?: () => string; } /** Create a Google Generative AI provider instance. */ export function createGoogleGenerativeAI( options: GoogleGenerativeAIProviderSettings = {}, ): GoogleGenerativeAIProvider { const baseURL = withoutTrailingSlash(options.baseURL) ?? 'https://generativelanguage.googleapis.com/v1beta'; const getHeaders = () => ({ 'x-goog-api-key': loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'GOOGLE_GENERATIVE_AI_API_KEY', description: 'Google Generative AI', }), ...options.headers, }); const createChatModel = (modelId: GoogleGenerativeAIModelId) => new GoogleGenerativeAILanguageModel(modelId, { provider: 'google.generative-ai', baseURL, headers: getHeaders, generateId: options.generateId ?? generateId, supportedUrls: () => ({ '*': [ // Google Generative Language "files" endpoint // e.g. https://generativelanguage.googleapis.com/v1beta/files/... new RegExp(`^${baseURL}/files/.*$`), // YouTube URLs (public or unlisted videos) new RegExp( `^https://(?:www\\.)?youtube\\.com/watch\\?v=[\\w-]+(?:&[\\w=&.-]*)?$`, ), new RegExp(`^https://youtu\\.be/[\\w-]+(?:\\?[\\w=&.-]*)?$`), ], }), fetch: options.fetch, }); const createEmbeddingModel = (modelId: GoogleGenerativeAIEmbeddingModelId) => new GoogleGenerativeAIEmbeddingModel(modelId, { provider: 'google.generative-ai', baseURL, headers: getHeaders, fetch: options.fetch, }); const createImageModel = ( modelId: GoogleGenerativeAIImageModelId, settings: GoogleGenerativeAIImageSettings = {}, ) => new GoogleGenerativeAIImageModel(modelId, settings, { provider: 'google.generative-ai', baseURL, headers: getHeaders, fetch: options.fetch, }); const provider = function (modelId: GoogleGenerativeAIModelId) { if (new.target) { throw new Error( 'The Google Generative AI model function cannot be called with the new keyword.', ); } return createChatModel(modelId); }; provider.languageModel = createChatModel; provider.chat = createChatModel; provider.generativeAI = createChatModel; provider.embedding = createEmbeddingModel; provider.textEmbedding = createEmbeddingModel; provider.textEmbeddingModel = createEmbeddingModel; provider.image = createImageModel; provider.imageModel = createImageModel; provider.tools = googleTools; return provider as GoogleGenerativeAIProvider; } /** Default Google Generative AI provider instance. */ export const google = createGoogleGenerativeAI(); --- File: /ai/packages/google/src/google-supported-file-url.test.ts --- import { isSupportedFileUrl } from './google-supported-file-url'; it('should return true for valid Google generative language file URLs', () => { const validUrl = new URL( 'https://generativelanguage.googleapis.com/v1beta/files/00000000-00000000-00000000-00000000', ); expect(isSupportedFileUrl(validUrl)).toBe(true); const simpleValidUrl = new URL( 'https://generativelanguage.googleapis.com/v1beta/files/test123', ); expect(isSupportedFileUrl(simpleValidUrl)).toBe(true); }); it('should return true for valid YouTube URLs', () => { const validYouTubeUrls = [ new URL('https://www.youtube.com/watch?v=dQw4w9WgXcQ'), new URL('https://youtube.com/watch?v=dQw4w9WgXcQ'), new URL('https://youtu.be/dQw4w9WgXcQ'), new URL('https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=youtu.be'), new URL('https://youtu.be/dQw4w9WgXcQ?t=42'), ]; validYouTubeUrls.forEach(url => { expect(isSupportedFileUrl(url)).toBe(true); }); }); it('should return false for invalid YouTube URLs', () => { const invalidYouTubeUrls = [ new URL('https://youtube.com/channel/UCdQw4w9WgXcQ'), new URL('https://youtube.com/playlist?list=PLdQw4w9WgXcQ'), new URL('https://m.youtube.com/watch?v=dQw4w9WgXcQ'), new URL('http://youtube.com/watch?v=dQw4w9WgXcQ'), new URL('https://vimeo.com/123456789'), ]; invalidYouTubeUrls.forEach(url => { expect(isSupportedFileUrl(url)).toBe(false); }); }); it('should return false for non-Google generative language file URLs', () => { const testCases = [ new URL('https://example.com'), new URL('https://example.com/foo/bar'), new URL('https://generativelanguage.googleapis.com'), new URL('https://generativelanguage.googleapis.com/v1/other'), new URL('http://generativelanguage.googleapis.com/v1beta/files/test'), new URL('https://api.googleapis.com/v1beta/files/test'), ]; testCases.forEach(url => { expect(isSupportedFileUrl(url)).toBe(false); }); }); --- File: /ai/packages/google/src/google-supported-file-url.ts --- export function isSupportedFileUrl(url: URL): boolean { const urlString = url.toString(); // Google Generative Language files API if ( urlString.startsWith( 'https://generativelanguage.googleapis.com/v1beta/files/', ) ) { return true; } // YouTube URLs (public or unlisted videos) const youtubeRegexes = [ /^https:\/\/(?:www\.)?youtube\.com\/watch\?v=[\w-]+(?:&[\w=&.-]*)?$/, /^https:\/\/youtu\.be\/[\w-]+(?:\?[\w=&.-]*)?$/, ]; return youtubeRegexes.some(regex => regex.test(urlString)); } --- File: /ai/packages/google/src/google-tools.ts --- import { codeExecution } from './tool/code-execution'; import { googleSearch } from './tool/google-search'; import { urlContext } from './tool/url-context'; export const googleTools = { /** * Creates a Google search tool that gives Google direct access to real-time web content. * Must have name "google_search". */ googleSearch, /** * Creates a URL context tool that gives Google direct access to real-time web content. * Must have name "url_context". */ urlContext, /** * A tool that enables the model to generate and run Python code. * Must have name "code_execution". * * @note Ensure the selected model supports Code Execution. * Multi-tool usage with the code execution tool is typically compatible with Gemini >=2 models. * * @see https://ai.google.dev/gemini-api/docs/code-execution (Google AI) * @see https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/code-execution-api (Vertex AI) */ codeExecution, }; --- File: /ai/packages/google/src/index.ts --- export type { GoogleErrorData } from './google-error'; export type { GoogleGenerativeAIProviderOptions } from './google-generative-ai-options'; export type { GoogleGenerativeAIProviderMetadata } from './google-generative-ai-prompt'; export type { GoogleGenerativeAIImageProviderOptions } from './google-generative-ai-image-model'; export type { GoogleGenerativeAIEmbeddingProviderOptions } from './google-generative-ai-embedding-options'; export { createGoogleGenerativeAI, google } from './google-provider'; export type { GoogleGenerativeAIProvider, GoogleGenerativeAIProviderSettings, } from './google-provider'; --- File: /ai/packages/google/src/map-google-generative-ai-finish-reason.ts --- import { LanguageModelV2FinishReason } from '@ai-sdk/provider'; export function mapGoogleGenerativeAIFinishReason({ finishReason, hasToolCalls, }: { finishReason: string | null | undefined; hasToolCalls: boolean; }): LanguageModelV2FinishReason { switch (finishReason) { case 'STOP': return hasToolCalls ? 'tool-calls' : 'stop'; case 'MAX_TOKENS': return 'length'; case 'IMAGE_SAFETY': case 'RECITATION': case 'SAFETY': case 'BLOCKLIST': case 'PROHIBITED_CONTENT': case 'SPII': return 'content-filter'; case 'FINISH_REASON_UNSPECIFIED': case 'OTHER': return 'other'; case 'MALFORMED_FUNCTION_CALL': return 'error'; default: return 'unknown'; } } --- File: /ai/packages/google/internal.d.ts --- export * from './dist/internal'; --- File: /ai/packages/google/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, { entry: ['src/internal/index.ts'], outDir: 'dist/internal', format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/google/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/google/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/google-vertex/anthropic/edge.d.ts --- export * from '../dist/anthropic/edge'; --- File: /ai/packages/google-vertex/anthropic/index.d.ts --- export * from '../dist/anthropic'; --- File: /ai/packages/google-vertex/src/anthropic/edge/google-vertex-anthropic-provider-edge.test.ts --- import { resolve } from '@ai-sdk/provider-utils'; import * as edgeAuth from '../../edge/google-vertex-auth-edge'; import { createVertexAnthropic as createVertexAnthropicOriginal } from '../google-vertex-anthropic-provider'; import { createVertexAnthropic as createVertexAnthropicEdge } from './google-vertex-anthropic-provider-edge'; // Mock the imported modules vi.mock('../../edge/google-vertex-auth-edge', () => ({ generateAuthToken: vi.fn().mockResolvedValue('mock-auth-token'), })); vi.mock('../google-vertex-anthropic-provider', () => ({ createVertexAnthropic: vi.fn().mockImplementation(options => ({ ...options, })), })); describe('google-vertex-anthropic-provider-edge', () => { beforeEach(() => { vi.clearAllMocks(); }); it('default headers function should return auth token', async () => { createVertexAnthropicEdge({ project: 'test-project' }); const mockCreateVertex = vi.mocked(createVertexAnthropicOriginal); const passedOptions = mockCreateVertex.mock.calls[0][0]; expect(mockCreateVertex).toHaveBeenCalledTimes(1); expect(typeof passedOptions?.headers).toBe('function'); expect(await resolve(passedOptions?.headers)).toStrictEqual({ Authorization: 'Bearer mock-auth-token', }); }); it('should use custom headers in addition to auth token when provided', async () => { createVertexAnthropicEdge({ project: 'test-project', headers: async () => ({ 'Custom-Header': 'custom-value', }), }); const mockCreateVertex = vi.mocked(createVertexAnthropicOriginal); const passedOptions = mockCreateVertex.mock.calls[0][0]; expect(mockCreateVertex).toHaveBeenCalledTimes(1); expect(typeof passedOptions?.headers).toBe('function'); expect(await resolve(passedOptions?.headers)).toEqual({ Authorization: 'Bearer mock-auth-token', 'Custom-Header': 'custom-value', }); }); it('should use edge auth token generator', async () => { createVertexAnthropicEdge({ project: 'test-project' }); const mockCreateVertex = vi.mocked(createVertexAnthropicOriginal); const passedOptions = mockCreateVertex.mock.calls[0][0]; // Verify the headers function actually calls generateAuthToken by checking its result expect(passedOptions?.headers).toBeDefined(); await resolve(passedOptions?.headers); expect(edgeAuth.generateAuthToken).toHaveBeenCalled(); }); it('passes googleCredentials to generateAuthToken', async () => { createVertexAnthropicEdge({ project: 'test-project', googleCredentials: { clientEmail: 'test@example.com', privateKey: 'test-key', }, }); const mockCreateVertex = vi.mocked(createVertexAnthropicOriginal); const passedOptions = mockCreateVertex.mock.calls[0][0]; await resolve(passedOptions?.headers); // call the headers function expect(edgeAuth.generateAuthToken).toHaveBeenCalledWith({ clientEmail: 'test@example.com', privateKey: 'test-key', }); }); }); --- File: /ai/packages/google-vertex/src/anthropic/edge/google-vertex-anthropic-provider-edge.ts --- import { resolve } from '@ai-sdk/provider-utils'; import { generateAuthToken, GoogleCredentials, } from '../../edge/google-vertex-auth-edge'; import { createVertexAnthropic as createVertexAnthropicOriginal, GoogleVertexAnthropicProvider, GoogleVertexAnthropicProviderSettings as GoogleVertexAnthropicProviderSettingsOriginal, } from '../google-vertex-anthropic-provider'; export type { GoogleVertexAnthropicProvider }; export interface GoogleVertexAnthropicProviderSettings extends GoogleVertexAnthropicProviderSettingsOriginal { /** * Optional. The Google credentials for the Google Cloud service account. If * not provided, the Google Vertex provider will use environment variables to * load the credentials. */ googleCredentials?: GoogleCredentials; } export function createVertexAnthropic( options: GoogleVertexAnthropicProviderSettings = {}, ): GoogleVertexAnthropicProvider { return createVertexAnthropicOriginal({ ...options, headers: async () => ({ Authorization: `Bearer ${await generateAuthToken( options.googleCredentials, )}`, ...(await resolve(options.headers)), }), }); } /** * Default Google Vertex AI Anthropic provider instance. */ export const vertexAnthropic = createVertexAnthropic(); --- File: /ai/packages/google-vertex/src/anthropic/edge/index.ts --- export { createVertexAnthropic, vertexAnthropic, } from './google-vertex-anthropic-provider-edge'; export type { GoogleVertexAnthropicProviderSettings, GoogleVertexAnthropicProvider, } from './google-vertex-anthropic-provider-edge'; --- File: /ai/packages/google-vertex/src/anthropic/google-vertex-anthropic-messages-options.ts --- // https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude export type GoogleVertexAnthropicMessagesModelId = | 'claude-3-7-sonnet@20250219' | 'claude-3-5-sonnet-v2@20241022' | 'claude-3-5-haiku@20241022' | 'claude-3-5-sonnet@20240620' | 'claude-3-haiku@20240307' | 'claude-3-sonnet@20240229' | 'claude-3-opus@20240229' | (string & {}); --- File: /ai/packages/google-vertex/src/anthropic/google-vertex-anthropic-provider-node.test.ts --- import { resolve } from '@ai-sdk/provider-utils'; import { createVertexAnthropic as createVertexAnthropicOriginal } from './google-vertex-anthropic-provider'; import { createVertexAnthropic as createVertexAnthropicNode } from './google-vertex-anthropic-provider-node'; import { generateAuthToken } from '../google-vertex-auth-google-auth-library'; // Mock the imported modules vi.mock('../google-vertex-auth-google-auth-library', () => ({ generateAuthToken: vi.fn().mockResolvedValue('mock-auth-token'), })); vi.mock('./google-vertex-anthropic-provider', () => ({ createVertexAnthropic: vi.fn().mockImplementation(options => ({ ...options, })), })); describe('google-vertex-anthropic-provider-node', () => { beforeEach(() => { vi.clearAllMocks(); }); it('default headers function should return auth token', async () => { createVertexAnthropicNode({ project: 'test-project' }); expect(createVertexAnthropicOriginal).toHaveBeenCalledTimes(1); const passedOptions = vi.mocked(createVertexAnthropicOriginal).mock .calls[0][0]; expect(typeof passedOptions?.headers).toBe('function'); expect(await resolve(passedOptions?.headers)).toStrictEqual({ Authorization: 'Bearer mock-auth-token', }); }); it('should use custom headers in addition to auth token when provided', async () => { createVertexAnthropicNode({ project: 'test-project', headers: async () => ({ 'Custom-Header': 'custom-value', }), }); expect(createVertexAnthropicOriginal).toHaveBeenCalledTimes(1); const passedOptions = vi.mocked(createVertexAnthropicOriginal).mock .calls[0][0]; expect(await resolve(passedOptions?.headers)).toEqual({ Authorization: 'Bearer mock-auth-token', 'Custom-Header': 'custom-value', }); }); it('passes googleAuthOptions to generateAuthToken', async () => { createVertexAnthropicNode({ googleAuthOptions: { scopes: ['https://www.googleapis.com/auth/cloud-platform'], keyFile: 'path/to/key.json', }, }); expect(createVertexAnthropicOriginal).toHaveBeenCalledTimes(1); const passedOptions = vi.mocked(createVertexAnthropicOriginal).mock .calls[0][0]; await resolve(passedOptions?.headers); // call the headers function expect(generateAuthToken).toHaveBeenCalledWith({ scopes: ['https://www.googleapis.com/auth/cloud-platform'], keyFile: 'path/to/key.json', }); }); }); --- File: /ai/packages/google-vertex/src/anthropic/google-vertex-anthropic-provider-node.ts --- import { resolve } from '@ai-sdk/provider-utils'; import { GoogleAuthOptions } from 'google-auth-library'; import { generateAuthToken } from '../google-vertex-auth-google-auth-library'; import { createVertexAnthropic as createVertexAnthropicOriginal, GoogleVertexAnthropicProvider, GoogleVertexAnthropicProviderSettings as GoogleVertexAnthropicProviderSettingsOriginal, } from './google-vertex-anthropic-provider'; export type { GoogleVertexAnthropicProvider }; export interface GoogleVertexAnthropicProviderSettings extends GoogleVertexAnthropicProviderSettingsOriginal { /** Optional. The Authentication options provided by google-auth-library. Complete list of authentication options is documented in the GoogleAuthOptions interface: https://github.com/googleapis/google-auth-library-nodejs/blob/main/src/auth/googleauth.ts. */ googleAuthOptions?: GoogleAuthOptions; } export function createVertexAnthropic( options: GoogleVertexAnthropicProviderSettings = {}, ): GoogleVertexAnthropicProvider { return createVertexAnthropicOriginal({ ...options, headers: async () => ({ Authorization: `Bearer ${await generateAuthToken( options.googleAuthOptions, )}`, ...(await resolve(options.headers)), }), }); } /** Default Google Vertex Anthropic provider instance. */ export const vertexAnthropic = createVertexAnthropic(); --- File: /ai/packages/google-vertex/src/anthropic/google-vertex-anthropic-provider.test.ts --- import { createVertexAnthropic } from './google-vertex-anthropic-provider'; import { NoSuchModelError } from '@ai-sdk/provider'; import { AnthropicMessagesLanguageModel, anthropicTools, } from '@ai-sdk/anthropic/internal'; // Mock the imported modules vi.mock('@ai-sdk/provider-utils', () => ({ loadOptionalSetting: vi .fn() .mockImplementation(({ settingValue }) => settingValue), withoutTrailingSlash: vi.fn().mockImplementation(url => url), createJsonErrorResponseHandler: vi.fn(), createProviderDefinedToolFactory: vi.fn(), createProviderDefinedToolFactoryWithOutputSchema: vi.fn(), })); vi.mock('@ai-sdk/anthropic/internal', async () => { const originalModule = await vi.importActual('@ai-sdk/anthropic/internal'); return { ...originalModule, AnthropicMessagesLanguageModel: vi.fn(), }; }); describe('google-vertex-anthropic-provider', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should create a language model with default settings', () => { const provider = createVertexAnthropic({ project: 'test-project', location: 'test-location', }); provider('test-model-id'); // Assert that the model constructor was called with the correct arguments expect(AnthropicMessagesLanguageModel).toHaveBeenCalledWith( 'test-model-id', expect.objectContaining({ baseURL: expect.stringContaining( '/projects/test-project/locations/test-location/publishers/anthropic/models', ), provider: 'vertex.anthropic.messages', headers: expect.any(Object), buildRequestUrl: expect.any(Function), transformRequestBody: expect.any(Function), }), ); }); it('should throw an error when using new keyword', () => { const provider = createVertexAnthropic({ project: 'test-project' }); expect(() => new (provider as any)('test-model-id')).toThrow( 'The Anthropic model function cannot be called with the new keyword.', ); }); it('should pass baseURL to the model when created', () => { const customBaseURL = 'https://custom-url.com'; const provider = createVertexAnthropic({ project: 'test-project', baseURL: customBaseURL, }); provider('test-model-id'); // Assert that the constructor was called with the correct baseURL expect(AnthropicMessagesLanguageModel).toHaveBeenCalledWith( expect.anything(), // modelId expect.objectContaining({ baseURL: customBaseURL, }), ); }); it('should throw NoSuchModelError for textEmbeddingModel', () => { const provider = createVertexAnthropic({ project: 'test-project' }); expect(() => provider.textEmbeddingModel('invalid-model-id')).toThrow( NoSuchModelError, ); }); it('should include anthropicTools', () => { const provider = createVertexAnthropic({ project: 'test-project' }); expect(provider.tools).toBe(anthropicTools); }); it('should pass custom headers to the model constructor', () => { const customHeaders = { 'Custom-Header': 'custom-value' }; const provider = createVertexAnthropic({ project: 'test-project', headers: customHeaders, }); provider('test-model-id'); // Assert that the model constructor was called with the correct headers expect(AnthropicMessagesLanguageModel).toHaveBeenCalledWith( expect.anything(), // modelId expect.objectContaining({ headers: customHeaders, }), ); }); it('should create a Google Vertex Anthropic provider instance with custom settings', () => { const customProvider = createVertexAnthropic({ project: 'custom-project', location: 'custom-location', baseURL: 'https://custom.base.url', headers: { 'Custom-Header': 'value' }, }); expect(customProvider).toBeDefined(); expect(typeof customProvider).toBe('function'); expect(customProvider.languageModel).toBeDefined(); }); it('should not support URL sources to force base64 conversion', () => { const provider = createVertexAnthropic(); provider('test-model-id'); // Assert that the model constructor was called with supportedUrls function expect(AnthropicMessagesLanguageModel).toHaveBeenCalledWith( 'test-model-id', expect.objectContaining({ supportedUrls: expect.any(Function), }), ); // Get the actual config passed to the constructor const constructorCall = vi.mocked(AnthropicMessagesLanguageModel).mock .calls[vi.mocked(AnthropicMessagesLanguageModel).mock.calls.length - 1]; const config = constructorCall[1]; // Verify that supportedUrls returns empty object to force base64 conversion expect(config.supportedUrls?.()).toEqual({}); }); it('should use correct URL for global location', () => { const provider = createVertexAnthropic({ project: 'test-project', location: 'global', }); provider('test-model-id'); expect(AnthropicMessagesLanguageModel).toHaveBeenCalledWith( 'test-model-id', expect.objectContaining({ baseURL: 'https://aiplatform.googleapis.com/v1/projects/test-project/locations/global/publishers/anthropic/models', provider: 'vertex.anthropic.messages', }), ); }); it('should use region-prefixed URL for non-global locations', () => { const provider = createVertexAnthropic({ project: 'test-project', location: 'us-east5', }); provider('test-model-id'); expect(AnthropicMessagesLanguageModel).toHaveBeenCalledWith( 'test-model-id', expect.objectContaining({ baseURL: 'https://us-east5-aiplatform.googleapis.com/v1/projects/test-project/locations/us-east5/publishers/anthropic/models', provider: 'vertex.anthropic.messages', }), ); }); }); --- File: /ai/packages/google-vertex/src/anthropic/google-vertex-anthropic-provider.ts --- import { LanguageModelV2, NoSuchModelError, ProviderV2, } from '@ai-sdk/provider'; import { FetchFunction, Resolvable, loadOptionalSetting, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { anthropicTools, AnthropicMessagesLanguageModel, } from '@ai-sdk/anthropic/internal'; import { GoogleVertexAnthropicMessagesModelId } from './google-vertex-anthropic-messages-options'; export interface GoogleVertexAnthropicProvider extends ProviderV2 { /** Creates a model for text generation. */ (modelId: GoogleVertexAnthropicMessagesModelId): LanguageModelV2; /** Creates a model for text generation. */ languageModel(modelId: GoogleVertexAnthropicMessagesModelId): LanguageModelV2; /** Anthropic-specific computer use tool. */ tools: typeof anthropicTools; } export interface GoogleVertexAnthropicProviderSettings { /** * Google Cloud project ID. Defaults to the value of the `GOOGLE_VERTEX_PROJECT` environment variable. */ project?: string; /** * Google Cloud region. Defaults to the value of the `GOOGLE_VERTEX_LOCATION` environment variable. */ location?: string; /** Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.anthropic.com/v1`. */ baseURL?: string; /** Custom headers to include in the requests. */ headers?: Resolvable<Record<string, string | undefined>>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } /** Create a Google Vertex Anthropic provider instance. */ export function createVertexAnthropic( options: GoogleVertexAnthropicProviderSettings = {}, ): GoogleVertexAnthropicProvider { const location = loadOptionalSetting({ settingValue: options.location, environmentVariableName: 'GOOGLE_VERTEX_LOCATION', }); const project = loadOptionalSetting({ settingValue: options.project, environmentVariableName: 'GOOGLE_VERTEX_PROJECT', }); const baseURL = withoutTrailingSlash(options.baseURL) ?? `https://${location === 'global' ? '' : location + '-'}aiplatform.googleapis.com/v1/projects/${project}/locations/${location}/publishers/anthropic/models`; const createChatModel = (modelId: GoogleVertexAnthropicMessagesModelId) => new AnthropicMessagesLanguageModel(modelId, { provider: 'vertex.anthropic.messages', baseURL, headers: options.headers ?? {}, fetch: options.fetch, buildRequestUrl: (baseURL, isStreaming) => `${baseURL}/${modelId}:${ isStreaming ? 'streamRawPredict' : 'rawPredict' }`, transformRequestBody: args => { // Remove model from args and add anthropic version const { model, ...rest } = args; return { ...rest, anthropic_version: 'vertex-2023-10-16', }; }, // Google Vertex Anthropic doesn't support URL sources, force download and base64 conversion supportedUrls: () => ({}), }); const provider = function (modelId: GoogleVertexAnthropicMessagesModelId) { if (new.target) { throw new Error( 'The Anthropic model function cannot be called with the new keyword.', ); } return createChatModel(modelId); }; provider.languageModel = createChatModel; provider.chat = createChatModel; provider.messages = createChatModel; provider.textEmbeddingModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'textEmbeddingModel' }); }; provider.imageModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'imageModel' }); }; provider.tools = anthropicTools; return provider; } --- File: /ai/packages/google-vertex/src/anthropic/index.ts --- export { vertexAnthropic, createVertexAnthropic, } from './google-vertex-anthropic-provider-node'; export type { GoogleVertexAnthropicProvider, GoogleVertexAnthropicProviderSettings, } from './google-vertex-anthropic-provider-node'; --- File: /ai/packages/google-vertex/src/edge/google-vertex-auth-edge.test.ts --- import { generateAuthToken, GoogleCredentials, } from './google-vertex-auth-edge'; describe('Google Vertex Edge Auth', () => { const mockCredentials: GoogleCredentials = { clientEmail: 'test@test.iam.gserviceaccount.com', privateKey: 'mock-private-key', privateKeyId: 'test-key-id', }; const setupAtobStub = (credentials: typeof mockCredentials) => { vi.stubGlobal( 'atob', vi.fn().mockImplementation(str => { const payload = { alg: 'RS256', typ: 'JWT', iss: credentials.clientEmail, scope: 'https://www.googleapis.com/auth/cloud-platform', aud: 'https://oauth2.googleapis.com/token', iat: 1616161616, exp: 1616165216, }; if (credentials.privateKeyId) { Object.assign(payload, { kid: credentials.privateKeyId }); } return JSON.stringify(payload); }), ); }; beforeEach(() => { // Mock WebCrypto const mockSubtleCrypto = { importKey: vi.fn().mockResolvedValue('mock-crypto-key'), sign: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3])), }; const mockCrypto = { subtle: mockSubtleCrypto, }; // Use vi.stubGlobal instead of direct assignment vi.stubGlobal('crypto', mockCrypto); // Mock fetch global.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ access_token: 'mock.jwt.token' }), }); setupAtobStub(mockCredentials); }); afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); it('should generate a valid JWT token', async () => { // Mock successful token exchange global.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ access_token: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJ0ZXN0QHRlc3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvY2xvdWQtcGxhdGZvcm0iLCJhdWQiOiJodHRwczovL29hdXRoMi5nb29nbGVhcGlzLmNvbS90b2tlbiIsImlhdCI6MTYxNjE2MTYxNiwiZXhwIjoxNjE2MTY1MjE2fQ.signature', }), }); const token = await generateAuthToken(mockCredentials); // JWT structure validation const parts = token.split('.'); expect(parts).toHaveLength(3); // Header validation const header = JSON.parse(atob(parts[0])); expect(header).toMatchObject({ alg: 'RS256', typ: 'JWT', kid: mockCredentials.privateKeyId, iss: mockCredentials.clientEmail, }); // Payload validation const payload = JSON.parse(atob(parts[1])); expect(payload).toHaveProperty('iss', mockCredentials.clientEmail); expect(payload).toHaveProperty( 'scope', 'https://www.googleapis.com/auth/cloud-platform', ); expect(payload).toHaveProperty( 'aud', 'https://oauth2.googleapis.com/token', ); expect(payload).toHaveProperty('iat'); expect(payload).toHaveProperty('exp'); // Verify exp is ~1 hour after iat expect(payload.exp - payload.iat).toBe(3600); }); it('should throw error with invalid credentials', async () => { // Mock failed token exchange global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 400, statusText: 'Bad Request', json: () => Promise.resolve({ error: 'invalid_grant' }), }); const invalidCredentials = { ...mockCredentials, private_key: 'invalid-key', }; await expect(generateAuthToken(invalidCredentials)).rejects.toThrow( 'Token request failed: Bad Request', ); }); it('should load credentials from environment variables', async () => { process.env.GOOGLE_CLIENT_EMAIL = mockCredentials.clientEmail; process.env.GOOGLE_PRIVATE_KEY = mockCredentials.privateKey; process.env.GOOGLE_PRIVATE_KEY_ID = mockCredentials.privateKeyId; // Mock successful token exchange global.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ access_token: 'mock.jwt.token' }), }); const token = await generateAuthToken(); expect(token).toBeTruthy(); expect(token.split('.')).toHaveLength(3); // Clean up delete process.env.GOOGLE_CLIENT_EMAIL; delete process.env.GOOGLE_PRIVATE_KEY; delete process.env.GOOGLE_PRIVATE_KEY_ID; }); it('should throw error when client email is missing', async () => { delete process.env.GOOGLE_CLIENT_EMAIL; process.env.GOOGLE_PRIVATE_KEY = mockCredentials.privateKey; process.env.GOOGLE_PRIVATE_KEY_ID = mockCredentials.privateKeyId; await expect(generateAuthToken()).rejects.toThrow( "Google client email setting is missing. Pass it using the 'clientEmail' parameter or the GOOGLE_CLIENT_EMAIL environment variable.", ); // Clean up delete process.env.GOOGLE_PRIVATE_KEY; delete process.env.GOOGLE_PRIVATE_KEY_ID; }); it('should throw error when private key is missing', async () => { process.env.GOOGLE_CLIENT_EMAIL = mockCredentials.clientEmail; delete process.env.GOOGLE_PRIVATE_KEY; process.env.GOOGLE_PRIVATE_KEY_ID = mockCredentials.privateKeyId; await expect(generateAuthToken()).rejects.toThrow( "Google private key setting is missing. Pass it using the 'privateKey' parameter or the GOOGLE_PRIVATE_KEY environment variable.", ); // Clean up delete process.env.GOOGLE_CLIENT_EMAIL; delete process.env.GOOGLE_PRIVATE_KEY_ID; }); it('should work with or without private key ID', async () => { // Test with private key ID process.env.GOOGLE_CLIENT_EMAIL = mockCredentials.clientEmail; process.env.GOOGLE_PRIVATE_KEY = mockCredentials.privateKey; process.env.GOOGLE_PRIVATE_KEY_ID = mockCredentials.privateKeyId; // Mock successful token exchange global.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ access_token: 'mock.jwt.token' }), }); const tokenWithKeyId = await generateAuthToken(); expect(tokenWithKeyId).toBeTruthy(); // Test without private key ID delete process.env.GOOGLE_PRIVATE_KEY_ID; const tokenWithoutKeyId = await generateAuthToken(); expect(tokenWithoutKeyId).toBeTruthy(); // Clean up delete process.env.GOOGLE_CLIENT_EMAIL; delete process.env.GOOGLE_PRIVATE_KEY; }); it('should handle newlines in private key from env vars', async () => { process.env.GOOGLE_CLIENT_EMAIL = mockCredentials.clientEmail; process.env.GOOGLE_PRIVATE_KEY = mockCredentials.privateKey.replace( /\n/g, '\\n', ); process.env.GOOGLE_PRIVATE_KEY_ID = mockCredentials.privateKeyId; // Mock successful token exchange global.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ access_token: 'mock.jwt.token' }), }); const token = await generateAuthToken(); expect(token).toBeTruthy(); // Clean up delete process.env.GOOGLE_CLIENT_EMAIL; delete process.env.GOOGLE_PRIVATE_KEY; delete process.env.GOOGLE_PRIVATE_KEY_ID; }); it('should throw error on fetch failure', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); await expect(generateAuthToken(mockCredentials)).rejects.toThrow( 'Network error', ); consoleSpy.mockRestore(); }); it('should throw error when token request fails', async () => { // Mock a failed response from the token endpoint global.fetch = vi.fn().mockResolvedValue({ ok: false, statusText: 'Unauthorized', status: 401, json: () => Promise.resolve({ error: 'unauthorized' }), }); await expect(generateAuthToken(mockCredentials)).rejects.toThrow( 'Token request failed: Unauthorized', ); }); it('should work without privateKeyId', async () => { const credentialsWithoutKeyId = { clientEmail: mockCredentials.clientEmail, privateKey: mockCredentials.privateKey, }; setupAtobStub(credentialsWithoutKeyId); // Mock successful token exchange global.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ access_token: 'mock.jwt.token' }), }); const token = await generateAuthToken(credentialsWithoutKeyId); expect(token).toBeTruthy(); // Verify the JWT structure const parts = token.split('.'); expect(parts).toHaveLength(3); // Verify header doesn't include kid when privateKeyId is not provided const header = JSON.parse(atob(parts[0])); expect(header).not.toHaveProperty('kid'); }); }); --- File: /ai/packages/google-vertex/src/edge/google-vertex-auth-edge.ts --- import { loadOptionalSetting, loadSetting } from '@ai-sdk/provider-utils'; export interface GoogleCredentials { /** * The client email for the Google Cloud service account. Defaults to the * value of the `GOOGLE_CLIENT_EMAIL` environment variable. */ clientEmail: string; /** * The private key for the Google Cloud service account. Defaults to the * value of the `GOOGLE_PRIVATE_KEY` environment variable. */ privateKey: string; /** * Optional. The private key ID for the Google Cloud service account. Defaults * to the value of the `GOOGLE_PRIVATE_KEY_ID` environment variable. */ privateKeyId?: string; } const loadCredentials = async (): Promise<GoogleCredentials> => { try { return { clientEmail: loadSetting({ settingValue: undefined, settingName: 'clientEmail', environmentVariableName: 'GOOGLE_CLIENT_EMAIL', description: 'Google client email', }), privateKey: loadSetting({ settingValue: undefined, settingName: 'privateKey', environmentVariableName: 'GOOGLE_PRIVATE_KEY', description: 'Google private key', }), privateKeyId: loadOptionalSetting({ settingValue: undefined, environmentVariableName: 'GOOGLE_PRIVATE_KEY_ID', }), }; } catch (error: any) { throw new Error(`Failed to load Google credentials: ${error.message}`); } }; // Convert a string to base64url const base64url = (str: string) => { return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); }; const importPrivateKey = async (pemKey: string) => { const pemHeader = '-----BEGIN PRIVATE KEY-----'; const pemFooter = '-----END PRIVATE KEY-----'; // Remove header, footer, and any whitespace/newlines const pemContents = pemKey .replace(pemHeader, '') .replace(pemFooter, '') .replace(/\s/g, ''); // Decode base64 to binary const binaryString = atob(pemContents); // Convert binary string to Uint8Array const binaryData = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { binaryData[i] = binaryString.charCodeAt(i); } return await crypto.subtle.importKey( 'pkcs8', binaryData, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, true, ['sign'], ); }; const buildJwt = async (credentials: GoogleCredentials) => { const now = Math.floor(Date.now() / 1000); // Only include kid in header if privateKeyId is provided const header: { alg: string; typ: string; kid?: string } = { alg: 'RS256', typ: 'JWT', }; if (credentials.privateKeyId) { header.kid = credentials.privateKeyId; } const payload = { iss: credentials.clientEmail, scope: 'https://www.googleapis.com/auth/cloud-platform', aud: 'https://oauth2.googleapis.com/token', exp: now + 3600, iat: now, }; const privateKey = await importPrivateKey(credentials.privateKey); const signingInput = `${base64url(JSON.stringify(header))}.${base64url( JSON.stringify(payload), )}`; const encoder = new TextEncoder(); const data = encoder.encode(signingInput); const signature = await crypto.subtle.sign( 'RSASSA-PKCS1-v1_5', privateKey, data, ); const signatureBase64 = base64url( String.fromCharCode(...new Uint8Array(signature)), ); return `${base64url(JSON.stringify(header))}.${base64url( JSON.stringify(payload), )}.${signatureBase64}`; }; /** * Generate an authentication token for Google Vertex AI in a manner compatible * with the Edge runtime. */ export async function generateAuthToken(credentials?: GoogleCredentials) { try { const creds = credentials || (await loadCredentials()); const jwt = await buildJwt(creds); const response = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: jwt, }), }); if (!response.ok) { throw new Error(`Token request failed: ${response.statusText}`); } const data = await response.json(); return data.access_token; } catch (error) { throw error; } } --- File: /ai/packages/google-vertex/src/edge/google-vertex-provider-edge.test.ts --- import { resolve } from '@ai-sdk/provider-utils'; import { createVertex as createVertexEdge } from './google-vertex-provider-edge'; import { createVertex as createVertexOriginal } from '../google-vertex-provider'; import * as edgeAuth from './google-vertex-auth-edge'; // Mock the imported modules vi.mock('./google-vertex-auth-edge', () => ({ generateAuthToken: vi.fn().mockResolvedValue('mock-auth-token'), })); vi.mock('../google-vertex-provider', () => ({ createVertex: vi.fn().mockImplementation(options => ({ ...options, })), })); describe('google-vertex-provider-edge', () => { beforeEach(() => { vi.clearAllMocks(); }); it('default headers function should return auth token', async () => { createVertexEdge({ project: 'test-project' }); const mockCreateVertex = vi.mocked(createVertexOriginal); const passedOptions = mockCreateVertex.mock.calls[0][0]; expect(mockCreateVertex).toHaveBeenCalledTimes(1); expect(typeof passedOptions?.headers).toBe('function'); expect(await resolve(passedOptions?.headers)).toStrictEqual({ Authorization: 'Bearer mock-auth-token', }); }); it('should use custom headers in addition to auth token when provided', async () => { createVertexEdge({ project: 'test-project', headers: async () => ({ 'Custom-Header': 'custom-value', }), }); const mockCreateVertex = vi.mocked(createVertexOriginal); const passedOptions = mockCreateVertex.mock.calls[0][0]; expect(mockCreateVertex).toHaveBeenCalledTimes(1); expect(typeof passedOptions?.headers).toBe('function'); expect(await resolve(passedOptions?.headers)).toEqual({ Authorization: 'Bearer mock-auth-token', 'Custom-Header': 'custom-value', }); }); it('should use edge auth token generator', async () => { createVertexEdge({ project: 'test-project' }); const mockCreateVertex = vi.mocked(createVertexOriginal); const passedOptions = mockCreateVertex.mock.calls[0][0]; // Verify the headers function actually calls generateAuthToken by checking its result expect(passedOptions?.headers).toBeDefined(); await resolve(passedOptions?.headers); expect(edgeAuth.generateAuthToken).toHaveBeenCalled(); }); it('passes googleCredentials to generateAuthToken', async () => { createVertexEdge({ project: 'test-project', googleCredentials: { clientEmail: 'test@example.com', privateKey: 'test-key', }, }); const mockCreateVertex = vi.mocked(createVertexOriginal); const passedOptions = mockCreateVertex.mock.calls[0][0]; await resolve(passedOptions?.headers); // call the headers function expect(edgeAuth.generateAuthToken).toHaveBeenCalledWith({ clientEmail: 'test@example.com', privateKey: 'test-key', }); }); }); --- File: /ai/packages/google-vertex/src/edge/google-vertex-provider-edge.ts --- import { resolve } from '@ai-sdk/provider-utils'; import { createVertex as createVertexOriginal, GoogleVertexProvider, GoogleVertexProviderSettings as GoogleVertexProviderSettingsOriginal, } from '../google-vertex-provider'; import { generateAuthToken, GoogleCredentials, } from './google-vertex-auth-edge'; export type { GoogleVertexProvider }; export interface GoogleVertexProviderSettings extends GoogleVertexProviderSettingsOriginal { /** * Optional. The Google credentials for the Google Cloud service account. If * not provided, the Google Vertex provider will use environment variables to * load the credentials. */ googleCredentials?: GoogleCredentials; } export function createVertex( options: GoogleVertexProviderSettings = {}, ): GoogleVertexProvider { return createVertexOriginal({ ...options, headers: async () => ({ Authorization: `Bearer ${await generateAuthToken( options.googleCredentials, )}`, ...(await resolve(options.headers)), }), }); } /** Default Google Vertex AI provider instance. */ export const vertex = createVertex(); --- File: /ai/packages/google-vertex/src/edge/index.ts --- export { createVertex, vertex } from './google-vertex-provider-edge'; export type { GoogleVertexProviderSettings, GoogleVertexProvider, } from './google-vertex-provider-edge'; --- File: /ai/packages/google-vertex/src/google-vertex-auth-google-auth-library.test.ts --- import { describe, it, expect, vi, beforeEach } from 'vitest'; import { generateAuthToken, _resetAuthInstance, } from './google-vertex-auth-google-auth-library'; import { GoogleAuth } from 'google-auth-library'; vi.mock('google-auth-library', () => { return { GoogleAuth: vi.fn().mockImplementation(() => { return { getClient: vi.fn().mockResolvedValue({ getAccessToken: vi.fn().mockResolvedValue({ token: 'mocked-token' }), }), }; }), }; }); describe('generateAuthToken', () => { beforeEach(() => { vi.clearAllMocks(); _resetAuthInstance(); }); it('should generate a valid auth token', async () => { const token = await generateAuthToken(); expect(token).toBe('mocked-token'); }); it('should return null if no token is received', async () => { // Reset the mock completely vi.mocked(GoogleAuth).mockReset(); // Create a new mock implementation vi.mocked(GoogleAuth).mockImplementation( () => ({ getClient: vi.fn().mockResolvedValue({ getAccessToken: vi.fn().mockResolvedValue({ token: null }), }), isGCE: vi.fn(), }) as unknown as GoogleAuth, ); const token = await generateAuthToken(); expect(token).toBeNull(); }); it('should create new auth instance with provided options', async () => { const options = { keyFile: 'test-key.json' }; await generateAuthToken(options); expect(GoogleAuth).toHaveBeenCalledWith({ scopes: ['https://www.googleapis.com/auth/cloud-platform'], keyFile: 'test-key.json', }); }); }); --- File: /ai/packages/google-vertex/src/google-vertex-auth-google-auth-library.ts --- import { GoogleAuth, GoogleAuthOptions } from 'google-auth-library'; let authInstance: GoogleAuth | null = null; let authOptions: GoogleAuthOptions | null = null; function getAuth(options: GoogleAuthOptions) { if (!authInstance || options !== authOptions) { authInstance = new GoogleAuth({ scopes: ['https://www.googleapis.com/auth/cloud-platform'], ...options, }); authOptions = options; } return authInstance; } export async function generateAuthToken(options?: GoogleAuthOptions) { const auth = getAuth(options || {}); const client = await auth.getClient(); const token = await client.getAccessToken(); return token?.token || null; } // For testing purposes only export function _resetAuthInstance() { authInstance = null; } --- File: /ai/packages/google-vertex/src/google-vertex-config.ts --- import { FetchFunction, Resolvable } from '@ai-sdk/provider-utils'; export interface GoogleVertexConfig { provider: string; baseURL: string; headers: Resolvable<Record<string, string | undefined>>; fetch?: FetchFunction; } --- File: /ai/packages/google-vertex/src/google-vertex-embedding-model.test.ts --- import { EmbeddingModelV2Embedding, TooManyEmbeddingValuesForCallError, } from '@ai-sdk/provider'; import { createTestServer } from '@ai-sdk/provider-utils/test'; import { GoogleVertexEmbeddingModel } from './google-vertex-embedding-model'; const dummyEmbeddings = [ [0.1, 0.2, 0.3], [0.4, 0.5, 0.6], ]; const testValues = ['test text one', 'test text two']; const DEFAULT_URL = 'https://us-central1-aiplatform.googleapis.com/v1/projects/test-project/locations/us-central1/publishers/google/models/textembedding-gecko@001:predict'; const CUSTOM_URL = 'https://custom-endpoint.com/models/textembedding-gecko@001:predict'; const server = createTestServer({ [DEFAULT_URL]: {}, [CUSTOM_URL]: {}, }); describe('GoogleVertexEmbeddingModel', () => { const mockModelId = 'textembedding-gecko@001'; const mockProviderOptions = { outputDimensionality: 768, taskType: 'SEMANTIC_SIMILARITY', }; const mockConfig = { provider: 'google-vertex', region: 'us-central1', project: 'test-project', headers: () => ({}), baseURL: 'https://us-central1-aiplatform.googleapis.com/v1/projects/test-project/locations/us-central1/publishers/google', }; const model = new GoogleVertexEmbeddingModel(mockModelId, mockConfig); function prepareJsonResponse({ embeddings = dummyEmbeddings, tokenCounts = [1, 1], headers, }: { embeddings?: EmbeddingModelV2Embedding[]; tokenCounts?: number[]; headers?: Record<string, string>; } = {}) { server.urls[DEFAULT_URL].response = { type: 'json-value', headers, body: { predictions: embeddings.map((values, i) => ({ embeddings: { values, statistics: { token_count: tokenCounts[i] }, }, })), }, }; } it('should extract embeddings', async () => { prepareJsonResponse(); const { embeddings } = await model.doEmbed({ values: testValues, providerOptions: { google: mockProviderOptions }, }); expect(embeddings).toStrictEqual(dummyEmbeddings); }); it('should expose the raw response', async () => { prepareJsonResponse({ headers: { 'test-header': 'test-value', }, }); const { response } = await model.doEmbed({ values: testValues, providerOptions: { google: mockProviderOptions }, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-length': '159', 'content-type': 'application/json', // custom header 'test-header': 'test-value', }); expect(response).toMatchSnapshot(); }); it('should extract usage', async () => { prepareJsonResponse({ tokenCounts: [10, 15], }); const { usage } = await model.doEmbed({ values: testValues, providerOptions: { google: mockProviderOptions }, }); expect(usage).toStrictEqual({ tokens: 25 }); }); it('should pass the model parameters correctly', async () => { prepareJsonResponse(); await model.doEmbed({ values: testValues, providerOptions: { google: mockProviderOptions }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ instances: testValues.map(value => ({ content: value })), parameters: { outputDimensionality: mockProviderOptions.outputDimensionality, taskType: mockProviderOptions.taskType, }, }); }); it('should pass the taskType setting', async () => { prepareJsonResponse(); await model.doEmbed({ values: testValues, providerOptions: { google: { taskType: mockProviderOptions.taskType } }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ instances: testValues.map(value => ({ content: value })), parameters: { taskType: mockProviderOptions.taskType, }, }); }); it('should pass headers correctly', async () => { prepareJsonResponse(); const model = new GoogleVertexEmbeddingModel(mockModelId, { ...mockConfig, headers: () => ({ 'X-Custom-Header': 'custom-value', }), }); await model.doEmbed({ values: testValues, headers: { 'X-Request-Header': 'request-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'x-custom-header': 'custom-value', 'x-request-header': 'request-value', }); }); it('should throw TooManyEmbeddingValuesForCallError when too many values provided', async () => { const tooManyValues = Array(2049).fill('test'); await expect( model.doEmbed({ values: tooManyValues, providerOptions: { google: mockProviderOptions }, }), ).rejects.toThrow(TooManyEmbeddingValuesForCallError); }); it('should use custom baseURL when provided', async () => { server.urls[CUSTOM_URL].response = { type: 'json-value', body: { predictions: dummyEmbeddings.map(values => ({ embeddings: { values, statistics: { token_count: 1 }, }, })), }, }; const modelWithCustomUrl = new GoogleVertexEmbeddingModel( 'textembedding-gecko@001', { headers: () => ({}), baseURL: 'https://custom-endpoint.com', provider: 'google-vertex', }, ); const response = await modelWithCustomUrl.doEmbed({ values: testValues, providerOptions: { google: { outputDimensionality: 768 }, }, }); expect(response.embeddings).toStrictEqual(dummyEmbeddings); expect(server.calls[0].requestUrl).toBe( 'https://custom-endpoint.com/models/textembedding-gecko@001:predict', ); }); it('should use custom fetch when provided and include proper request content', async () => { const customFetch = vi.fn().mockResolvedValue( new Response( JSON.stringify({ predictions: dummyEmbeddings.map(values => ({ embeddings: { values, statistics: { token_count: 1 }, }, })), }), ), ); const modelWithCustomFetch = new GoogleVertexEmbeddingModel( 'textembedding-gecko@001', { headers: () => ({}), baseURL: 'https://custom-endpoint.com', provider: 'google-vertex', fetch: customFetch, }, ); const response = await modelWithCustomFetch.doEmbed({ values: testValues, providerOptions: { google: { outputDimensionality: 768 }, }, }); expect(response.embeddings).toStrictEqual(dummyEmbeddings); expect(customFetch).toHaveBeenCalledWith(CUSTOM_URL, expect.any(Object)); const [_, secondArgument] = customFetch.mock.calls[0]; const requestBody = JSON.parse(secondArgument.body); expect(requestBody).toStrictEqual({ instances: testValues.map(value => ({ content: value })), parameters: { outputDimensionality: 768, }, }); }); }); --- File: /ai/packages/google-vertex/src/google-vertex-embedding-model.ts --- import { EmbeddingModelV2, TooManyEmbeddingValuesForCallError, } from '@ai-sdk/provider'; import { combineHeaders, createJsonResponseHandler, postJsonToApi, resolve, Resolvable, parseProviderOptions, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { googleVertexFailedResponseHandler } from './google-vertex-error'; import { GoogleVertexEmbeddingModelId, googleVertexEmbeddingProviderOptions, } from './google-vertex-embedding-options'; import { GoogleVertexConfig } from './google-vertex-config'; export class GoogleVertexEmbeddingModel implements EmbeddingModelV2<string> { readonly specificationVersion = 'v2'; readonly modelId: GoogleVertexEmbeddingModelId; readonly maxEmbeddingsPerCall = 2048; readonly supportsParallelCalls = true; private readonly config: GoogleVertexConfig; get provider(): string { return this.config.provider; } constructor( modelId: GoogleVertexEmbeddingModelId, config: GoogleVertexConfig, ) { this.modelId = modelId; this.config = config; } async doEmbed({ values, headers, abortSignal, providerOptions, }: Parameters<EmbeddingModelV2<string>['doEmbed']>[0]): Promise< Awaited<ReturnType<EmbeddingModelV2<string>['doEmbed']>> > { // Parse provider options const googleOptions = (await parseProviderOptions({ provider: 'google', providerOptions, schema: googleVertexEmbeddingProviderOptions, })) ?? {}; if (values.length > this.maxEmbeddingsPerCall) { throw new TooManyEmbeddingValuesForCallError({ provider: this.provider, modelId: this.modelId, maxEmbeddingsPerCall: this.maxEmbeddingsPerCall, values, }); } const mergedHeaders = combineHeaders( await resolve(this.config.headers), headers, ); const url = `${this.config.baseURL}/models/${this.modelId}:predict`; const { responseHeaders, value: response, rawValue, } = await postJsonToApi({ url, headers: mergedHeaders, body: { instances: values.map(value => ({ content: value })), parameters: { outputDimensionality: googleOptions.outputDimensionality, taskType: googleOptions.taskType, }, }, failedResponseHandler: googleVertexFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( googleVertexTextEmbeddingResponseSchema, ), abortSignal, fetch: this.config.fetch, }); return { embeddings: response.predictions.map( prediction => prediction.embeddings.values, ), usage: { tokens: response.predictions.reduce( (tokenCount, prediction) => tokenCount + prediction.embeddings.statistics.token_count, 0, ), }, response: { headers: responseHeaders, body: rawValue }, }; } } // minimal version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const googleVertexTextEmbeddingResponseSchema = z.object({ predictions: z.array( z.object({ embeddings: z.object({ values: z.array(z.number()), statistics: z.object({ token_count: z.number(), }), }), }), ), }); --- File: /ai/packages/google-vertex/src/google-vertex-embedding-options.ts --- import { z } from 'zod/v4'; // https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/text-embeddings-api export type GoogleVertexEmbeddingModelId = | 'textembedding-gecko' | 'textembedding-gecko@001' | 'textembedding-gecko@003' | 'textembedding-gecko-multilingual' | 'textembedding-gecko-multilingual@001' | 'text-multilingual-embedding-002' | 'text-embedding-004' | 'text-embedding-005' | (string & {}); export const googleVertexEmbeddingProviderOptions = z.object({ /** * Optional. Optional reduced dimension for the output embedding. * If set, excessive values in the output embedding are truncated from the end. */ outputDimensionality: z.number().optional(), /** * Optional. Specifies the task type for generating embeddings. * Supported task types: * - SEMANTIC_SIMILARITY: Optimized for text similarity. * - CLASSIFICATION: Optimized for text classification. * - CLUSTERING: Optimized for clustering texts based on similarity. * - RETRIEVAL_DOCUMENT: Optimized for document retrieval. * - RETRIEVAL_QUERY: Optimized for query-based retrieval. * - QUESTION_ANSWERING: Optimized for answering questions. * - FACT_VERIFICATION: Optimized for verifying factual information. * - CODE_RETRIEVAL_QUERY: Optimized for retrieving code blocks based on natural language queries. */ taskType: z .enum([ 'SEMANTIC_SIMILARITY', 'CLASSIFICATION', 'CLUSTERING', 'RETRIEVAL_DOCUMENT', 'RETRIEVAL_QUERY', 'QUESTION_ANSWERING', 'FACT_VERIFICATION', 'CODE_RETRIEVAL_QUERY', ]) .optional(), }); export type GoogleVertexEmbeddingProviderOptions = z.infer< typeof googleVertexEmbeddingProviderOptions >; --- File: /ai/packages/google-vertex/src/google-vertex-error.ts --- import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; const googleVertexErrorDataSchema = z.object({ error: z.object({ code: z.number().nullable(), message: z.string(), status: z.string(), }), }); export type GoogleVertexErrorData = z.infer<typeof googleVertexErrorDataSchema>; export const googleVertexFailedResponseHandler = createJsonErrorResponseHandler( { errorSchema: googleVertexErrorDataSchema, errorToMessage: data => data.error.message, }, ); --- File: /ai/packages/google-vertex/src/google-vertex-image-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { GoogleVertexImageModel } from './google-vertex-image-model'; const prompt = 'A cute baby sea otter'; const model = new GoogleVertexImageModel('imagen-3.0-generate-002', { provider: 'google-vertex', baseURL: 'https://api.example.com', headers: { 'api-key': 'test-key' }, }); const server = createTestServer({ 'https://api.example.com/models/imagen-3.0-generate-002:predict': {}, 'https://api.example.com/models/imagen-4.0-generate-preview-06-06:predict': {}, 'https://api.example.com/models/imagen-4.0-fast-generate-preview-06-06:predict': {}, 'https://api.example.com/models/imagen-4.0-ultra-generate-preview-06-06:predict': {}, }); describe('GoogleVertexImageModel', () => { describe('doGenerate', () => { function prepareJsonResponse({ headers, }: { headers?: Record<string, string>; } = {}) { server.urls[ 'https://api.example.com/models/imagen-3.0-generate-002:predict' ].response = { type: 'json-value', headers, body: { predictions: [ { mimeType: 'image/png', prompt: 'revised prompt 1', bytesBase64Encoded: 'base64-image-1', }, { mimeType: 'image/png', prompt: 'revised prompt 2', bytesBase64Encoded: 'base64-image-2', someFutureField: 'some future value', }, ], }, }; } it('should pass headers', async () => { prepareJsonResponse(); const modelWithHeaders = new GoogleVertexImageModel( 'imagen-3.0-generate-002', { provider: 'google-vertex', baseURL: 'https://api.example.com', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }, ); await modelWithHeaders.doGenerate({ prompt, n: 2, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should use default maxImagesPerCall when not specified', () => { const defaultModel = new GoogleVertexImageModel( 'imagen-3.0-generate-002', { provider: 'google-vertex', baseURL: 'https://api.example.com', headers: { 'api-key': 'test-key' }, }, ); expect(defaultModel.maxImagesPerCall).toBe(4); }); it('should extract the generated images', async () => { prepareJsonResponse(); const result = await model.doGenerate({ prompt, n: 2, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.images).toStrictEqual(['base64-image-1', 'base64-image-2']); }); it('sends aspect ratio in the request', async () => { prepareJsonResponse(); await model.doGenerate({ prompt: 'test prompt', n: 1, size: undefined, aspectRatio: '16:9', seed: undefined, providerOptions: {}, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ instances: [{ prompt: 'test prompt' }], parameters: { sampleCount: 1, aspectRatio: '16:9', }, }); }); it('should pass aspect ratio directly when specified', async () => { prepareJsonResponse(); await model.doGenerate({ prompt: 'test prompt', n: 1, size: undefined, aspectRatio: '16:9', seed: undefined, providerOptions: {}, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ instances: [{ prompt: 'test prompt' }], parameters: { sampleCount: 1, aspectRatio: '16:9', }, }); }); it('should pass seed directly when specified', async () => { prepareJsonResponse(); await model.doGenerate({ prompt: 'test prompt', n: 1, size: undefined, aspectRatio: undefined, seed: 42, providerOptions: {}, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ instances: [{ prompt: 'test prompt' }], parameters: { sampleCount: 1, seed: 42, }, }); }); it('should combine aspectRatio, seed and provider options', async () => { prepareJsonResponse(); await model.doGenerate({ prompt: 'test prompt', n: 1, size: undefined, aspectRatio: '1:1', seed: 42, providerOptions: { vertex: { addWatermark: false, }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ instances: [{ prompt: 'test prompt' }], parameters: { sampleCount: 1, aspectRatio: '1:1', seed: 42, addWatermark: false, }, }); }); it('should return warnings for unsupported settings', async () => { prepareJsonResponse(); const result = await model.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: '1:1', seed: 123, providerOptions: {}, }); expect(result.warnings).toStrictEqual([ { type: 'unsupported-setting', setting: 'size', details: 'This model does not support the `size` option. Use `aspectRatio` instead.', }, ]); }); it('should include response data with timestamp, modelId and headers', async () => { prepareJsonResponse({ headers: { 'request-id': 'test-request-id', 'x-goog-quota-remaining': '123', }, }); const testDate = new Date('2024-03-15T12:00:00Z'); const customModel = new GoogleVertexImageModel( 'imagen-3.0-generate-002', { provider: 'google-vertex', baseURL: 'https://api.example.com', headers: { 'api-key': 'test-key' }, _internal: { currentDate: () => testDate, }, }, ); const result = await customModel.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.response).toStrictEqual({ timestamp: testDate, modelId: 'imagen-3.0-generate-002', headers: { 'content-length': '237', 'content-type': 'application/json', 'request-id': 'test-request-id', 'x-goog-quota-remaining': '123', }, }); }); it('should use real date when no custom date provider is specified', async () => { prepareJsonResponse(); const beforeDate = new Date(); const result = await model.doGenerate({ prompt, n: 2, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); const afterDate = new Date(); expect(result.response.timestamp.getTime()).toBeGreaterThanOrEqual( beforeDate.getTime(), ); expect(result.response.timestamp.getTime()).toBeLessThanOrEqual( afterDate.getTime(), ); expect(result.response.modelId).toBe('imagen-3.0-generate-002'); }); it('should only pass valid provider options', async () => { prepareJsonResponse(); await model.doGenerate({ prompt, n: 2, size: undefined, aspectRatio: '16:9', seed: undefined, providerOptions: { vertex: { addWatermark: false, negativePrompt: 'negative prompt', personGeneration: 'allow_all', foo: 'bar', }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ instances: [{ prompt }], parameters: { sampleCount: 2, addWatermark: false, negativePrompt: 'negative prompt', personGeneration: 'allow_all', aspectRatio: '16:9', }, }); }); it('should return image meta data', async () => { prepareJsonResponse(); const result = await model.doGenerate({ prompt, n: 2, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.providerMetadata?.vertex).toStrictEqual({ images: [ { revisedPrompt: 'revised prompt 1', }, { revisedPrompt: 'revised prompt 2', }, ], }); }); }); describe('Imagen 4 Models', () => { describe('imagen-4.0-generate-preview-06-06', () => { const imagen4Model = new GoogleVertexImageModel( 'imagen-4.0-generate-preview-06-06', { provider: 'google-vertex', baseURL: 'https://api.example.com', headers: { 'api-key': 'test-key' }, }, ); function prepareImagen4Response() { server.urls[ 'https://api.example.com/models/imagen-4.0-generate-preview-06-06:predict' ].response = { type: 'json-value', body: { predictions: [ { mimeType: 'image/png', prompt: 'revised imagen 4 prompt', bytesBase64Encoded: 'base64-imagen4-image', }, ], }, }; } it('should generate images with Imagen 4', async () => { prepareImagen4Response(); const result = await imagen4Model.doGenerate({ prompt: 'A beautiful sunset over mountains', n: 1, size: undefined, aspectRatio: '16:9', seed: 42, providerOptions: { vertex: { addWatermark: false, }, }, }); expect(result.images).toStrictEqual(['base64-imagen4-image']); expect(result.providerMetadata?.vertex).toStrictEqual({ images: [ { revisedPrompt: 'revised imagen 4 prompt', }, ], }); }); it('should send correct request parameters for Imagen 4', async () => { prepareImagen4Response(); await imagen4Model.doGenerate({ prompt: 'test imagen 4 prompt', n: 2, size: undefined, aspectRatio: '1:1', seed: 123, providerOptions: { vertex: { personGeneration: 'allow_adult', safetySetting: 'block_medium_and_above', }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ instances: [{ prompt: 'test imagen 4 prompt' }], parameters: { sampleCount: 2, aspectRatio: '1:1', seed: 123, personGeneration: 'allow_adult', safetySetting: 'block_medium_and_above', }, }); }); }); describe('imagen-4.0-fast-generate-preview-06-06', () => { const imagen4FastModel = new GoogleVertexImageModel( 'imagen-4.0-fast-generate-preview-06-06', { provider: 'google-vertex', baseURL: 'https://api.example.com', headers: { 'api-key': 'test-key' }, }, ); function prepareImagen4FastResponse() { server.urls[ 'https://api.example.com/models/imagen-4.0-fast-generate-preview-06-06:predict' ].response = { type: 'json-value', body: { predictions: [ { mimeType: 'image/png', prompt: 'revised imagen 4 fast prompt', bytesBase64Encoded: 'base64-imagen4-fast-image', }, ], }, }; } it('should generate images with Imagen 4 Fast', async () => { prepareImagen4FastResponse(); const result = await imagen4FastModel.doGenerate({ prompt: 'A quick sketch of a cat', n: 1, size: undefined, aspectRatio: '3:4', seed: undefined, providerOptions: {}, }); expect(result.images).toStrictEqual(['base64-imagen4-fast-image']); expect(result.providerMetadata?.vertex).toStrictEqual({ images: [ { revisedPrompt: 'revised imagen 4 fast prompt', }, ], }); }); }); describe('imagen-4.0-ultra-generate-preview-06-06', () => { const imagen4UltraModel = new GoogleVertexImageModel( 'imagen-4.0-ultra-generate-preview-06-06', { provider: 'google-vertex', baseURL: 'https://api.example.com', headers: { 'api-key': 'test-key' }, }, ); function prepareImagen4UltraResponse() { server.urls[ 'https://api.example.com/models/imagen-4.0-ultra-generate-preview-06-06:predict' ].response = { type: 'json-value', body: { predictions: [ { mimeType: 'image/png', prompt: 'revised imagen 4 ultra prompt', bytesBase64Encoded: 'base64-imagen4-ultra-image', }, ], }, }; } it('should generate images with Imagen 4 Ultra', async () => { prepareImagen4UltraResponse(); const result = await imagen4UltraModel.doGenerate({ prompt: 'A highly detailed photorealistic portrait', n: 1, size: undefined, aspectRatio: '4:3', seed: 999, providerOptions: { vertex: { negativePrompt: 'blurry, low quality', addWatermark: true, }, }, }); expect(result.images).toStrictEqual(['base64-imagen4-ultra-image']); expect(result.providerMetadata?.vertex).toStrictEqual({ images: [ { revisedPrompt: 'revised imagen 4 ultra prompt', }, ], }); }); it('should handle all provider options with Imagen 4 Ultra', async () => { prepareImagen4UltraResponse(); await imagen4UltraModel.doGenerate({ prompt: 'comprehensive test prompt', n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: { vertex: { negativePrompt: 'avoid this content', personGeneration: 'dont_allow', safetySetting: 'block_only_high', addWatermark: true, storageUri: 'gs://my-bucket/images/', }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ instances: [{ prompt: 'comprehensive test prompt' }], parameters: { sampleCount: 1, negativePrompt: 'avoid this content', personGeneration: 'dont_allow', safetySetting: 'block_only_high', addWatermark: true, storageUri: 'gs://my-bucket/images/', }, }); }); }); }); }); --- File: /ai/packages/google-vertex/src/google-vertex-image-model.ts --- import { ImageModelV2, ImageModelV2CallWarning } from '@ai-sdk/provider'; import { Resolvable, combineHeaders, createJsonResponseHandler, parseProviderOptions, postJsonToApi, resolve, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { googleVertexFailedResponseHandler } from './google-vertex-error'; import { GoogleVertexImageModelId } from './google-vertex-image-settings'; interface GoogleVertexImageModelConfig { provider: string; baseURL: string; headers?: Resolvable<Record<string, string | undefined>>; fetch?: typeof fetch; _internal?: { currentDate?: () => Date; }; } // https://cloud.google.com/vertex-ai/generative-ai/docs/image/generate-images export class GoogleVertexImageModel implements ImageModelV2 { readonly specificationVersion = 'v2'; // https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/imagen-api#parameter_list readonly maxImagesPerCall = 4; get provider(): string { return this.config.provider; } constructor( readonly modelId: GoogleVertexImageModelId, private config: GoogleVertexImageModelConfig, ) {} async doGenerate({ prompt, n, size, aspectRatio, seed, providerOptions, headers, abortSignal, }: Parameters<ImageModelV2['doGenerate']>[0]): Promise< Awaited<ReturnType<ImageModelV2['doGenerate']>> > { const warnings: Array<ImageModelV2CallWarning> = []; if (size != null) { warnings.push({ type: 'unsupported-setting', setting: 'size', details: 'This model does not support the `size` option. Use `aspectRatio` instead.', }); } const vertexImageOptions = await parseProviderOptions({ provider: 'vertex', providerOptions, schema: vertexImageProviderOptionsSchema, }); const body = { instances: [{ prompt }], parameters: { sampleCount: n, ...(aspectRatio != null ? { aspectRatio } : {}), ...(seed != null ? { seed } : {}), ...(vertexImageOptions ?? {}), }, }; const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { value: response, responseHeaders } = await postJsonToApi({ url: `${this.config.baseURL}/models/${this.modelId}:predict`, headers: combineHeaders(await resolve(this.config.headers), headers), body, failedResponseHandler: googleVertexFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( vertexImageResponseSchema, ), abortSignal, fetch: this.config.fetch, }); return { images: response.predictions?.map( ({ bytesBase64Encoded }) => bytesBase64Encoded, ) ?? [], warnings, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, }, providerMetadata: { vertex: { images: response.predictions?.map(prediction => { const { // normalize revised prompt property prompt: revisedPrompt, } = prediction; return { ...(revisedPrompt != null && { revisedPrompt }) }; }) ?? [], }, }, }; } } // minimal version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const vertexImageResponseSchema = z.object({ predictions: z .array( z.object({ bytesBase64Encoded: z.string(), mimeType: z.string(), prompt: z.string().nullish(), }), ) .nullish(), }); const vertexImageProviderOptionsSchema = z.object({ negativePrompt: z.string().nullish(), personGeneration: z .enum(['dont_allow', 'allow_adult', 'allow_all']) .nullish(), safetySetting: z .enum([ 'block_low_and_above', 'block_medium_and_above', 'block_only_high', 'block_none', ]) .nullish(), addWatermark: z.boolean().nullish(), storageUri: z.string().nullish(), }); export type GoogleVertexImageProviderOptions = z.infer< typeof vertexImageProviderOptionsSchema >; --- File: /ai/packages/google-vertex/src/google-vertex-image-settings.ts --- export type GoogleVertexImageModelId = | 'imagen-3.0-generate-001' | 'imagen-3.0-generate-002' | 'imagen-3.0-fast-generate-001' | 'imagen-4.0-generate-preview-06-06' | 'imagen-4.0-fast-generate-preview-06-06' | 'imagen-4.0-ultra-generate-preview-06-06' | (string & {}); --- File: /ai/packages/google-vertex/src/google-vertex-options.ts --- // https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#supported-models // Note preview and experimental models may only be detailed in AI Studio: // https://console.cloud.google.com/vertex-ai/studio/ export type GoogleVertexModelId = // Stable models | 'gemini-2.0-flash-001' | 'gemini-1.5-flash' | 'gemini-1.5-flash-001' | 'gemini-1.5-flash-002' | 'gemini-1.5-pro' | 'gemini-1.5-pro-001' | 'gemini-1.5-pro-002' | 'gemini-1.0-pro-001' | 'gemini-1.0-pro-vision-001' | 'gemini-1.0-pro' | 'gemini-1.0-pro-001' | 'gemini-1.0-pro-002' // Preview models | 'gemini-2.0-flash-lite-preview-02-05' // Experimental models | 'gemini-2.0-pro-exp-02-05' | 'gemini-2.0-flash-exp' | (string & {}); --- File: /ai/packages/google-vertex/src/google-vertex-provider-node.test.ts --- import { resolve } from '@ai-sdk/provider-utils'; import { createVertex as createVertexOriginal } from './google-vertex-provider'; import { createVertex as createVertexNode } from './google-vertex-provider-node'; import { generateAuthToken } from './google-vertex-auth-google-auth-library'; // Mock the imported modules vi.mock('./google-vertex-auth-google-auth-library', () => ({ generateAuthToken: vi.fn().mockResolvedValue('mock-auth-token'), })); vi.mock('./google-vertex-provider', () => ({ createVertex: vi.fn().mockImplementation(options => ({ ...options, })), })); describe('google-vertex-provider-node', () => { beforeEach(() => { vi.clearAllMocks(); }); it('default headers function should return auth token', async () => { createVertexNode({ project: 'test-project' }); expect(createVertexOriginal).toHaveBeenCalledTimes(1); const passedOptions = vi.mocked(createVertexOriginal).mock.calls[0][0]; expect(typeof passedOptions?.headers).toBe('function'); expect(await resolve(passedOptions?.headers)).toStrictEqual({ Authorization: 'Bearer mock-auth-token', }); }); it('should use custom headers in addition to auth token when provided', async () => { createVertexNode({ project: 'test-project', headers: async () => ({ 'Custom-Header': 'custom-value', }), }); expect(createVertexOriginal).toHaveBeenCalledTimes(1); const passedOptions = vi.mocked(createVertexOriginal).mock.calls[0][0]; expect(await resolve(passedOptions?.headers)).toEqual({ Authorization: 'Bearer mock-auth-token', 'Custom-Header': 'custom-value', }); }); it('passes googleAuthOptions to generateAuthToken', async () => { createVertexNode({ googleAuthOptions: { scopes: ['https://www.googleapis.com/auth/cloud-platform'], keyFile: 'path/to/key.json', }, }); expect(createVertexOriginal).toHaveBeenCalledTimes(1); const passedOptions = vi.mocked(createVertexOriginal).mock.calls[0][0]; await resolve(passedOptions?.headers); // call the headers function expect(generateAuthToken).toHaveBeenCalledWith({ scopes: ['https://www.googleapis.com/auth/cloud-platform'], keyFile: 'path/to/key.json', }); }); }); --- File: /ai/packages/google-vertex/src/google-vertex-provider-node.ts --- import { resolve } from '@ai-sdk/provider-utils'; import { GoogleAuthOptions } from 'google-auth-library'; import { generateAuthToken } from './google-vertex-auth-google-auth-library'; import { createVertex as createVertexOriginal, GoogleVertexProvider, GoogleVertexProviderSettings as GoogleVertexProviderSettingsOriginal, } from './google-vertex-provider'; export interface GoogleVertexProviderSettings extends GoogleVertexProviderSettingsOriginal { /** Optional. The Authentication options provided by google-auth-library. Complete list of authentication options is documented in the GoogleAuthOptions interface: https://github.com/googleapis/google-auth-library-nodejs/blob/main/src/auth/googleauth.ts. */ googleAuthOptions?: GoogleAuthOptions; } export type { GoogleVertexProvider }; export function createVertex( options: GoogleVertexProviderSettings = {}, ): GoogleVertexProvider { return createVertexOriginal({ ...options, headers: async () => ({ Authorization: `Bearer ${await generateAuthToken( options.googleAuthOptions, )}`, ...(await resolve(options.headers)), }), }); } /** Default Google Vertex AI provider instance. */ export const vertex = createVertex(); --- File: /ai/packages/google-vertex/src/google-vertex-provider.test.ts --- import { describe, it, expect, beforeEach, vi } from 'vitest'; import { createVertex } from './google-vertex-provider'; import { GoogleGenerativeAILanguageModel } from '@ai-sdk/google/internal'; import { GoogleVertexEmbeddingModel } from './google-vertex-embedding-model'; import { GoogleVertexImageModel } from './google-vertex-image-model'; // Mock the imported modules vi.mock('@ai-sdk/provider-utils', () => ({ loadSetting: vi.fn().mockImplementation(({ settingValue }) => settingValue), generateId: vi.fn().mockReturnValue('mock-id'), withoutTrailingSlash: vi.fn().mockImplementation(url => url), })); vi.mock('@ai-sdk/google/internal', () => ({ GoogleGenerativeAILanguageModel: vi.fn(), })); vi.mock('./google-vertex-embedding-model', () => ({ GoogleVertexEmbeddingModel: vi.fn(), })); vi.mock('./google-vertex-image-model', () => ({ GoogleVertexImageModel: vi.fn(), })); describe('google-vertex-provider', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should create a language model with default settings', () => { const provider = createVertex({ project: 'test-project', location: 'test-location', }); provider('test-model-id'); expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith( 'test-model-id', expect.objectContaining({ provider: 'google.vertex.chat', baseURL: 'https://test-location-aiplatform.googleapis.com/v1/projects/test-project/locations/test-location/publishers/google', headers: expect.any(Object), generateId: expect.any(Function), }), ); }); it('should throw an error when using new keyword', () => { const provider = createVertex({ project: 'test-project' }); expect(() => new (provider as any)('test-model-id')).toThrow( 'The Google Vertex AI model function cannot be called with the new keyword.', ); }); it('should create an embedding model with correct settings', () => { const provider = createVertex({ project: 'test-project', location: 'test-location', }); provider.textEmbeddingModel('test-embedding-model'); expect(GoogleVertexEmbeddingModel).toHaveBeenCalledWith( 'test-embedding-model', expect.objectContaining({ provider: 'google.vertex.embedding', headers: expect.any(Object), baseURL: 'https://test-location-aiplatform.googleapis.com/v1/projects/test-project/locations/test-location/publishers/google', }), ); }); it('should pass custom headers to the model constructor', () => { const customHeaders = { 'Custom-Header': 'custom-value' }; const provider = createVertex({ project: 'test-project', location: 'test-location', headers: customHeaders, }); provider('test-model-id'); expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ headers: customHeaders, }), ); }); it('should pass custom generateId function to the model constructor', () => { const customGenerateId = () => 'custom-id'; const provider = createVertex({ project: 'test-project', location: 'test-location', generateId: customGenerateId, }); provider('test-model-id'); expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ generateId: customGenerateId, }), ); }); it('should use languageModel method to create a model', () => { const provider = createVertex({ project: 'test-project', location: 'test-location', }); provider.languageModel('test-model-id'); expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith( 'test-model-id', expect.any(Object), ); }); it('should use custom baseURL when provided', () => { const customBaseURL = 'https://custom-endpoint.example.com'; const provider = createVertex({ project: 'test-project', location: 'test-location', baseURL: customBaseURL, }); provider('test-model-id'); expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith( 'test-model-id', expect.objectContaining({ baseURL: customBaseURL, }), ); }); it('should create an image model with default settings', () => { const provider = createVertex({ project: 'test-project', location: 'test-location', }); provider.image('imagen-3.0-generate-002'); expect(GoogleVertexImageModel).toHaveBeenCalledWith( 'imagen-3.0-generate-002', expect.objectContaining({ provider: 'google.vertex.image', baseURL: 'https://test-location-aiplatform.googleapis.com/v1/projects/test-project/locations/test-location/publishers/google', headers: expect.any(Object), }), ); }); it('should use correct URL for global region', () => { const provider = createVertex({ project: 'test-project', location: 'global', }); provider('test-model-id'); expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith( 'test-model-id', expect.objectContaining({ provider: 'google.vertex.chat', baseURL: 'https://aiplatform.googleapis.com/v1/projects/test-project/locations/global/publishers/google', headers: expect.any(Object), generateId: expect.any(Function), }), ); }); it('should use correct URL for global region with embedding model', () => { const provider = createVertex({ project: 'test-project', location: 'global', }); provider.textEmbeddingModel('test-embedding-model'); expect(GoogleVertexEmbeddingModel).toHaveBeenCalledWith( 'test-embedding-model', expect.objectContaining({ provider: 'google.vertex.embedding', headers: expect.any(Object), baseURL: 'https://aiplatform.googleapis.com/v1/projects/test-project/locations/global/publishers/google', }), ); }); it('should use correct URL for global region with image model', () => { const provider = createVertex({ project: 'test-project', location: 'global', }); provider.image('imagen-3.0-generate-002'); expect(GoogleVertexImageModel).toHaveBeenCalledWith( 'imagen-3.0-generate-002', expect.objectContaining({ provider: 'google.vertex.image', baseURL: 'https://aiplatform.googleapis.com/v1/projects/test-project/locations/global/publishers/google', headers: expect.any(Object), }), ); }); it('should use region-prefixed URL for non-global regions', () => { const provider = createVertex({ project: 'test-project', location: 'us-central1', }); provider('test-model-id'); expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith( 'test-model-id', expect.objectContaining({ provider: 'google.vertex.chat', baseURL: 'https://us-central1-aiplatform.googleapis.com/v1/projects/test-project/locations/us-central1/publishers/google', headers: expect.any(Object), generateId: expect.any(Function), }), ); }); }); --- File: /ai/packages/google-vertex/src/google-vertex-provider.ts --- import { GoogleGenerativeAILanguageModel } from '@ai-sdk/google/internal'; import { ImageModelV2, LanguageModelV2, ProviderV2 } from '@ai-sdk/provider'; import { FetchFunction, generateId, loadSetting, Resolvable, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { GoogleVertexConfig } from './google-vertex-config'; import { GoogleVertexEmbeddingModel } from './google-vertex-embedding-model'; import { GoogleVertexEmbeddingModelId } from './google-vertex-embedding-options'; import { GoogleVertexImageModel } from './google-vertex-image-model'; import { GoogleVertexImageModelId } from './google-vertex-image-settings'; import { GoogleVertexModelId } from './google-vertex-options'; export interface GoogleVertexProvider extends ProviderV2 { /** Creates a model for text generation. */ (modelId: GoogleVertexModelId): LanguageModelV2; languageModel: (modelId: GoogleVertexModelId) => LanguageModelV2; /** * Creates a model for image generation. */ image(modelId: GoogleVertexImageModelId): ImageModelV2; /** Creates a model for image generation. */ imageModel(modelId: GoogleVertexImageModelId): ImageModelV2; } export interface GoogleVertexProviderSettings { /** Your Google Vertex location. Defaults to the environment variable `GOOGLE_VERTEX_LOCATION`. */ location?: string; /** Your Google Vertex project. Defaults to the environment variable `GOOGLE_VERTEX_PROJECT`. */ project?: string; /** * Headers to use for requests. Can be: * - A headers object * - A Promise that resolves to a headers object * - A function that returns a headers object * - A function that returns a Promise of a headers object */ headers?: Resolvable<Record<string, string | undefined>>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; // for testing generateId?: () => string; /** Base URL for the Google Vertex API calls. */ baseURL?: string; } /** Create a Google Vertex AI provider instance. */ export function createVertex( options: GoogleVertexProviderSettings = {}, ): GoogleVertexProvider { const loadVertexProject = () => loadSetting({ settingValue: options.project, settingName: 'project', environmentVariableName: 'GOOGLE_VERTEX_PROJECT', description: 'Google Vertex project', }); const loadVertexLocation = () => loadSetting({ settingValue: options.location, settingName: 'location', environmentVariableName: 'GOOGLE_VERTEX_LOCATION', description: 'Google Vertex location', }); const loadBaseURL = () => { const region = loadVertexLocation(); const project = loadVertexProject(); // For global region, use aiplatform.googleapis.com directly // For other regions, use region-aiplatform.googleapis.com const baseHost = `${region === 'global' ? '' : region + '-'}aiplatform.googleapis.com`; return ( withoutTrailingSlash(options.baseURL) ?? `https://${baseHost}/v1/projects/${project}/locations/${region}/publishers/google` ); }; const createConfig = (name: string): GoogleVertexConfig => { return { provider: `google.vertex.${name}`, headers: options.headers ?? {}, fetch: options.fetch, baseURL: loadBaseURL(), }; }; const createChatModel = (modelId: GoogleVertexModelId) => { return new GoogleGenerativeAILanguageModel(modelId, { ...createConfig('chat'), generateId: options.generateId ?? generateId, supportedUrls: () => ({ '*': [ // HTTP URLs: /^https?:\/\/.*$/, // Google Cloud Storage URLs: /^gs:\/\/.*$/, ], }), }); }; const createEmbeddingModel = (modelId: GoogleVertexEmbeddingModelId) => new GoogleVertexEmbeddingModel(modelId, createConfig('embedding')); const createImageModel = (modelId: GoogleVertexImageModelId) => new GoogleVertexImageModel(modelId, createConfig('image')); const provider = function (modelId: GoogleVertexModelId) { if (new.target) { throw new Error( 'The Google Vertex AI model function cannot be called with the new keyword.', ); } return createChatModel(modelId); }; provider.languageModel = createChatModel; provider.textEmbeddingModel = createEmbeddingModel; provider.image = createImageModel; provider.imageModel = createImageModel; return provider; } --- File: /ai/packages/google-vertex/src/index.ts --- export type { GoogleVertexImageProviderOptions } from './google-vertex-image-model'; export { createVertex, vertex } from './google-vertex-provider-node'; export type { GoogleVertexProvider, GoogleVertexProviderSettings, } from './google-vertex-provider-node'; --- File: /ai/packages/google-vertex/edge.d.ts --- export * from './dist/edge'; --- File: /ai/packages/google-vertex/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, outDir: 'dist', }, { entry: ['src/edge/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, outDir: 'dist/edge', }, { entry: ['src/anthropic/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, outDir: 'dist/anthropic', }, { entry: ['src/anthropic/edge/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, outDir: 'dist/anthropic/edge', }, ]); --- File: /ai/packages/google-vertex/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/google-vertex/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/groq/src/convert-to-groq-chat-messages.test.ts --- import { convertToGroqChatMessages } from './convert-to-groq-chat-messages'; describe('user messages', () => { it('should convert messages with image parts', async () => { const result = convertToGroqChatMessages([ { role: 'user', content: [ { type: 'text', text: 'Hello' }, { type: 'file', data: 'AAECAw==', mediaType: 'image/png', }, ], }, ]); expect(result).toEqual([ { role: 'user', content: [ { type: 'text', text: 'Hello' }, { type: 'image_url', image_url: { url: 'data:image/png;base64,AAECAw==' }, }, ], }, ]); }); it('should convert messages with only a text part to a string content', async () => { const result = convertToGroqChatMessages([ { role: 'user', content: [{ type: 'text', text: 'Hello' }], }, ]); expect(result).toEqual([{ role: 'user', content: 'Hello' }]); }); }); describe('tool calls', () => { it('should stringify arguments to tool calls', () => { const result = convertToGroqChatMessages([ { role: 'assistant', content: [ { type: 'tool-call', input: { foo: 'bar123' }, toolCallId: 'quux', toolName: 'thwomp', }, ], }, { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'quux', toolName: 'thwomp', output: { type: 'json', value: { oof: '321rab' } }, }, ], }, ]); expect(result).toEqual([ { role: 'assistant', content: '', tool_calls: [ { type: 'function', id: 'quux', function: { name: 'thwomp', arguments: JSON.stringify({ foo: 'bar123' }), }, }, ], }, { role: 'tool', content: JSON.stringify({ oof: '321rab' }), tool_call_id: 'quux', }, ]); }); }); --- File: /ai/packages/groq/src/convert-to-groq-chat-messages.ts --- import { LanguageModelV2Prompt, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { GroqChatPrompt } from './groq-api-types'; export function convertToGroqChatMessages( prompt: LanguageModelV2Prompt, ): GroqChatPrompt { const messages: GroqChatPrompt = []; for (const { role, content } of prompt) { switch (role) { case 'system': { messages.push({ role: 'system', content }); break; } case 'user': { if (content.length === 1 && content[0].type === 'text') { messages.push({ role: 'user', content: content[0].text }); break; } messages.push({ role: 'user', content: content.map(part => { switch (part.type) { case 'text': { return { type: 'text', text: part.text }; } case 'file': { if (!part.mediaType.startsWith('image/')) { throw new UnsupportedFunctionalityError({ functionality: 'Non-image file content parts', }); } const mediaType = part.mediaType === 'image/*' ? 'image/jpeg' : part.mediaType; return { type: 'image_url', image_url: { url: part.data instanceof URL ? part.data.toString() : `data:${mediaType};base64,${part.data}`, }, }; } } }), }); break; } case 'assistant': { let text = ''; const toolCalls: Array<{ id: string; type: 'function'; function: { name: string; arguments: string }; }> = []; for (const part of content) { switch (part.type) { case 'text': { text += part.text; break; } case 'tool-call': { toolCalls.push({ id: part.toolCallId, type: 'function', function: { name: part.toolName, arguments: JSON.stringify(part.input), }, }); break; } } } messages.push({ role: 'assistant', content: text, tool_calls: toolCalls.length > 0 ? toolCalls : undefined, }); break; } case 'tool': { for (const toolResponse of content) { const output = toolResponse.output; let contentValue: string; switch (output.type) { case 'text': case 'error-text': contentValue = output.value; break; case 'content': case 'json': case 'error-json': contentValue = JSON.stringify(output.value); break; } messages.push({ role: 'tool', tool_call_id: toolResponse.toolCallId, content: contentValue, }); } break; } default: { const _exhaustiveCheck: never = role; throw new Error(`Unsupported role: ${_exhaustiveCheck}`); } } } return messages; } --- File: /ai/packages/groq/src/get-response-metadata.ts --- export function getResponseMetadata({ id, model, created, }: { id?: string | undefined | null; created?: number | undefined | null; model?: string | undefined | null; }) { return { id: id ?? undefined, modelId: model ?? undefined, timestamp: created != null ? new Date(created * 1000) : undefined, }; } --- File: /ai/packages/groq/src/groq-api-types.ts --- export type GroqChatPrompt = Array<GroqMessage>; export type GroqMessage = | GroqSystemMessage | GroqUserMessage | GroqAssistantMessage | GroqToolMessage; export interface GroqSystemMessage { role: 'system'; content: string; } export interface GroqUserMessage { role: 'user'; content: string | Array<GroqContentPart>; } export type GroqContentPart = GroqContentPartText | GroqContentPartImage; export interface GroqContentPartImage { type: 'image_url'; image_url: { url: string }; } export interface GroqContentPartText { type: 'text'; text: string; } export interface GroqAssistantMessage { role: 'assistant'; content?: string | null; tool_calls?: Array<GroqMessageToolCall>; } export interface GroqMessageToolCall { type: 'function'; id: string; function: { arguments: string; name: string; }; } export interface GroqToolMessage { role: 'tool'; content: string; tool_call_id: string; } export interface GroqTranscriptionAPITypes { /** * The audio file object for direct upload to translate/transcribe. * Required unless using url instead. */ file?: string; /** * The audio URL to translate/transcribe (supports Base64URL). * Required unless using file instead. */ url?: string; /** * The language of the input audio. Supplying the input language in ISO-639-1 (i.e. en, tr`) format will improve accuracy and latency. * The translations endpoint only supports 'en' as a parameter option. */ language?: string; /** * ID of the model to use. */ model: string; /** * Prompt to guide the model's style or specify how to spell unfamiliar words. (limited to 224 tokens) */ prompt?: string; /** * Define the output response format. * Set to verbose_json to receive timestamps for audio segments. * Set to text to return a text response. */ response_format?: string; /** * The temperature between 0 and 1. For translations and transcriptions, we recommend the default value of 0. */ temperature?: number; /** * The timestamp granularities to populate for this transcription. response_format must be set verbose_json to use timestamp granularities. * Either or both of word and segment are supported. * segment returns full metadata and word returns only word, start, and end timestamps. To get both word-level timestamps and full segment metadata, include both values in the array. */ timestamp_granularities?: Array<string>; } --- File: /ai/packages/groq/src/groq-chat-language-model.test.ts --- import { LanguageModelV2Prompt } from '@ai-sdk/provider'; import { convertReadableStreamToArray, createTestServer, isNodeVersion, } from '@ai-sdk/provider-utils/test'; import { createGroq } from './groq-provider'; const TEST_PROMPT: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; const provider = createGroq({ apiKey: 'test-api-key' }); const model = provider('gemma2-9b-it'); const server = createTestServer({ 'https://api.groq.com/openai/v1/chat/completions': {}, }); describe('doGenerate', () => { function prepareJsonResponse({ content = '', reasoning, tool_calls, function_call, usage = { prompt_tokens: 4, total_tokens: 34, completion_tokens: 30, }, finish_reason = 'stop', id = 'chatcmpl-95ZTZkhr0mHNKqerQfiwkuox3PHAd', created = 1711115037, model = 'gemma2-9b-it', headers, }: { content?: string; reasoning?: string; tool_calls?: Array<{ id: string; type: 'function'; function: { name: string; arguments: string; }; }>; function_call?: { name: string; arguments: string; }; usage?: { prompt_tokens?: number; total_tokens?: number; completion_tokens?: number; }; finish_reason?: string; created?: number; id?: string; model?: string; headers?: Record<string, string>; } = {}) { server.urls['https://api.groq.com/openai/v1/chat/completions'].response = { type: 'json-value', headers, body: { id, object: 'chat.completion', created, model, choices: [ { index: 0, message: { role: 'assistant', content, reasoning, tool_calls, function_call, }, finish_reason, }, ], usage, system_fingerprint: 'fp_3bc1b5746c', }, }; } it('should extract text', async () => { prepareJsonResponse({ content: 'Hello, World!' }); const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "text": "Hello, World!", "type": "text", }, ] `); }); it('should extract reasoning', async () => { prepareJsonResponse({ reasoning: 'This is a test reasoning', }); const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "text": "This is a test reasoning", "type": "reasoning", }, ] `); }); it('should extract usage', async () => { prepareJsonResponse({ usage: { prompt_tokens: 20, total_tokens: 25, completion_tokens: 5 }, }); const { usage } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(usage).toMatchInlineSnapshot(` { "inputTokens": 20, "outputTokens": 5, "totalTokens": 25, } `); }); it('should send additional response information', async () => { prepareJsonResponse({ id: 'test-id', created: 123, model: 'test-model', }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect({ id: response?.id, timestamp: response?.timestamp, modelId: response?.modelId, }).toStrictEqual({ id: 'test-id', timestamp: new Date(123 * 1000), modelId: 'test-model', }); }); it('should support partial usage', async () => { prepareJsonResponse({ usage: { prompt_tokens: 20, total_tokens: 20 }, }); const { usage } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(usage).toMatchInlineSnapshot(` { "inputTokens": 20, "outputTokens": undefined, "totalTokens": 20, } `); }); it('should extract finish reason', async () => { prepareJsonResponse({ finish_reason: 'stop', }); const response = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response.finishReason).toStrictEqual('stop'); }); it('should support unknown finish reason', async () => { prepareJsonResponse({ finish_reason: 'eos', }); const response = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response.finishReason).toStrictEqual('unknown'); }); it('should expose the raw response headers', async () => { prepareJsonResponse({ headers: { 'test-header': 'test-value', }, }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-length': '315', 'content-type': 'application/json', // custom header 'test-header': 'test-value', }); }); it('should pass the model and the messages', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gemma2-9b-it', messages: [{ role: 'user', content: 'Hello' }], }); }); it('should pass provider options', async () => { prepareJsonResponse(); await provider('gemma2-9b-it').doGenerate({ prompt: TEST_PROMPT, providerOptions: { groq: { reasoningFormat: 'hidden', user: 'test-user-id', parallelToolCalls: false, }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gemma2-9b-it', messages: [{ role: 'user', content: 'Hello' }], parallel_tool_calls: false, user: 'test-user-id', reasoning_format: 'hidden', }); }); it('should pass tools and toolChoice', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], toolChoice: { type: 'tool', toolName: 'test-tool', }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gemma2-9b-it', messages: [{ role: 'user', content: 'Hello' }], tools: [ { type: 'function', function: { name: 'test-tool', parameters: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }, ], tool_choice: { type: 'function', function: { name: 'test-tool' }, }, }); }); it('should pass headers', async () => { prepareJsonResponse({ content: '' }); const provider = createGroq({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider('gemma2-9b-it').doGenerate({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should parse tool results', async () => { prepareJsonResponse({ tool_calls: [ { id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', type: 'function', function: { name: 'test-tool', arguments: '{"value":"Spark"}', }, }, ], }); const result = await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], toolChoice: { type: 'tool', toolName: 'test-tool', }, prompt: TEST_PROMPT, }); expect(result.content).toMatchInlineSnapshot(` [ { "input": "{"value":"Spark"}", "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, ] `); }); it('should pass response format information as json_schema when structuredOutputs enabled by default', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = provider('gemma2-9b-it'); await model.doGenerate({ responseFormat: { type: 'json', name: 'test-name', description: 'test description', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gemma2-9b-it', messages: [{ role: 'user', content: 'Hello' }], response_format: { type: 'json_schema', json_schema: { name: 'test-name', description: 'test description', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }, }); }); it('should pass response format information as json_object when structuredOutputs explicitly disabled', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = provider('gemma2-9b-it'); const { warnings } = await model.doGenerate({ providerOptions: { groq: { structuredOutputs: false, }, }, responseFormat: { type: 'json', name: 'test-name', description: 'test description', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gemma2-9b-it', messages: [{ role: 'user', content: 'Hello' }], response_format: { type: 'json_object', }, }); expect(warnings).toEqual([ { details: 'JSON response format schema is only supported with structuredOutputs', setting: 'responseFormat', type: 'unsupported-setting', }, ]); }); it('should use json_schema format when structuredOutputs explicitly enabled', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = provider('gemma2-9b-it'); await model.doGenerate({ providerOptions: { groq: { structuredOutputs: true, }, }, responseFormat: { type: 'json', name: 'test-name', description: 'test description', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gemma2-9b-it', messages: [{ role: 'user', content: 'Hello' }], response_format: { type: 'json_schema', json_schema: { name: 'test-name', description: 'test description', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }, }); }); it('should allow explicit structuredOutputs override', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = provider('gemma2-9b-it'); await model.doGenerate({ providerOptions: { groq: { structuredOutputs: true, }, }, responseFormat: { type: 'json', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gemma2-9b-it', messages: [{ role: 'user', content: 'Hello' }], response_format: { type: 'json_schema', json_schema: { name: 'response', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }, }); }); it('should handle structured outputs with Kimi K2 model', async () => { prepareJsonResponse({ content: '{"recipe":{"name":"Spaghetti Aglio e Olio","ingredients":["spaghetti","garlic","olive oil","parmesan"],"instructions":["Boil pasta","Sauté garlic","Combine"]}}', }); const kimiModel = provider('moonshotai/kimi-k2-instruct'); const result = await kimiModel.doGenerate({ providerOptions: { groq: { structuredOutputs: true, }, }, responseFormat: { type: 'json', name: 'recipe_response', description: 'A recipe with ingredients and instructions', schema: { type: 'object', properties: { recipe: { type: 'object', properties: { name: { type: 'string' }, ingredients: { type: 'array', items: { type: 'string' } }, instructions: { type: 'array', items: { type: 'string' } }, }, required: ['name', 'ingredients', 'instructions'], }, }, required: ['recipe'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, prompt: [ { role: 'user', content: [{ type: 'text', text: 'Generate a simple pasta recipe' }], }, ], }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "messages": [ { "content": "Generate a simple pasta recipe", "role": "user", }, ], "model": "moonshotai/kimi-k2-instruct", "response_format": { "json_schema": { "description": "A recipe with ingredients and instructions", "name": "recipe_response", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "recipe": { "properties": { "ingredients": { "items": { "type": "string", }, "type": "array", }, "instructions": { "items": { "type": "string", }, "type": "array", }, "name": { "type": "string", }, }, "required": [ "name", "ingredients", "instructions", ], "type": "object", }, }, "required": [ "recipe", ], "type": "object", }, }, "type": "json_schema", }, } `); expect(result.content).toMatchInlineSnapshot(` [ { "text": "{"recipe":{"name":"Spaghetti Aglio e Olio","ingredients":["spaghetti","garlic","olive oil","parmesan"],"instructions":["Boil pasta","Sauté garlic","Combine"]}}", "type": "text", }, ] `); }); it('should include warnings when structured outputs explicitly disabled but schema provided', async () => { prepareJsonResponse({ content: '{"value":"test"}' }); const { warnings } = await model.doGenerate({ providerOptions: { groq: { structuredOutputs: false, }, }, responseFormat: { type: 'json', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, prompt: TEST_PROMPT, }); expect(warnings).toMatchInlineSnapshot(` [ { "details": "JSON response format schema is only supported with structuredOutputs", "setting": "responseFormat", "type": "unsupported-setting", }, ] `); }); it('should send request body', async () => { prepareJsonResponse({ content: '' }); const { request } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(request).toStrictEqual({ body: '{"model":"gemma2-9b-it","messages":[{"role":"user","content":"Hello"}]}', }); }); }); describe('doStream', () => { function prepareStreamResponse({ content = [], finish_reason = 'stop', headers, }: { content?: string[]; finish_reason?: string; headers?: Record<string, string>; }) { server.urls['https://api.groq.com/openai/v1/chat/completions'].response = { type: 'stream-chunks', headers, chunks: [ `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1702657020,"model":"gemma2-9b-it",` + `"system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}\n\n`, ...content.map(text => { return ( `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1702657020,"model":"gemma2-9b-it",` + `"system_fingerprint":null,"choices":[{"index":1,"delta":{"content":"${text}"},"finish_reason":null}]}\n\n` ); }), `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1702657020,"model":"gemma2-9b-it",` + `"system_fingerprint":null,"choices":[{"index":0,"delta":{},"finish_reason":"${finish_reason}"}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"${finish_reason}"}],` + `"x_groq":{"id":"req_01jadadp0femyae9kav1gpkhe8","usage":{"queue_time":0.061348671,"prompt_tokens":18,"prompt_time":0.000211569,` + `"completion_tokens":439,"completion_time":0.798181818,"total_tokens":457,"total_time":0.798393387}}}\n\n`, 'data: [DONE]\n\n', ], }; } it('should stream text deltas', async () => { prepareStreamResponse({ content: ['Hello', ', ', 'World!'], finish_reason: 'stop', }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); // note: space moved to last chunk bc of trimming expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", "modelId": "gemma2-9b-it", "timestamp": 2023-12-15T16:17:00.000Z, "type": "response-metadata", }, { "id": "txt-0", "type": "text-start", }, { "delta": "Hello", "id": "txt-0", "type": "text-delta", }, { "delta": ", ", "id": "txt-0", "type": "text-delta", }, { "delta": "World!", "id": "txt-0", "type": "text-delta", }, { "id": "txt-0", "type": "text-end", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 18, "outputTokens": 439, "totalTokens": 457, }, }, ] `); }); it('should stream reasoning deltas', async () => { server.urls['https://api.groq.com/openai/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1702657020,"model":"gemma2-9b-it",` + `"system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1702657020,"model":"gemma2-9b-it",` + `"system_fingerprint":null,"choices":[{"index":1,"delta":{"reasoning":"I think,"},"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1702657020,"model":"gemma2-9b-it",` + `"system_fingerprint":null,"choices":[{"index":1,"delta":{"reasoning":"therefore I am."},"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1702657020,"model":"gemma2-9b-it",` + `"system_fingerprint":null,"choices":[{"index":1,"delta":{"content":"Hello"},"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1702657020,"model":"gemma2-9b-it",` + `"system_fingerprint":null,"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],` + `"x_groq":{"id":"req_01jadadp0femyae9kav1gpkhe8","usage":{"queue_time":0.061348671,"prompt_tokens":18,"prompt_time":0.000211569,` + `"completion_tokens":439,"completion_time":0.798181818,"total_tokens":457,"total_time":0.798393387}}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", "modelId": "gemma2-9b-it", "timestamp": 2023-12-15T16:17:00.000Z, "type": "response-metadata", }, { "id": "reasoning-0", "type": "reasoning-start", }, { "delta": "I think,", "id": "reasoning-0", "type": "reasoning-delta", }, { "delta": "therefore I am.", "id": "reasoning-0", "type": "reasoning-delta", }, { "id": "txt-0", "type": "text-start", }, { "delta": "Hello", "id": "txt-0", "type": "text-delta", }, { "id": "reasoning-0", "type": "reasoning-end", }, { "id": "txt-0", "type": "text-end", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 18, "outputTokens": 439, "totalTokens": 457, }, }, ] `); }); it('should stream tool deltas', async () => { server.urls['https://api.groq.com/openai/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,` + `"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":""}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\""}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"value"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\":\\""}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Spark"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"le"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" Day"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"}"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],` + `"x_groq":{"id":"req_01jadadp0femyae9kav1gpkhe8","usage":{"queue_time":0.061348671,"prompt_tokens":18,"prompt_time":0.000211569,` + `"completion_tokens":439,"completion_time":0.798181818,"total_tokens":457,"total_time":0.798393387}}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", "modelId": "gemma2-9b-it", "timestamp": 2024-03-25T09:06:38.000Z, "type": "response-metadata", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-input-start", }, { "delta": "{"", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "value", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "":"", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "Spark", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "le", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": " Day", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": ""}", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-end", }, { "input": "{"value":"Sparkle Day"}", "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "tool-calls", "type": "finish", "usage": { "inputTokens": 18, "outputTokens": 439, "totalTokens": 457, }, }, ] `); }); it('should stream tool call deltas when tool call arguments are passed in the first chunk', async () => { server.urls['https://api.groq.com/openai/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,` + `"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":"{\\""}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"va"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"lue"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\":\\""}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Spark"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"le"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" Day"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"}"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],` + `"x_groq":{"id":"req_01jadadp0femyae9kav1gpkhe8","usage":{"queue_time":0.061348671,"prompt_tokens":18,"prompt_time":0.000211569,` + `"completion_tokens":439,"completion_time":0.798181818,"total_tokens":457,"total_time":0.798393387}}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", "modelId": "gemma2-9b-it", "timestamp": 2024-03-25T09:06:38.000Z, "type": "response-metadata", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-input-start", }, { "delta": "{"", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "va", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "lue", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "":"", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "Spark", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "le", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": " Day", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": ""}", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-end", }, { "input": "{"value":"Sparkle Day"}", "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "tool-calls", "type": "finish", "usage": { "inputTokens": 18, "outputTokens": 439, "totalTokens": 457, }, }, ] `); }); it('should not duplicate tool calls when there is an additional empty chunk after the tool call has been completed', async () => { server.urls['https://api.groq.com/openai/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":226,"completion_tokens":0}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"id":"chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa",` + `"type":"function","index":0,"function":{"name":"searchGoogle"}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":233,"completion_tokens":7}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":"{\\"query\\": \\""}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":241,"completion_tokens":15}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":"latest"}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":242,"completion_tokens":16}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":" news"}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":243,"completion_tokens":17}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":" on"}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":244,"completion_tokens":18}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":" ai\\"}"}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":245,"completion_tokens":19}}\n\n`, // empty arguments chunk after the tool call has already been finished: `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":""}}]},"logprobs":null,"finish_reason":"tool_calls","stop_reason":128008}],` + `"usage":{"prompt_tokens":226,"total_tokens":246,"completion_tokens":20}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[],` + `"usage":{"prompt_tokens":226,"total_tokens":246,"completion_tokens":20}}\n\n`, `data: [DONE]\n\n`, ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'searchGoogle', inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chat-2267f7e2910a4254bac0650ba74cfc1c", "modelId": "meta/llama-3.1-8b-instruct:fp8", "timestamp": 2024-12-02T17:57:21.000Z, "type": "response-metadata", }, { "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "toolName": "searchGoogle", "type": "tool-input-start", }, { "delta": "{"query": "", "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-delta", }, { "delta": "latest", "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-delta", }, { "delta": " news", "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-delta", }, { "delta": " on", "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-delta", }, { "delta": " ai"}", "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-delta", }, { "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-end", }, { "input": "{"query": "latest news on ai"}", "toolCallId": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "toolName": "searchGoogle", "type": "tool-call", }, { "finishReason": "tool-calls", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it('should stream tool call that is sent in one chunk', async () => { server.urls['https://api.groq.com/openai/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,` + `"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":"{\\"value\\":\\"Sparkle Day\\"}"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"gemma2-9b-it",` + `"system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],` + `"x_groq":{"id":"req_01jadadp0femyae9kav1gpkhe8","usage":{"queue_time":0.061348671,"prompt_tokens":18,"prompt_time":0.000211569,` + `"completion_tokens":439,"completion_time":0.798181818,"total_tokens":457,"total_time":0.798393387}}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", "modelId": "gemma2-9b-it", "timestamp": 2024-03-25T09:06:38.000Z, "type": "response-metadata", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-input-start", }, { "delta": "{"value":"Sparkle Day"}", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-end", }, { "input": "{"value":"Sparkle Day"}", "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "tool-calls", "type": "finish", "usage": { "inputTokens": 18, "outputTokens": 439, "totalTokens": 457, }, }, ] `); }); it('should handle error stream parts', async () => { server.urls['https://api.groq.com/openai/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"error":{"message": "The server had an error processing your request. Sorry about that!","type":"invalid_request_error"}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "error": { "message": "The server had an error processing your request. Sorry about that!", "type": "invalid_request_error", }, "type": "error", }, { "finishReason": "error", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it.skipIf(isNodeVersion(20))( 'should handle unparsable stream parts', async () => { server.urls['https://api.groq.com/openai/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [`data: {unparsable}\n\n`, 'data: [DONE]\n\n'], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "error": [AI_JSONParseError: JSON parsing failed: Text: {unparsable}. Error message: Expected property name or '}' in JSON at position 1 (line 1 column 2)], "type": "error", }, { "finishReason": "error", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }, ); it('should expose the raw response headers', async () => { prepareStreamResponse({ headers: { 'test-header': 'test-value', }, }); const { response } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', // custom header 'test-header': 'test-value', }); }); it('should pass the messages and the model', async () => { prepareStreamResponse({ content: [] }); await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ stream: true, model: 'gemma2-9b-it', messages: [{ role: 'user', content: 'Hello' }], }); }); it('should pass headers', async () => { prepareStreamResponse({ content: [] }); const provider = createGroq({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider('gemma2-9b-it').doStream({ prompt: TEST_PROMPT, includeRawChunks: false, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should send request body', async () => { prepareStreamResponse({ content: [] }); const { request } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(request).toStrictEqual({ body: '{"model":"gemma2-9b-it","messages":[{"role":"user","content":"Hello"}],"stream":true}', }); }); }); describe('doStream with raw chunks', () => { it('should stream raw chunks when includeRawChunks is true', async () => { server.urls['https://api.groq.com/openai/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gemma2-9b-it","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-456","object":"chat.completion.chunk","created":1234567890,"model":"gemma2-9b-it","choices":[{"index":0,"delta":{"content":" world"},"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-789","object":"chat.completion.chunk","created":1234567890,"model":"gemma2-9b-it","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"x_groq":{"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15}}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: true, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "rawValue": { "choices": [ { "delta": { "content": "Hello", }, "finish_reason": null, "index": 0, }, ], "created": 1234567890, "id": "chatcmpl-123", "model": "gemma2-9b-it", "object": "chat.completion.chunk", }, "type": "raw", }, { "id": "chatcmpl-123", "modelId": "gemma2-9b-it", "timestamp": 2009-02-13T23:31:30.000Z, "type": "response-metadata", }, { "id": "txt-0", "type": "text-start", }, { "delta": "Hello", "id": "txt-0", "type": "text-delta", }, { "rawValue": { "choices": [ { "delta": { "content": " world", }, "finish_reason": null, "index": 0, }, ], "created": 1234567890, "id": "chatcmpl-456", "model": "gemma2-9b-it", "object": "chat.completion.chunk", }, "type": "raw", }, { "delta": " world", "id": "txt-0", "type": "text-delta", }, { "rawValue": { "choices": [ { "delta": {}, "finish_reason": "stop", "index": 0, }, ], "created": 1234567890, "id": "chatcmpl-789", "model": "gemma2-9b-it", "object": "chat.completion.chunk", "x_groq": { "usage": { "completion_tokens": 5, "prompt_tokens": 10, "total_tokens": 15, }, }, }, "type": "raw", }, { "id": "txt-0", "type": "text-end", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 10, "outputTokens": 5, "totalTokens": 15, }, }, ] `); }); }); --- File: /ai/packages/groq/src/groq-chat-language-model.ts --- import { InvalidResponseDataError, LanguageModelV2, LanguageModelV2CallWarning, LanguageModelV2Content, LanguageModelV2FinishReason, LanguageModelV2Prompt, LanguageModelV2StreamPart, LanguageModelV2Usage, SharedV2ProviderMetadata, } from '@ai-sdk/provider'; import { FetchFunction, ParseResult, combineHeaders, createEventSourceResponseHandler, createJsonResponseHandler, generateId, isParsableJson, parseProviderOptions, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { convertToGroqChatMessages } from './convert-to-groq-chat-messages'; import { getResponseMetadata } from './get-response-metadata'; import { GroqChatModelId, groqProviderOptions } from './groq-chat-options'; import { groqErrorDataSchema, groqFailedResponseHandler } from './groq-error'; import { prepareTools } from './groq-prepare-tools'; import { mapGroqFinishReason } from './map-groq-finish-reason'; type GroqChatConfig = { provider: string; headers: () => Record<string, string | undefined>; url: (options: { modelId: string; path: string }) => string; fetch?: FetchFunction; }; export class GroqChatLanguageModel implements LanguageModelV2 { readonly specificationVersion = 'v2'; readonly modelId: GroqChatModelId; readonly supportedUrls = { 'image/*': [/^https?:\/\/.*$/], }; private readonly config: GroqChatConfig; constructor(modelId: GroqChatModelId, config: GroqChatConfig) { this.modelId = modelId; this.config = config; } get provider(): string { return this.config.provider; } private async getArgs({ prompt, maxOutputTokens, temperature, topP, topK, frequencyPenalty, presencePenalty, stopSequences, responseFormat, seed, stream, tools, toolChoice, providerOptions, }: Parameters<LanguageModelV2['doGenerate']>[0] & { stream: boolean; }) { const warnings: LanguageModelV2CallWarning[] = []; const groqOptions = await parseProviderOptions({ provider: 'groq', providerOptions, schema: groqProviderOptions, }); const structuredOutputs = groqOptions?.structuredOutputs ?? true; if (topK != null) { warnings.push({ type: 'unsupported-setting', setting: 'topK', }); } if ( responseFormat?.type === 'json' && responseFormat.schema != null && !structuredOutputs ) { warnings.push({ type: 'unsupported-setting', setting: 'responseFormat', details: 'JSON response format schema is only supported with structuredOutputs', }); } const { tools: groqTools, toolChoice: groqToolChoice, toolWarnings, } = prepareTools({ tools, toolChoice }); return { args: { // model id: model: this.modelId, // model specific settings: user: groqOptions?.user, parallel_tool_calls: groqOptions?.parallelToolCalls, // standardized settings: max_tokens: maxOutputTokens, temperature, top_p: topP, frequency_penalty: frequencyPenalty, presence_penalty: presencePenalty, stop: stopSequences, seed, // response format: response_format: responseFormat?.type === 'json' ? structuredOutputs && responseFormat.schema != null ? { type: 'json_schema', json_schema: { schema: responseFormat.schema, name: responseFormat.name ?? 'response', description: responseFormat.description, }, } : { type: 'json_object' } : undefined, // provider options: reasoning_format: groqOptions?.reasoningFormat, reasoning_effort: groqOptions?.reasoningEffort, // messages: messages: convertToGroqChatMessages(prompt), // tools: tools: groqTools, tool_choice: groqToolChoice, }, warnings: [...warnings, ...toolWarnings], }; } async doGenerate( options: Parameters<LanguageModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>> { const { args, warnings } = await this.getArgs({ ...options, stream: false, }); const body = JSON.stringify(args); const { responseHeaders, value: response, rawValue: rawResponse, } = await postJsonToApi({ url: this.config.url({ path: '/chat/completions', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body: args, failedResponseHandler: groqFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( groqChatResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const choice = response.choices[0]; const content: Array<LanguageModelV2Content> = []; // text content: const text = choice.message.content; if (text != null && text.length > 0) { content.push({ type: 'text', text: text }); } // reasoning: const reasoning = choice.message.reasoning; if (reasoning != null && reasoning.length > 0) { content.push({ type: 'reasoning', text: reasoning, }); } // tool calls: if (choice.message.tool_calls != null) { for (const toolCall of choice.message.tool_calls) { content.push({ type: 'tool-call', toolCallId: toolCall.id ?? generateId(), toolName: toolCall.function.name, input: toolCall.function.arguments!, }); } } return { content, finishReason: mapGroqFinishReason(choice.finish_reason), usage: { inputTokens: response.usage?.prompt_tokens ?? undefined, outputTokens: response.usage?.completion_tokens ?? undefined, totalTokens: response.usage?.total_tokens ?? undefined, }, response: { ...getResponseMetadata(response), headers: responseHeaders, body: rawResponse, }, warnings, request: { body }, }; } async doStream( options: Parameters<LanguageModelV2['doStream']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>> { const { args, warnings } = await this.getArgs({ ...options, stream: true }); const body = JSON.stringify({ ...args, stream: true }); const { responseHeaders, value: response } = await postJsonToApi({ url: this.config.url({ path: '/chat/completions', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body: { ...args, stream: true, }, failedResponseHandler: groqFailedResponseHandler, successfulResponseHandler: createEventSourceResponseHandler(groqChatChunkSchema), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const toolCalls: Array<{ id: string; type: 'function'; function: { name: string; arguments: string; }; hasFinished: boolean; }> = []; let finishReason: LanguageModelV2FinishReason = 'unknown'; const usage: LanguageModelV2Usage = { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, }; let isFirstChunk = true; let isActiveText = false; let isActiveReasoning = false; let providerMetadata: SharedV2ProviderMetadata | undefined; return { stream: response.pipeThrough( new TransformStream< ParseResult<z.infer<typeof groqChatChunkSchema>>, LanguageModelV2StreamPart >({ start(controller) { controller.enqueue({ type: 'stream-start', warnings }); }, transform(chunk, controller) { // Emit raw chunk if requested (before anything else) if (options.includeRawChunks) { controller.enqueue({ type: 'raw', rawValue: chunk.rawValue }); } // handle failed chunk parsing / validation: if (!chunk.success) { finishReason = 'error'; controller.enqueue({ type: 'error', error: chunk.error }); return; } const value = chunk.value; // handle error chunks: if ('error' in value) { finishReason = 'error'; controller.enqueue({ type: 'error', error: value.error }); return; } if (isFirstChunk) { isFirstChunk = false; controller.enqueue({ type: 'response-metadata', ...getResponseMetadata(value), }); } if (value.x_groq?.usage != null) { usage.inputTokens = value.x_groq.usage.prompt_tokens ?? undefined; usage.outputTokens = value.x_groq.usage.completion_tokens ?? undefined; usage.totalTokens = value.x_groq.usage.total_tokens ?? undefined; } const choice = value.choices[0]; if (choice?.finish_reason != null) { finishReason = mapGroqFinishReason(choice.finish_reason); } if (choice?.delta == null) { return; } const delta = choice.delta; if (delta.reasoning != null && delta.reasoning.length > 0) { if (!isActiveReasoning) { controller.enqueue({ type: 'reasoning-start', id: 'reasoning-0', }); isActiveReasoning = true; } controller.enqueue({ type: 'reasoning-delta', id: 'reasoning-0', delta: delta.reasoning, }); } if (delta.content != null && delta.content.length > 0) { if (!isActiveText) { controller.enqueue({ type: 'text-start', id: 'txt-0' }); isActiveText = true; } controller.enqueue({ type: 'text-delta', id: 'txt-0', delta: delta.content, }); } if (delta.tool_calls != null) { for (const toolCallDelta of delta.tool_calls) { const index = toolCallDelta.index; if (toolCalls[index] == null) { if (toolCallDelta.type !== 'function') { throw new InvalidResponseDataError({ data: toolCallDelta, message: `Expected 'function' type.`, }); } if (toolCallDelta.id == null) { throw new InvalidResponseDataError({ data: toolCallDelta, message: `Expected 'id' to be a string.`, }); } if (toolCallDelta.function?.name == null) { throw new InvalidResponseDataError({ data: toolCallDelta, message: `Expected 'function.name' to be a string.`, }); } controller.enqueue({ type: 'tool-input-start', id: toolCallDelta.id, toolName: toolCallDelta.function.name, }); toolCalls[index] = { id: toolCallDelta.id, type: 'function', function: { name: toolCallDelta.function.name, arguments: toolCallDelta.function.arguments ?? '', }, hasFinished: false, }; const toolCall = toolCalls[index]; if ( toolCall.function?.name != null && toolCall.function?.arguments != null ) { // send delta if the argument text has already started: if (toolCall.function.arguments.length > 0) { controller.enqueue({ type: 'tool-input-delta', id: toolCall.id, delta: toolCall.function.arguments, }); } // check if tool call is complete // (some providers send the full tool call in one chunk): if (isParsableJson(toolCall.function.arguments)) { controller.enqueue({ type: 'tool-input-end', id: toolCall.id, }); controller.enqueue({ type: 'tool-call', toolCallId: toolCall.id ?? generateId(), toolName: toolCall.function.name, input: toolCall.function.arguments, }); toolCall.hasFinished = true; } } continue; } // existing tool call, merge if not finished const toolCall = toolCalls[index]; if (toolCall.hasFinished) { continue; } if (toolCallDelta.function?.arguments != null) { toolCall.function!.arguments += toolCallDelta.function?.arguments ?? ''; } // send delta controller.enqueue({ type: 'tool-input-delta', id: toolCall.id, delta: toolCallDelta.function.arguments ?? '', }); // check if tool call is complete if ( toolCall.function?.name != null && toolCall.function?.arguments != null && isParsableJson(toolCall.function.arguments) ) { controller.enqueue({ type: 'tool-input-end', id: toolCall.id, }); controller.enqueue({ type: 'tool-call', toolCallId: toolCall.id ?? generateId(), toolName: toolCall.function.name, input: toolCall.function.arguments, }); toolCall.hasFinished = true; } } } }, flush(controller) { if (isActiveReasoning) { controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' }); } if (isActiveText) { controller.enqueue({ type: 'text-end', id: 'txt-0' }); } controller.enqueue({ type: 'finish', finishReason, usage, ...(providerMetadata != null ? { providerMetadata } : {}), }); }, }), ), request: { body }, response: { headers: responseHeaders }, }; } } // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const groqChatResponseSchema = z.object({ id: z.string().nullish(), created: z.number().nullish(), model: z.string().nullish(), choices: z.array( z.object({ message: z.object({ content: z.string().nullish(), reasoning: z.string().nullish(), tool_calls: z .array( z.object({ id: z.string().nullish(), type: z.literal('function'), function: z.object({ name: z.string(), arguments: z.string(), }), }), ) .nullish(), }), index: z.number(), finish_reason: z.string().nullish(), }), ), usage: z .object({ prompt_tokens: z.number().nullish(), completion_tokens: z.number().nullish(), total_tokens: z.number().nullish(), }) .nullish(), }); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const groqChatChunkSchema = z.union([ z.object({ id: z.string().nullish(), created: z.number().nullish(), model: z.string().nullish(), choices: z.array( z.object({ delta: z .object({ content: z.string().nullish(), reasoning: z.string().nullish(), tool_calls: z .array( z.object({ index: z.number(), id: z.string().nullish(), type: z.literal('function').optional(), function: z.object({ name: z.string().nullish(), arguments: z.string().nullish(), }), }), ) .nullish(), }) .nullish(), finish_reason: z.string().nullable().optional(), index: z.number(), }), ), x_groq: z .object({ usage: z .object({ prompt_tokens: z.number().nullish(), completion_tokens: z.number().nullish(), total_tokens: z.number().nullish(), }) .nullish(), }) .nullish(), }), groqErrorDataSchema, ]); --- File: /ai/packages/groq/src/groq-chat-options.ts --- import { z } from 'zod/v4'; // https://console.groq.com/docs/models export type GroqChatModelId = // production models | 'gemma2-9b-it' | 'llama-3.1-8b-instant' | 'llama-3.3-70b-versatile' | 'meta-llama/llama-guard-4-12b' | 'openai/gpt-oss-120b' | 'openai/gpt-oss-20b' // preview models (selection) | 'deepseek-r1-distill-llama-70b' | 'meta-llama/llama-4-maverick-17b-128e-instruct' | 'meta-llama/llama-4-scout-17b-16e-instruct' | 'meta-llama/llama-prompt-guard-2-22m' | 'meta-llama/llama-prompt-guard-2-86m' | 'mistral-saba-24b' | 'moonshotai/kimi-k2-instruct' | 'qwen/qwen3-32b' | 'llama-guard-3-8b' | 'llama3-70b-8192' | 'llama3-8b-8192' | 'mixtral-8x7b-32768' | 'qwen-qwq-32b' | 'qwen-2.5-32b' | 'deepseek-r1-distill-qwen-32b' | (string & {}); export const groqProviderOptions = z.object({ reasoningFormat: z.enum(['parsed', 'raw', 'hidden']).optional(), reasoningEffort: z.enum(['none', 'default']).optional(), /** * Whether to enable parallel function calling during tool use. Default to true. */ parallelToolCalls: z.boolean().optional(), /** * A unique identifier representing your end-user, which can help OpenAI to * monitor and detect abuse. Learn more. */ user: z.string().optional(), /** * Whether to use structured outputs. * * @default true */ structuredOutputs: z.boolean().optional(), }); export type GroqProviderOptions = z.infer<typeof groqProviderOptions>; --- File: /ai/packages/groq/src/groq-config.ts --- import { FetchFunction } from '@ai-sdk/provider-utils'; export type GroqConfig = { provider: string; url: (options: { modelId: string; path: string }) => string; headers: () => Record<string, string | undefined>; fetch?: FetchFunction; generateId?: () => string; }; --- File: /ai/packages/groq/src/groq-error.ts --- import { z } from 'zod/v4'; import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; export const groqErrorDataSchema = z.object({ error: z.object({ message: z.string(), type: z.string(), }), }); export type GroqErrorData = z.infer<typeof groqErrorDataSchema>; export const groqFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: groqErrorDataSchema, errorToMessage: data => data.error.message, }); --- File: /ai/packages/groq/src/groq-prepare-tools.ts --- import { LanguageModelV2CallOptions, LanguageModelV2CallWarning, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; export function prepareTools({ tools, toolChoice, }: { tools: LanguageModelV2CallOptions['tools']; toolChoice?: LanguageModelV2CallOptions['toolChoice']; }): { tools: | undefined | Array<{ type: 'function'; function: { name: string; description: string | undefined; parameters: unknown; }; }>; toolChoice: | { type: 'function'; function: { name: string } } | 'auto' | 'none' | 'required' | undefined; toolWarnings: LanguageModelV2CallWarning[]; } { // when the tools array is empty, change it to undefined to prevent errors: tools = tools?.length ? tools : undefined; const toolWarnings: LanguageModelV2CallWarning[] = []; if (tools == null) { return { tools: undefined, toolChoice: undefined, toolWarnings }; } const groqTools: Array<{ type: 'function'; function: { name: string; description: string | undefined; parameters: unknown; }; }> = []; for (const tool of tools) { if (tool.type === 'provider-defined') { toolWarnings.push({ type: 'unsupported-tool', tool }); } else { groqTools.push({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.inputSchema, }, }); } } if (toolChoice == null) { return { tools: groqTools, toolChoice: undefined, toolWarnings }; } const type = toolChoice.type; switch (type) { case 'auto': case 'none': case 'required': return { tools: groqTools, toolChoice: type, toolWarnings }; case 'tool': return { tools: groqTools, toolChoice: { type: 'function', function: { name: toolChoice.toolName, }, }, toolWarnings, }; default: { const _exhaustiveCheck: never = type; throw new UnsupportedFunctionalityError({ functionality: `tool choice type: ${_exhaustiveCheck}`, }); } } } --- File: /ai/packages/groq/src/groq-provider.ts --- import { LanguageModelV2, NoSuchModelError, ProviderV2, TranscriptionModelV2, } from '@ai-sdk/provider'; import { FetchFunction, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { GroqChatLanguageModel } from './groq-chat-language-model'; import { GroqChatModelId } from './groq-chat-options'; import { GroqTranscriptionModelId } from './groq-transcription-options'; import { GroqTranscriptionModel } from './groq-transcription-model'; export interface GroqProvider extends ProviderV2 { /** Creates a model for text generation. */ (modelId: GroqChatModelId): LanguageModelV2; /** Creates an Groq chat model for text generation. */ languageModel(modelId: GroqChatModelId): LanguageModelV2; /** Creates a model for transcription. */ transcription(modelId: GroqTranscriptionModelId): TranscriptionModelV2; } export interface GroqProviderSettings { /** Base URL for the Groq API calls. */ baseURL?: string; /** API key for authenticating requests. */ apiKey?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } /** Create an Groq provider instance. */ export function createGroq(options: GroqProviderSettings = {}): GroqProvider { const baseURL = withoutTrailingSlash(options.baseURL) ?? 'https://api.groq.com/openai/v1'; const getHeaders = () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'GROQ_API_KEY', description: 'Groq', })}`, ...options.headers, }); const createChatModel = (modelId: GroqChatModelId) => new GroqChatLanguageModel(modelId, { provider: 'groq.chat', url: ({ path }) => `${baseURL}${path}`, headers: getHeaders, fetch: options.fetch, }); const createLanguageModel = (modelId: GroqChatModelId) => { if (new.target) { throw new Error( 'The Groq model function cannot be called with the new keyword.', ); } return createChatModel(modelId); }; const createTranscriptionModel = (modelId: GroqTranscriptionModelId) => { return new GroqTranscriptionModel(modelId, { provider: 'groq.transcription', url: ({ path }) => `${baseURL}${path}`, headers: getHeaders, fetch: options.fetch, }); }; const provider = function (modelId: GroqChatModelId) { return createLanguageModel(modelId); }; provider.languageModel = createLanguageModel; provider.chat = createChatModel; provider.textEmbeddingModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'textEmbeddingModel' }); }; provider.imageModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'imageModel' }); }; provider.transcription = createTranscriptionModel; return provider; } /** Default Groq provider instance. */ export const groq = createGroq(); --- File: /ai/packages/groq/src/groq-transcription-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { GroqTranscriptionModel } from './groq-transcription-model'; import { createGroq } from './groq-provider'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; const audioData = await readFile(path.join(__dirname, 'transcript-test.mp3')); const provider = createGroq({ apiKey: 'test-api-key' }); const model = provider.transcription('whisper-large-v3-turbo'); const server = createTestServer({ 'https://api.groq.com/openai/v1/audio/transcriptions': {}, }); describe('doGenerate', () => { function prepareJsonResponse({ headers, }: { headers?: Record<string, string>; } = {}) { server.urls[ 'https://api.groq.com/openai/v1/audio/transcriptions' ].response = { type: 'json-value', headers, body: { task: 'transcribe', language: 'English', duration: 2.5, text: 'Hello world!', segments: [ { id: 0, seek: 0, start: 0, end: 2.48, text: 'Hello world!', tokens: [50365, 2425, 490, 264], temperature: 0, avg_logprob: -0.29010406, compression_ratio: 0.7777778, no_speech_prob: 0.032802984, }, ], x_groq: { id: 'req_01jrh9nn61f24rydqq1r4b3yg5' }, }, }; } it('should pass the model', async () => { prepareJsonResponse(); await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(await server.calls[0].requestBodyMultipart).toMatchObject({ model: 'whisper-large-v3-turbo', }); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createGroq({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.transcription('whisper-large-v3-turbo').doGenerate({ audio: audioData, mediaType: 'audio/wav', headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toMatchObject({ authorization: 'Bearer test-api-key', 'content-type': expect.stringMatching( /^multipart\/form-data; boundary=----formdata-undici-\d+$/, ), 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should extract the transcription text', async () => { prepareJsonResponse(); const result = await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.text).toBe('Hello world!'); }); it('should include response data with timestamp, modelId and headers', async () => { prepareJsonResponse({ headers: { 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); const testDate = new Date(0); const customModel = new GroqTranscriptionModel('whisper-large-v3-turbo', { provider: 'test-provider', url: () => 'https://api.groq.com/openai/v1/audio/transcriptions', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.response).toMatchObject({ timestamp: testDate, modelId: 'whisper-large-v3-turbo', headers: { 'content-type': 'application/json', 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); }); it('should use real date when no custom date provider is specified', async () => { prepareJsonResponse(); const testDate = new Date(0); const customModel = new GroqTranscriptionModel('whisper-large-v3-turbo', { provider: 'test-provider', url: () => 'https://api.groq.com/openai/v1/audio/transcriptions', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.response.timestamp.getTime()).toEqual(testDate.getTime()); expect(result.response.modelId).toBe('whisper-large-v3-turbo'); }); }); --- File: /ai/packages/groq/src/groq-transcription-model.ts --- import { TranscriptionModelV2, TranscriptionModelV2CallWarning, } from '@ai-sdk/provider'; import { combineHeaders, convertBase64ToUint8Array, createJsonResponseHandler, parseProviderOptions, postFormDataToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { GroqConfig } from './groq-config'; import { groqFailedResponseHandler } from './groq-error'; import { GroqTranscriptionModelId } from './groq-transcription-options'; import { GroqTranscriptionAPITypes } from './groq-api-types'; // https://console.groq.com/docs/speech-to-text const groqProviderOptionsSchema = z.object({ language: z.string().nullish(), prompt: z.string().nullish(), responseFormat: z.string().nullish(), temperature: z.number().min(0).max(1).nullish(), timestampGranularities: z.array(z.string()).nullish(), }); export type GroqTranscriptionCallOptions = z.infer< typeof groqProviderOptionsSchema >; interface GroqTranscriptionModelConfig extends GroqConfig { _internal?: { currentDate?: () => Date; }; } export class GroqTranscriptionModel implements TranscriptionModelV2 { readonly specificationVersion = 'v2'; get provider(): string { return this.config.provider; } constructor( readonly modelId: GroqTranscriptionModelId, private readonly config: GroqTranscriptionModelConfig, ) {} private async getArgs({ audio, mediaType, providerOptions, }: Parameters<TranscriptionModelV2['doGenerate']>[0]) { const warnings: TranscriptionModelV2CallWarning[] = []; // Parse provider options const groqOptions = await parseProviderOptions({ provider: 'groq', providerOptions, schema: groqProviderOptionsSchema, }); // Create form data with base fields const formData = new FormData(); const blob = audio instanceof Uint8Array ? new Blob([audio]) : new Blob([convertBase64ToUint8Array(audio)]); formData.append('model', this.modelId); formData.append('file', new File([blob], 'audio', { type: mediaType })); // Add provider-specific options if (groqOptions) { const transcriptionModelOptions: Omit< GroqTranscriptionAPITypes, 'model' > = { language: groqOptions.language ?? undefined, prompt: groqOptions.prompt ?? undefined, response_format: groqOptions.responseFormat ?? undefined, temperature: groqOptions.temperature ?? undefined, timestamp_granularities: groqOptions.timestampGranularities ?? undefined, }; for (const key in transcriptionModelOptions) { const value = transcriptionModelOptions[ key as keyof Omit<GroqTranscriptionAPITypes, 'model'> ]; if (value !== undefined) { formData.append(key, String(value)); } } } return { formData, warnings, }; } async doGenerate( options: Parameters<TranscriptionModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<TranscriptionModelV2['doGenerate']>>> { const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { formData, warnings } = await this.getArgs(options); const { value: response, responseHeaders, rawValue: rawResponse, } = await postFormDataToApi({ url: this.config.url({ path: '/audio/transcriptions', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), formData, failedResponseHandler: groqFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( groqTranscriptionResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); return { text: response.text, segments: response.segments?.map(segment => ({ text: segment.text, startSecond: segment.start, endSecond: segment.end, })) ?? [], language: response.language, durationInSeconds: response.duration, warnings, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, body: rawResponse, }, }; } } const groqTranscriptionResponseSchema = z.object({ task: z.string(), language: z.string(), duration: z.number(), text: z.string(), segments: z.array( z.object({ id: z.number(), seek: z.number(), start: z.number(), end: z.number(), text: z.string(), tokens: z.array(z.number()), temperature: z.number(), avg_logprob: z.number(), compression_ratio: z.number(), no_speech_prob: z.number(), }), ), x_groq: z.object({ id: z.string(), }), }); --- File: /ai/packages/groq/src/groq-transcription-options.ts --- export type GroqTranscriptionModelId = | 'whisper-large-v3-turbo' | 'distil-whisper-large-v3-en' | 'whisper-large-v3' | (string & {}); --- File: /ai/packages/groq/src/index.ts --- export { createGroq, groq } from './groq-provider'; export type { GroqProvider, GroqProviderSettings } from './groq-provider'; export type { GroqProviderOptions } from './groq-chat-options'; --- File: /ai/packages/groq/src/map-groq-finish-reason.ts --- import { LanguageModelV2FinishReason } from '@ai-sdk/provider'; export function mapGroqFinishReason( finishReason: string | null | undefined, ): LanguageModelV2FinishReason { switch (finishReason) { case 'stop': return 'stop'; case 'length': return 'length'; case 'content_filter': return 'content-filter'; case 'function_call': case 'tool_calls': return 'tool-calls'; default: return 'unknown'; } } --- File: /ai/packages/groq/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/groq/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/groq/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/hume/src/hume-api-types.ts --- type HumeSpeechAPIUtterances = Array<{ text: string; description?: string; speed?: number; trailing_silence?: number; voice?: | { id: string; provider?: 'HUME_AI' | 'CUSTOM_VOICE'; } | { name: string; provider?: 'HUME_AI' | 'CUSTOM_VOICE'; }; }>; export type HumeSpeechAPITypes = { utterances: HumeSpeechAPIUtterances; context?: | { generation_id: string; } | { utterances: HumeSpeechAPIUtterances; }; format: { type: 'mp3' | 'pcm' | 'wav'; }; }; --- File: /ai/packages/hume/src/hume-config.ts --- import { FetchFunction } from '@ai-sdk/provider-utils'; export type HumeConfig = { provider: string; url: (options: { modelId: string; path: string }) => string; headers: () => Record<string, string | undefined>; fetch?: FetchFunction; generateId?: () => string; }; --- File: /ai/packages/hume/src/hume-error.test.ts --- import { safeParseJSON } from '@ai-sdk/provider-utils'; import { humeErrorDataSchema } from './hume-error'; describe('humeErrorDataSchema', () => { it('should parse Hume resource exhausted error', async () => { const error = ` {"error":{"message":"{\\n \\"error\\": {\\n \\"code\\": 429,\\n \\"message\\": \\"Resource has been exhausted (e.g. check quota).\\",\\n \\"status\\": \\"RESOURCE_EXHAUSTED\\"\\n }\\n}\\n","code":429}} `; const result = await safeParseJSON({ text: error, schema: humeErrorDataSchema, }); expect(result).toStrictEqual({ success: true, value: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, rawValue: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, }); }); }); --- File: /ai/packages/hume/src/hume-error.ts --- import { z } from 'zod/v4'; import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; export const humeErrorDataSchema = z.object({ error: z.object({ message: z.string(), code: z.number(), }), }); export type HumeErrorData = z.infer<typeof humeErrorDataSchema>; export const humeFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: humeErrorDataSchema, errorToMessage: data => data.error.message, }); --- File: /ai/packages/hume/src/hume-provider.ts --- import { SpeechModelV2, ProviderV2 } from '@ai-sdk/provider'; import { FetchFunction, loadApiKey } from '@ai-sdk/provider-utils'; import { HumeSpeechModel } from './hume-speech-model'; export interface HumeProvider extends Pick<ProviderV2, 'speechModel'> { (settings?: {}): { speech: HumeSpeechModel; }; /** Creates a model for speech synthesis. */ speech(): SpeechModelV2; } export interface HumeProviderSettings { /** API key for authenticating requests. */ apiKey?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } /** Create an Hume provider instance. */ export function createHume(options: HumeProviderSettings = {}): HumeProvider { const getHeaders = () => ({ 'X-Hume-Api-Key': loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'HUME_API_KEY', description: 'Hume', }), ...options.headers, }); const createSpeechModel = () => new HumeSpeechModel('', { provider: `hume.speech`, url: ({ path }) => `https://api.hume.ai${path}`, headers: getHeaders, fetch: options.fetch, }); const provider = function () { return { speech: createSpeechModel(), }; }; provider.speech = createSpeechModel; provider.speechModel = createSpeechModel; return provider satisfies HumeProvider; } /** Default Hume provider instance. */ export const hume = createHume(); --- File: /ai/packages/hume/src/hume-speech-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { HumeSpeechModel } from './hume-speech-model'; import { createHume } from './hume-provider'; const provider = createHume({ apiKey: 'test-api-key' }); const model = provider.speech(); const server = createTestServer({ 'https://api.hume.ai/v0/tts/file': {}, }); describe('doGenerate', () => { function prepareAudioResponse({ headers, format = 'mp3', }: { headers?: Record<string, string>; format?: 'mp3' | 'pcm' | 'wav'; } = {}) { const audioBuffer = new Uint8Array(100); // Mock audio data server.urls['https://api.hume.ai/v0/tts/file'].response = { type: 'binary', headers: { 'content-type': `audio/${format}`, ...headers, }, body: Buffer.from(audioBuffer), }; return audioBuffer; } it('should pass the model and text', async () => { prepareAudioResponse(); await model.doGenerate({ text: 'Hello from the AI SDK!', }); expect(await server.calls[0].requestBodyJson).toMatchObject({ utterances: [ { text: 'Hello from the AI SDK!', voice: { id: 'd8ab67c6-953d-4bd8-9370-8fa53a0f1453', provider: 'HUME_AI', }, }, ], format: { type: 'mp3', }, }); }); it('should pass headers', async () => { prepareAudioResponse(); const provider = createHume({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.speech().doGenerate({ text: 'Hello from the AI SDK!', headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toMatchObject({ 'x-hume-api-key': 'test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should pass options', async () => { prepareAudioResponse(); await model.doGenerate({ text: 'Hello from the AI SDK!', voice: 'test-voice', outputFormat: 'mp3', speed: 1.5, }); expect(await server.calls[0].requestBodyJson).toMatchObject({ utterances: [ { text: 'Hello from the AI SDK!', voice: { id: 'test-voice', provider: 'HUME_AI', }, speed: 1.5, }, ], format: { type: 'mp3', }, }); }); it('should return audio data with correct content type', async () => { const audio = new Uint8Array(100); // Mock audio data prepareAudioResponse({ format: 'mp3', headers: { 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); const result = await model.doGenerate({ text: 'Hello from the AI SDK!', outputFormat: 'mp3', }); expect(result.audio).toStrictEqual(audio); }); it('should include response data with timestamp, modelId and headers', async () => { prepareAudioResponse({ headers: { 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); const testDate = new Date(0); const customModel = new HumeSpeechModel('', { provider: 'test-provider', url: () => 'https://api.hume.ai/v0/tts/file', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ text: 'Hello from the AI SDK!', }); expect(result.response).toMatchObject({ timestamp: testDate, headers: { 'content-type': 'audio/mp3', 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); }); it('should use real date when no custom date provider is specified', async () => { prepareAudioResponse(); const testDate = new Date(0); const customModel = new HumeSpeechModel('', { provider: 'test-provider', url: () => 'https://api.hume.ai/v0/tts/file', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ text: 'Hello from the AI SDK!', }); expect(result.response.timestamp.getTime()).toEqual(testDate.getTime()); expect(result.response.modelId).toBe(''); }); it('should handle different audio formats', async () => { const formats = ['mp3', 'pcm', 'wav'] as const; for (const format of formats) { const audio = prepareAudioResponse({ format }); const result = await model.doGenerate({ text: 'Hello from the AI SDK!', providerOptions: { lmnt: { format, }, }, }); expect(result.audio).toStrictEqual(audio); } }); it('should include warnings if any are generated', async () => { prepareAudioResponse(); const result = await model.doGenerate({ text: 'Hello from the AI SDK!', }); expect(result.warnings).toEqual([]); }); }); --- File: /ai/packages/hume/src/hume-speech-model.ts --- import { SpeechModelV2, SpeechModelV2CallWarning } from '@ai-sdk/provider'; import { combineHeaders, createBinaryResponseHandler, parseProviderOptions, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { HumeConfig } from './hume-config'; import { humeFailedResponseHandler } from './hume-error'; import { HumeSpeechAPITypes } from './hume-api-types'; // https://dev.hume.ai/reference/text-to-speech-tts/synthesize-file const humeSpeechCallOptionsSchema = z.object({ /** * Context for the speech synthesis request. * Can be either a generationId for retrieving a previous generation, * or a list of utterances to synthesize. */ context: z .object({ /** * ID of a previously generated speech synthesis to retrieve. */ generationId: z.string(), }) .or( z.object({ /** * List of utterances to synthesize into speech. */ utterances: z.array( z.object({ /** * The text content to convert to speech. */ text: z.string(), /** * Optional description or instructions for how the text should be spoken. */ description: z.string().optional(), /** * Optional speech rate multiplier. */ speed: z.number().optional(), /** * Optional duration of silence to add after the utterance in seconds. */ trailingSilence: z.number().optional(), /** * Voice configuration for the utterance. * Can be specified by ID or name. */ voice: z .object({ /** * ID of the voice to use. */ id: z.string(), /** * Provider of the voice, either Hume's built-in voices or a custom voice. */ provider: z.enum(['HUME_AI', 'CUSTOM_VOICE']).optional(), }) .or( z.object({ /** * Name of the voice to use. */ name: z.string(), /** * Provider of the voice, either Hume's built-in voices or a custom voice. */ provider: z.enum(['HUME_AI', 'CUSTOM_VOICE']).optional(), }), ) .optional(), }), ), }), ) .nullish(), }); export type HumeSpeechCallOptions = z.infer<typeof humeSpeechCallOptionsSchema>; interface HumeSpeechModelConfig extends HumeConfig { _internal?: { currentDate?: () => Date; }; } export class HumeSpeechModel implements SpeechModelV2 { readonly specificationVersion = 'v2'; get provider(): string { return this.config.provider; } constructor( readonly modelId: '', private readonly config: HumeSpeechModelConfig, ) {} private async getArgs({ text, voice = 'd8ab67c6-953d-4bd8-9370-8fa53a0f1453', outputFormat = 'mp3', speed, instructions, language, providerOptions, }: Parameters<SpeechModelV2['doGenerate']>[0]) { const warnings: SpeechModelV2CallWarning[] = []; // Parse provider options const humeOptions = await parseProviderOptions({ provider: 'hume', providerOptions, schema: humeSpeechCallOptionsSchema, }); // Create request body const requestBody: HumeSpeechAPITypes = { utterances: [ { text, speed, description: instructions, voice: { id: voice, provider: 'HUME_AI', }, }, ], format: { type: 'mp3' }, }; if (outputFormat) { if (['mp3', 'pcm', 'wav'].includes(outputFormat)) { requestBody.format = { type: outputFormat as 'mp3' | 'pcm' | 'wav' }; } else { warnings.push({ type: 'unsupported-setting', setting: 'outputFormat', details: `Unsupported output format: ${outputFormat}. Using mp3 instead.`, }); } } // Add provider-specific options if (humeOptions) { const speechModelOptions: Omit< HumeSpeechAPITypes, 'utterances' | 'format' > = {}; if (humeOptions.context) { if ('generationId' in humeOptions.context) { speechModelOptions.context = { generation_id: humeOptions.context.generationId, }; } else { speechModelOptions.context = { utterances: humeOptions.context.utterances.map(utterance => ({ text: utterance.text, description: utterance.description, speed: utterance.speed, trailing_silence: utterance.trailingSilence, voice: utterance.voice, })), }; } } for (const key in speechModelOptions) { const value = speechModelOptions[ key as keyof Omit<HumeSpeechAPITypes, 'utterances' | 'format'> ]; if (value !== undefined) { (requestBody as Record<string, unknown>)[key] = value; } } } if (language) { warnings.push({ type: 'unsupported-setting', setting: 'language', details: `Hume speech models do not support language selection. Language parameter "${language}" was ignored.`, }); } return { requestBody, warnings, }; } async doGenerate( options: Parameters<SpeechModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<SpeechModelV2['doGenerate']>>> { const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { requestBody, warnings } = await this.getArgs(options); const { value: audio, responseHeaders, rawValue: rawResponse, } = await postJsonToApi({ url: this.config.url({ path: '/v0/tts/file', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body: requestBody, failedResponseHandler: humeFailedResponseHandler, successfulResponseHandler: createBinaryResponseHandler(), abortSignal: options.abortSignal, fetch: this.config.fetch, }); return { audio, warnings, request: { body: JSON.stringify(requestBody), }, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, body: rawResponse, }, }; } } --- File: /ai/packages/hume/src/index.ts --- export { createHume, hume } from './hume-provider'; export type { HumeProvider, HumeProviderSettings } from './hume-provider'; --- File: /ai/packages/hume/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/hume/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/hume/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/langchain/src/index.ts --- export * from './langchain-adapter'; --- File: /ai/packages/langchain/src/langchain-adapter.test.ts --- import { convertArrayToReadableStream, convertReadableStreamToArray, } from 'ai/test'; import { toUIMessageStream } from './langchain-adapter'; describe('toUIMessageStream', () => { it('should convert ReadableStream<LangChainAIMessageChunk>', async () => { const inputStream = convertArrayToReadableStream([ { content: 'Hello' }, { content: [{ type: 'text', text: 'World' }] }, ]); expect(await convertReadableStreamToArray(toUIMessageStream(inputStream))) .toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, { "delta": "Hello", "id": "1", "type": "text-delta", }, { "delta": "World", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, ] `); }); it('should convert ReadableStream<string> (LangChain StringOutputParser)', async () => { const inputStream = convertArrayToReadableStream(['Hello', 'World']); expect(await convertReadableStreamToArray(toUIMessageStream(inputStream))) .toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, { "delta": "Hello", "id": "1", "type": "text-delta", }, { "delta": "World", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, ] `); }); it('should convert ReadableStream<LangChainStreamEvent>', async () => { const inputStream = convertArrayToReadableStream([ { event: 'on_chat_model_stream', data: { chunk: { content: 'Hello' } } }, { event: 'on_chat_model_stream', data: { chunk: { content: 'World' } } }, ]); expect(await convertReadableStreamToArray(toUIMessageStream(inputStream))) .toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, { "delta": "Hello", "id": "1", "type": "text-delta", }, { "delta": "World", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, ] `); }); }); --- File: /ai/packages/langchain/src/langchain-adapter.ts --- import { UIMessageChunk } from 'ai'; import { createCallbacksTransformer, StreamCallbacks, } from './stream-callbacks'; type LangChainImageDetail = 'auto' | 'low' | 'high'; type LangChainMessageContentText = { type: 'text'; text: string; }; type LangChainMessageContentImageUrl = { type: 'image_url'; image_url: | string | { url: string; detail?: LangChainImageDetail; }; }; type LangChainMessageContentComplex = | LangChainMessageContentText | LangChainMessageContentImageUrl | (Record<string, any> & { type?: 'text' | 'image_url' | string; }) | (Record<string, any> & { type?: never; }); type LangChainMessageContent = string | LangChainMessageContentComplex[]; type LangChainAIMessageChunk = { content: LangChainMessageContent; }; // LC stream event v2 type LangChainStreamEvent = { event: string; data: any; }; /** Converts LangChain output streams to an AI SDK Data Stream. The following streams are supported: - `LangChainAIMessageChunk` streams (LangChain `model.stream` output) - `string` streams (LangChain `StringOutputParser` output) */ export function toUIMessageStream( stream: | ReadableStream<LangChainStreamEvent> | ReadableStream<LangChainAIMessageChunk> | ReadableStream<string>, callbacks?: StreamCallbacks, ) { return stream .pipeThrough( new TransformStream< LangChainStreamEvent | LangChainAIMessageChunk | string >({ transform: async (value, controller) => { // text stream: if (typeof value === 'string') { controller.enqueue(value); return; } // LC stream events v2: if ('event' in value) { // chunk is AIMessage Chunk for on_chat_model_stream event: if (value.event === 'on_chat_model_stream') { forwardAIMessageChunk( value.data?.chunk as LangChainAIMessageChunk, controller, ); } return; } // AI Message chunk stream: forwardAIMessageChunk(value, controller); }, }), ) .pipeThrough(createCallbacksTransformer(callbacks)) .pipeThrough( new TransformStream<string, UIMessageChunk>({ start: async controller => { controller.enqueue({ type: 'text-start', id: '1' }); }, transform: async (chunk, controller) => { controller.enqueue({ type: 'text-delta', delta: chunk, id: '1' }); }, flush: async controller => { controller.enqueue({ type: 'text-end', id: '1' }); }, }), ); } function forwardAIMessageChunk( chunk: LangChainAIMessageChunk, controller: TransformStreamDefaultController<any>, ) { if (typeof chunk.content === 'string') { controller.enqueue(chunk.content); } else { const content: LangChainMessageContentComplex[] = chunk.content; for (const item of content) { if (item.type === 'text') { controller.enqueue(item.text); } } } } --- File: /ai/packages/langchain/src/stream-callbacks.ts --- /** * Configuration options and helper callback methods for stream lifecycle events. */ export interface StreamCallbacks { /** `onStart`: Called once when the stream is initialized. */ onStart?: () => Promise<void> | void; /** `onFinal`: Called once when the stream is closed with the final completion message. */ onFinal?: (completion: string) => Promise<void> | void; /** `onToken`: Called for each tokenized message. */ onToken?: (token: string) => Promise<void> | void; /** `onText`: Called for each text chunk. */ onText?: (text: string) => Promise<void> | void; } /** * Creates a transform stream that encodes input messages and invokes optional callback functions. * The transform stream uses the provided callbacks to execute custom logic at different stages of the stream's lifecycle. * - `onStart`: Called once when the stream is initialized. * - `onToken`: Called for each tokenized message. * - `onFinal`: Called once when the stream is closed with the final completion message. * * This function is useful when you want to process a stream of messages and perform specific actions during the stream's lifecycle. * * @param {StreamCallbacks} [callbacks] - An object containing the callback functions. * @return {TransformStream<string, Uint8Array>} A transform stream that encodes input messages as Uint8Array and allows the execution of custom logic through callbacks. * * @example * const callbacks = { * onStart: async () => console.log('Stream started'), * onToken: async (token) => console.log(`Token: ${token}`), * onFinal: async () => data.close() * }; * const transformer = createCallbacksTransformer(callbacks); */ export function createCallbacksTransformer( callbacks: StreamCallbacks | undefined = {}, ): TransformStream<string, string> { let aggregatedResponse = ''; return new TransformStream({ async start(): Promise<void> { if (callbacks.onStart) await callbacks.onStart(); }, async transform(message, controller): Promise<void> { controller.enqueue(message); aggregatedResponse += message; if (callbacks.onToken) await callbacks.onToken(message); if (callbacks.onText && typeof message === 'string') { await callbacks.onText(message); } }, async flush(): Promise<void> { if (callbacks.onFinal) { await callbacks.onFinal(aggregatedResponse); } }, }); } --- File: /ai/packages/langchain/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/langchain/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts{,x}'], exclude: [ '**/*.ui.test.ts{,x}', '**/*.e2e.test.ts{,x}', '**/node_modules/**', ], typecheck: { enabled: true, }, }, }); --- File: /ai/packages/langchain/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts{,x}'], exclude: [ '**/*.ui.test.ts{,x}', '**/*.e2e.test.ts{,x}', '**/node_modules/**', ], typecheck: { enabled: true, }, }, }); --- File: /ai/packages/llamaindex/src/index.ts --- export * from './llamaindex-adapter'; --- File: /ai/packages/llamaindex/src/llamaindex-adapter.test.ts --- import { convertArrayToAsyncIterable, convertReadableStreamToArray, } from 'ai/test'; import { toUIMessageStream } from './llamaindex-adapter'; describe('toUIMessageStream', () => { it('should convert AsyncIterable<EngineResponse>', async () => { const inputStream = convertArrayToAsyncIterable([ { delta: 'Hello' }, { delta: 'World' }, ]); expect(await convertReadableStreamToArray(toUIMessageStream(inputStream))) .toMatchInlineSnapshot(` [ { "id": "1", "type": "text-start", }, { "delta": "Hello", "id": "1", "type": "text-delta", }, { "delta": "World", "id": "1", "type": "text-delta", }, { "id": "1", "type": "text-end", }, ] `); }); }); --- File: /ai/packages/llamaindex/src/llamaindex-adapter.ts --- import { UIMessageChunk } from 'ai'; import { convertAsyncIteratorToReadableStream } from 'ai/internal'; import { createCallbacksTransformer, StreamCallbacks, } from './stream-callbacks'; type EngineResponse = { delta: string; }; export function toUIMessageStream( stream: AsyncIterable<EngineResponse>, callbacks?: StreamCallbacks, ) { const trimStart = trimStartOfStream(); return convertAsyncIteratorToReadableStream(stream[Symbol.asyncIterator]()) .pipeThrough( new TransformStream({ async transform(message, controller): Promise<void> { controller.enqueue(trimStart(message.delta)); }, }), ) .pipeThrough(createCallbacksTransformer(callbacks)) .pipeThrough( new TransformStream<string, UIMessageChunk>({ start: async controller => { controller.enqueue({ type: 'text-start', id: '1' }); }, transform: async (chunk, controller) => { controller.enqueue({ type: 'text-delta', delta: chunk, id: '1' }); }, flush: async controller => { controller.enqueue({ type: 'text-end', id: '1' }); }, }), ); } function trimStartOfStream(): (text: string) => string { let isStreamStart = true; return (text: string): string => { if (isStreamStart) { text = text.trimStart(); if (text) isStreamStart = false; } return text; }; } --- File: /ai/packages/llamaindex/src/stream-callbacks.ts --- /** * Configuration options and helper callback methods for stream lifecycle events. */ export interface StreamCallbacks { /** `onStart`: Called once when the stream is initialized. */ onStart?: () => Promise<void> | void; /** `onFinal`: Called once when the stream is closed with the final completion message. */ onFinal?: (completion: string) => Promise<void> | void; /** `onToken`: Called for each tokenized message. */ onToken?: (token: string) => Promise<void> | void; /** `onText`: Called for each text chunk. */ onText?: (text: string) => Promise<void> | void; } /** * Creates a transform stream that encodes input messages and invokes optional callback functions. * The transform stream uses the provided callbacks to execute custom logic at different stages of the stream's lifecycle. * - `onStart`: Called once when the stream is initialized. * - `onToken`: Called for each tokenized message. * - `onFinal`: Called once when the stream is closed with the final completion message. * * This function is useful when you want to process a stream of messages and perform specific actions during the stream's lifecycle. * * @param {StreamCallbacks} [callbacks] - An object containing the callback functions. * @return {TransformStream<string, Uint8Array>} A transform stream that encodes input messages as Uint8Array and allows the execution of custom logic through callbacks. * * @example * const callbacks = { * onStart: async () => console.log('Stream started'), * onToken: async (token) => console.log(`Token: ${token}`), * onFinal: async () => data.close() * }; * const transformer = createCallbacksTransformer(callbacks); */ export function createCallbacksTransformer( callbacks: StreamCallbacks | undefined = {}, ): TransformStream<string, string> { let aggregatedResponse = ''; return new TransformStream({ async start(): Promise<void> { if (callbacks.onStart) await callbacks.onStart(); }, async transform(message, controller): Promise<void> { controller.enqueue(message); aggregatedResponse += message; if (callbacks.onToken) await callbacks.onToken(message); if (callbacks.onText && typeof message === 'string') { await callbacks.onText(message); } }, async flush(): Promise<void> { if (callbacks.onFinal) { await callbacks.onFinal(aggregatedResponse); } }, }); } --- File: /ai/packages/llamaindex/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/llamaindex/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts{,x}'], exclude: [ '**/*.ui.test.ts{,x}', '**/*.e2e.test.ts{,x}', '**/node_modules/**', ], typecheck: { enabled: true, }, }, }); --- File: /ai/packages/llamaindex/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts{,x}'], exclude: [ '**/*.ui.test.ts{,x}', '**/*.e2e.test.ts{,x}', '**/node_modules/**', ], typecheck: { enabled: true, }, }, }); --- File: /ai/packages/lmnt/src/index.ts --- export { createLMNT, lmnt } from './lmnt-provider'; export type { LMNTProvider, LMNTProviderSettings } from './lmnt-provider'; --- File: /ai/packages/lmnt/src/lmnt-api-types.ts --- export type LMNTSpeechAPITypes = { /** The voice id of the voice to use; voice ids can be retrieved by calls to List voices or Voice info. */ voice: string; /** The text to synthesize; max 5000 characters per request (including spaces) */ text: string; /** The model to use for synthesis. One of aurora (default) or blizzard. */ model?: 'aurora' | 'blizzard'; /** The desired language. Two letter ISO 639-1 code. Does not work with professional clones. Not all languages work with all models. Defaults to auto language detection. */ language?: | 'auto' | 'en' | 'es' | 'pt' | 'fr' | 'de' | 'zh' | 'ko' | 'hi' | 'ja' | 'ru' | 'it' | 'tr'; /** The file format of the audio output */ format?: 'aac' | 'mp3' | 'mulaw' | 'raw' | 'wav'; /** The desired output sample rate in Hz */ sample_rate?: 8000 | 16000 | 24000; /** The talking speed of the generated speech, a floating point value between 0.25 (slow) and 2.0 (fast). */ speed?: number; /** Seed used to specify a different take; defaults to random */ seed?: number; /** Set this to true to generate conversational-style speech rather than reading-style speech. Does not work with the blizzard model. */ conversational?: boolean; /** Produce speech of this length in seconds; maximum 300.0 (5 minutes). Does not work with the blizzard model. */ length?: number; /** Controls the stability of the generated speech. A lower value (like 0.3) produces more consistent, reliable speech. A higher value (like 0.9) gives more flexibility in how words are spoken, but might occasionally produce unusual intonations or speech patterns. */ top_p?: number; /** Influences how expressive and emotionally varied the speech becomes. Lower values (like 0.3) create more neutral, consistent speaking styles. Higher values (like 1.0) allow for more dynamic emotional range and speaking styles. */ temperature?: number; }; --- File: /ai/packages/lmnt/src/lmnt-config.ts --- import { FetchFunction } from '@ai-sdk/provider-utils'; export type LMNTConfig = { provider: string; url: (options: { modelId: string; path: string }) => string; headers: () => Record<string, string | undefined>; fetch?: FetchFunction; generateId?: () => string; }; --- File: /ai/packages/lmnt/src/lmnt-error.test.ts --- import { safeParseJSON } from '@ai-sdk/provider-utils'; import { lmntErrorDataSchema } from './lmnt-error'; describe('lmntErrorDataSchema', () => { it('should parse LMNT resource exhausted error', async () => { const error = ` {"error":{"message":"{\\n \\"error\\": {\\n \\"code\\": 429,\\n \\"message\\": \\"Resource has been exhausted (e.g. check quota).\\",\\n \\"status\\": \\"RESOURCE_EXHAUSTED\\"\\n }\\n}\\n","code":429}} `; const result = await safeParseJSON({ text: error, schema: lmntErrorDataSchema, }); expect(result).toStrictEqual({ success: true, value: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, rawValue: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, }); }); }); --- File: /ai/packages/lmnt/src/lmnt-error.ts --- import { z } from 'zod/v4'; import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; export const lmntErrorDataSchema = z.object({ error: z.object({ message: z.string(), code: z.number(), }), }); export type LMNTErrorData = z.infer<typeof lmntErrorDataSchema>; export const lmntFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: lmntErrorDataSchema, errorToMessage: data => data.error.message, }); --- File: /ai/packages/lmnt/src/lmnt-provider.ts --- import { SpeechModelV2, ProviderV2 } from '@ai-sdk/provider'; import { FetchFunction, loadApiKey } from '@ai-sdk/provider-utils'; import { LMNTSpeechModel } from './lmnt-speech-model'; import { LMNTSpeechModelId } from './lmnt-speech-options'; export interface LMNTProvider extends Pick<ProviderV2, 'speechModel'> { ( modelId: 'aurora', settings?: {}, ): { speech: LMNTSpeechModel; }; /** Creates a model for speech synthesis. */ speech(modelId: LMNTSpeechModelId): SpeechModelV2; } export interface LMNTProviderSettings { /** API key for authenticating requests. */ apiKey?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } /** Create an LMNT provider instance. */ export function createLMNT(options: LMNTProviderSettings = {}): LMNTProvider { const getHeaders = () => ({ 'x-api-key': loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'LMNT_API_KEY', description: 'LMNT', }), ...options.headers, }); const createSpeechModel = (modelId: LMNTSpeechModelId) => new LMNTSpeechModel(modelId, { provider: `lmnt.speech`, url: ({ path }) => `https://api.lmnt.com${path}`, headers: getHeaders, fetch: options.fetch, }); const provider = function (modelId: LMNTSpeechModelId) { return { speech: createSpeechModel(modelId), }; }; provider.speech = createSpeechModel; provider.speechModel = createSpeechModel; return provider as LMNTProvider; } /** Default LMNT provider instance. */ export const lmnt = createLMNT(); --- File: /ai/packages/lmnt/src/lmnt-speech-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { LMNTSpeechModel } from './lmnt-speech-model'; import { createLMNT } from './lmnt-provider'; const provider = createLMNT({ apiKey: 'test-api-key' }); const model = provider.speech('aurora'); const server = createTestServer({ 'https://api.lmnt.com/v1/ai/speech/bytes': {}, }); describe('doGenerate', () => { function prepareAudioResponse({ headers, format = 'mp3', }: { headers?: Record<string, string>; format?: 'aac' | 'mp3' | 'mulaw' | 'raw' | 'wav'; } = {}) { const audioBuffer = new Uint8Array(100); // Mock audio data server.urls['https://api.lmnt.com/v1/ai/speech/bytes'].response = { type: 'binary', headers: { 'content-type': `audio/${format}`, ...headers, }, body: Buffer.from(audioBuffer), }; return audioBuffer; } it('should pass the model and text', async () => { prepareAudioResponse(); await model.doGenerate({ text: 'Hello from the AI SDK!', }); expect(await server.calls[0].requestBodyJson).toMatchObject({ model: 'aurora', text: 'Hello from the AI SDK!', }); }); it('should pass headers', async () => { prepareAudioResponse(); const provider = createLMNT({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.speech('aurora').doGenerate({ text: 'Hello from the AI SDK!', headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toMatchObject({ 'x-api-key': 'test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should pass options', async () => { prepareAudioResponse(); await model.doGenerate({ text: 'Hello from the AI SDK!', voice: 'nova', outputFormat: 'mp3', speed: 1.5, }); expect(await server.calls[0].requestBodyJson).toMatchObject({ model: 'aurora', text: 'Hello from the AI SDK!', voice: 'nova', speed: 1.5, response_format: 'mp3', }); }); it('should return audio data with correct content type', async () => { const audio = new Uint8Array(100); // Mock audio data prepareAudioResponse({ format: 'mp3', headers: { 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); const result = await model.doGenerate({ text: 'Hello from the AI SDK!', outputFormat: 'mp3', }); expect(result.audio).toStrictEqual(audio); }); it('should include response data with timestamp, modelId and headers', async () => { prepareAudioResponse({ headers: { 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); const testDate = new Date(0); const customModel = new LMNTSpeechModel('aurora', { provider: 'test-provider', url: () => 'https://api.lmnt.com/v1/ai/speech/bytes', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ text: 'Hello from the AI SDK!', }); expect(result.response).toMatchObject({ timestamp: testDate, modelId: 'aurora', headers: { 'content-type': 'audio/mp3', 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); }); it('should use real date when no custom date provider is specified', async () => { prepareAudioResponse(); const testDate = new Date(0); const customModel = new LMNTSpeechModel('aurora', { provider: 'test-provider', url: () => 'https://api.lmnt.com/v1/ai/speech/bytes', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ text: 'Hello from the AI SDK!', }); expect(result.response.timestamp.getTime()).toEqual(testDate.getTime()); expect(result.response.modelId).toBe('aurora'); }); it('should handle different audio formats', async () => { const formats = ['aac', 'mp3', 'mulaw', 'raw', 'wav'] as const; for (const format of formats) { const audio = prepareAudioResponse({ format }); const result = await model.doGenerate({ text: 'Hello from the AI SDK!', providerOptions: { lmnt: { format, }, }, }); expect(result.audio).toStrictEqual(audio); } }); it('should include warnings if any are generated', async () => { prepareAudioResponse(); const result = await model.doGenerate({ text: 'Hello from the AI SDK!', }); expect(result.warnings).toEqual([]); }); }); --- File: /ai/packages/lmnt/src/lmnt-speech-model.ts --- import { SpeechModelV2, SpeechModelV2CallWarning } from '@ai-sdk/provider'; import { combineHeaders, createBinaryResponseHandler, parseProviderOptions, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { LMNTConfig } from './lmnt-config'; import { lmntFailedResponseHandler } from './lmnt-error'; import { LMNTSpeechModelId } from './lmnt-speech-options'; import { LMNTSpeechAPITypes } from './lmnt-api-types'; // https://docs.lmnt.com/api-reference/speech/synthesize-speech-bytes const lmntSpeechCallOptionsSchema = z.object({ /** * The model to use for speech synthesis e.g. 'aurora' or 'blizzard'. * @default 'aurora' */ model: z .union([z.enum(['aurora', 'blizzard']), z.string()]) .nullish() .default('aurora'), /** * The audio format of the output. * @default 'mp3' */ format: z .enum(['aac', 'mp3', 'mulaw', 'raw', 'wav']) .nullish() .default('mp3'), /** * The sample rate of the output audio in Hz. * @default 24000 */ sampleRate: z .union([z.literal(8000), z.literal(16000), z.literal(24000)]) .nullish() .default(24000), /** * The speed of the speech. Range: 0.25 to 2. * @default 1 */ speed: z.number().min(0.25).max(2).nullish().default(1), /** * A seed value for deterministic generation. */ seed: z.number().int().nullish(), /** * Whether to use a conversational style. * @default false */ conversational: z.boolean().nullish().default(false), /** * Maximum length of the output in seconds (up to 300). */ length: z.number().max(300).nullish(), /** * Top-p sampling parameter. Range: 0 to 1. * @default 1 */ topP: z.number().min(0).max(1).nullish().default(1), /** * Temperature for sampling. Higher values increase randomness. * @default 1 */ temperature: z.number().min(0).nullish().default(1), }); export type LMNTSpeechCallOptions = z.infer<typeof lmntSpeechCallOptionsSchema>; interface LMNTSpeechModelConfig extends LMNTConfig { _internal?: { currentDate?: () => Date; }; } export class LMNTSpeechModel implements SpeechModelV2 { readonly specificationVersion = 'v2'; get provider(): string { return this.config.provider; } constructor( readonly modelId: LMNTSpeechModelId, private readonly config: LMNTSpeechModelConfig, ) {} private async getArgs({ text, voice = 'ava', outputFormat = 'mp3', speed, language, providerOptions, }: Parameters<SpeechModelV2['doGenerate']>[0]) { const warnings: SpeechModelV2CallWarning[] = []; // Parse provider options const lmntOptions = await parseProviderOptions({ provider: 'lmnt', providerOptions, schema: lmntSpeechCallOptionsSchema, }); // Create request body const requestBody: Record<string, unknown> = { model: this.modelId, text, voice, response_format: 'mp3', speed, }; if (outputFormat) { if (['mp3', 'aac', 'mulaw', 'raw', 'wav'].includes(outputFormat)) { requestBody.response_format = outputFormat; } else { warnings.push({ type: 'unsupported-setting', setting: 'outputFormat', details: `Unsupported output format: ${outputFormat}. Using mp3 instead.`, }); } } // Add provider-specific options if (lmntOptions) { const speechModelOptions: Omit<LMNTSpeechAPITypes, 'voice' | 'text'> = { conversational: lmntOptions.conversational ?? undefined, length: lmntOptions.length ?? undefined, seed: lmntOptions.seed ?? undefined, speed: lmntOptions.speed ?? undefined, temperature: lmntOptions.temperature ?? undefined, top_p: lmntOptions.topP ?? undefined, sample_rate: lmntOptions.sampleRate ?? undefined, }; for (const key in speechModelOptions) { const value = speechModelOptions[ key as keyof Omit<LMNTSpeechAPITypes, 'voice' | 'text'> ]; if (value !== undefined) { requestBody[key] = value; } } } if (language) { requestBody.language = language; } return { requestBody, warnings, }; } async doGenerate( options: Parameters<SpeechModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<SpeechModelV2['doGenerate']>>> { const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { requestBody, warnings } = await this.getArgs(options); const { value: audio, responseHeaders, rawValue: rawResponse, } = await postJsonToApi({ url: this.config.url({ path: '/v1/ai/speech/bytes', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body: requestBody, failedResponseHandler: lmntFailedResponseHandler, successfulResponseHandler: createBinaryResponseHandler(), abortSignal: options.abortSignal, fetch: this.config.fetch, }); return { audio, warnings, request: { body: JSON.stringify(requestBody), }, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, body: rawResponse, }, }; } } --- File: /ai/packages/lmnt/src/lmnt-speech-options.ts --- export type LMNTSpeechModelId = 'aurora' | 'blizzard' | (string & {}); --- File: /ai/packages/lmnt/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/lmnt/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/lmnt/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/luma/src/index.ts --- export { createLuma, luma } from './luma-provider'; export type { LumaProvider, LumaProviderSettings } from './luma-provider'; export type { LumaErrorData } from './luma-image-model'; --- File: /ai/packages/luma/src/luma-image-model.test.ts --- import { FetchFunction } from '@ai-sdk/provider-utils'; import { createTestServer } from '@ai-sdk/provider-utils/test'; import { describe, expect, it } from 'vitest'; import { LumaImageModel } from './luma-image-model'; import { InvalidResponseDataError } from '@ai-sdk/provider'; const prompt = 'A cute baby sea otter'; function createBasicModel({ headers, fetch, currentDate, }: { headers?: () => Record<string, string>; fetch?: FetchFunction; currentDate?: () => Date; } = {}) { return new LumaImageModel('test-model', { provider: 'luma', baseURL: 'https://api.example.com', headers: headers ?? (() => ({ 'api-key': 'test-key' })), fetch, _internal: { currentDate, }, }); } describe('LumaImageModel', () => { const server = createTestServer({ 'https://api.example.com/dream-machine/v1/generations/image': { response: { type: 'json-value', body: { id: 'test-generation-id', generation_type: 'image', state: 'queued', created_at: '2024-01-01T00:00:00Z', model: 'test-model', request: { generation_type: 'image', model: 'test-model', prompt: 'A cute baby sea otter', }, }, }, }, 'https://api.example.com/dream-machine/v1/generations/test-generation-id': { response: { type: 'json-value', body: { id: 'test-generation-id', generation_type: 'image', state: 'completed', created_at: '2024-01-01T00:00:00Z', assets: { image: 'https://api.example.com/image.png', }, model: 'test-model', request: { generation_type: 'image', model: 'test-model', prompt: 'A cute baby sea otter', }, }, }, }, 'https://api.example.com/image.png': { response: { type: 'binary', body: Buffer.from('test-binary-content'), }, }, }); describe('doGenerate', () => { it('should pass the correct parameters including aspect ratio', async () => { const model = createBasicModel(); await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: '16:9', seed: undefined, providerOptions: { luma: { additional_param: 'value' } }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ prompt, aspect_ratio: '16:9', model: 'test-model', additional_param: 'value', }); }); it('should call the correct urls in sequence', async () => { const model = createBasicModel(); await model.doGenerate({ prompt, n: 1, aspectRatio: '16:9', providerOptions: {}, size: undefined, seed: undefined, }); expect(server.calls[0].requestMethod).toBe('POST'); expect(server.calls[0].requestUrl).toBe( 'https://api.example.com/dream-machine/v1/generations/image', ); expect(server.calls[1].requestMethod).toBe('GET'); expect(server.calls[1].requestUrl).toBe( 'https://api.example.com/dream-machine/v1/generations/test-generation-id', ); expect(server.calls[2].requestMethod).toBe('GET'); expect(server.calls[2].requestUrl).toBe( 'https://api.example.com/image.png', ); }); it('should pass headers', async () => { const modelWithHeaders = createBasicModel({ headers: () => ({ 'Custom-Provider-Header': 'provider-header-value', }), }); await modelWithHeaders.doGenerate({ prompt, n: 1, providerOptions: {}, headers: { 'Custom-Request-Header': 'request-header-value', }, size: undefined, seed: undefined, aspectRatio: undefined, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should not pass providerOptions.{pollIntervalMillis,maxPollAttempts}', async () => { const model = createBasicModel(); await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: '16:9', seed: undefined, providerOptions: { luma: { pollIntervalMillis: 1000, maxPollAttempts: 5, additional_param: 'value', }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ prompt, aspect_ratio: '16:9', model: 'test-model', additional_param: 'value', }); }); it('should handle API errors', async () => { server.urls[ 'https://api.example.com/dream-machine/v1/generations/image' ].response = { type: 'error', status: 400, body: 'Bad Request', }; const model = createBasicModel(); await expect( model.doGenerate({ prompt, n: 1, providerOptions: {}, size: undefined, seed: undefined, aspectRatio: undefined, }), ).rejects.toMatchObject({ message: 'Bad Request', statusCode: 400, url: 'https://api.example.com/dream-machine/v1/generations/image', requestBodyValues: { prompt: 'A cute baby sea otter', }, responseBody: 'Bad Request', }); }); it('should handle failed generation state', async () => { server.urls[ 'https://api.example.com/dream-machine/v1/generations/test-generation-id' ].response = { type: 'json-value', body: { id: 'test-generation-id', generation_type: 'image', state: 'failed', failure_reason: 'Generation failed', created_at: '2024-01-01T00:00:00Z', model: 'test-model', request: { generation_type: 'image', model: 'test-model', prompt: 'A cute baby sea otter', }, }, }; const model = createBasicModel(); await expect( model.doGenerate({ prompt, n: 1, providerOptions: {}, size: undefined, seed: undefined, aspectRatio: undefined, }), ).rejects.toThrow(InvalidResponseDataError); }); describe('warnings', () => { it('should return warnings for unsupported parameters', async () => { const model = createBasicModel(); const result = await model.doGenerate({ prompt, n: 1, size: '1024x1024', seed: 123, providerOptions: {}, aspectRatio: undefined, }); expect(result.warnings).toContainEqual({ type: 'unsupported-setting', setting: 'seed', details: 'This model does not support the `seed` option.', }); expect(result.warnings).toContainEqual({ type: 'unsupported-setting', setting: 'size', details: 'This model does not support the `size` option. Use `aspectRatio` instead.', }); }); }); describe('response metadata', () => { it('should include timestamp, headers and modelId in response', async () => { const testDate = new Date('2024-01-01T00:00:00Z'); const model = createBasicModel({ currentDate: () => testDate, }); const result = await model.doGenerate({ prompt, n: 1, providerOptions: {}, size: undefined, seed: undefined, aspectRatio: undefined, }); expect(result.response).toStrictEqual({ timestamp: testDate, modelId: 'test-model', headers: expect.any(Object), }); }); }); }); describe('constructor', () => { it('should expose correct provider and model information', () => { const model = createBasicModel(); expect(model.provider).toBe('luma'); expect(model.modelId).toBe('test-model'); expect(model.specificationVersion).toBe('v2'); expect(model.maxImagesPerCall).toBe(1); }); }); describe('response schema validation', () => { it('should parse response with image references', async () => { server.urls[ 'https://api.example.com/dream-machine/v1/generations/test-generation-id' ].response = { type: 'json-value', body: { id: 'test-generation-id', generation_type: 'image', state: 'completed', created_at: '2024-01-01T00:00:00Z', assets: { image: 'https://api.example.com/image.png', }, model: 'test-model', request: { generation_type: 'image', model: 'test-model', prompt: 'A cute baby sea otter', image_ref: [ { url: 'https://example.com/ref1.jpg', weight: 0.85, }, ], }, }, }; const model = createBasicModel(); const result = await model.doGenerate({ prompt, n: 1, providerOptions: {}, size: undefined, seed: undefined, aspectRatio: undefined, }); // If schema validation fails, this won't get reached expect(result.images).toBeDefined(); }); it('should parse response with style references', async () => { server.urls[ 'https://api.example.com/dream-machine/v1/generations/test-generation-id' ].response = { type: 'json-value', body: { id: 'test-generation-id', generation_type: 'image', state: 'completed', created_at: '2024-01-01T00:00:00Z', assets: { image: 'https://api.example.com/image.png', }, model: 'test-model', request: { generation_type: 'image', model: 'test-model', prompt: 'A cute baby sea otter', style_ref: [ { url: 'https://example.com/style1.jpg', weight: 0.8, }, ], }, }, }; const model = createBasicModel(); const result = await model.doGenerate({ prompt, n: 1, providerOptions: {}, size: undefined, seed: undefined, aspectRatio: undefined, }); expect(result.images).toBeDefined(); }); it('should parse response with character references', async () => { server.urls[ 'https://api.example.com/dream-machine/v1/generations/test-generation-id' ].response = { type: 'json-value', body: { id: 'test-generation-id', generation_type: 'image', state: 'completed', created_at: '2024-01-01T00:00:00Z', assets: { image: 'https://api.example.com/image.png', }, model: 'test-model', request: { generation_type: 'image', model: 'test-model', prompt: 'A cute baby sea otter', character_ref: { identity0: { images: ['https://example.com/character1.jpg'], }, }, }, }, }; const model = createBasicModel(); const result = await model.doGenerate({ prompt, n: 1, providerOptions: {}, size: undefined, seed: undefined, aspectRatio: undefined, }); expect(result.images).toBeDefined(); }); it('should parse response with modify image reference', async () => { server.urls[ 'https://api.example.com/dream-machine/v1/generations/test-generation-id' ].response = { type: 'json-value', body: { id: 'test-generation-id', generation_type: 'image', state: 'completed', created_at: '2024-01-01T00:00:00Z', assets: { image: 'https://api.example.com/image.png', }, model: 'test-model', request: { generation_type: 'image', model: 'test-model', prompt: 'A cute baby sea otter', modify_image_ref: { url: 'https://example.com/modify.jpg', weight: 1.0, }, }, }, }; const model = createBasicModel(); const result = await model.doGenerate({ prompt, n: 1, providerOptions: {}, size: undefined, seed: undefined, aspectRatio: undefined, }); expect(result.images).toBeDefined(); }); it('should parse response with multiple reference types', async () => { server.urls[ 'https://api.example.com/dream-machine/v1/generations/test-generation-id' ].response = { type: 'json-value', body: { id: 'test-generation-id', generation_type: 'image', state: 'completed', created_at: '2024-01-01T00:00:00Z', assets: { image: 'https://api.example.com/image.png', }, model: 'test-model', request: { generation_type: 'image', model: 'test-model', prompt: 'A cute baby sea otter', image_ref: [ { url: 'https://example.com/ref1.jpg', weight: 0.85, }, ], style_ref: [ { url: 'https://example.com/style1.jpg', weight: 0.8, }, ], character_ref: { identity0: { images: ['https://example.com/character1.jpg'], }, }, modify_image_ref: { url: 'https://example.com/modify.jpg', weight: 1.0, }, }, }, }; const model = createBasicModel(); const result = await model.doGenerate({ prompt, n: 1, providerOptions: {}, size: undefined, seed: undefined, aspectRatio: undefined, }); expect(result.images).toBeDefined(); }); }); }); --- File: /ai/packages/luma/src/luma-image-model.ts --- import { ImageModelV2, ImageModelV2CallWarning, InvalidResponseDataError, } from '@ai-sdk/provider'; import { FetchFunction, combineHeaders, createBinaryResponseHandler, createJsonResponseHandler, createJsonErrorResponseHandler, createStatusCodeErrorResponseHandler, delay, getFromApi, postJsonToApi, } from '@ai-sdk/provider-utils'; import { LumaImageSettings } from './luma-image-settings'; import { z } from 'zod/v4'; const DEFAULT_POLL_INTERVAL_MILLIS = 500; const DEFAULT_MAX_POLL_ATTEMPTS = 60000 / DEFAULT_POLL_INTERVAL_MILLIS; interface LumaImageModelConfig { provider: string; baseURL: string; headers: () => Record<string, string>; fetch?: FetchFunction; _internal?: { currentDate?: () => Date; }; } export class LumaImageModel implements ImageModelV2 { readonly specificationVersion = 'v2'; readonly maxImagesPerCall = 1; readonly pollIntervalMillis = DEFAULT_POLL_INTERVAL_MILLIS; readonly maxPollAttempts = DEFAULT_MAX_POLL_ATTEMPTS; get provider(): string { return this.config.provider; } constructor( readonly modelId: string, private readonly config: LumaImageModelConfig, ) {} async doGenerate({ prompt, n, size, aspectRatio, seed, providerOptions, headers, abortSignal, }: Parameters<ImageModelV2['doGenerate']>[0]): Promise< Awaited<ReturnType<ImageModelV2['doGenerate']>> > { const warnings: Array<ImageModelV2CallWarning> = []; if (seed != null) { warnings.push({ type: 'unsupported-setting', setting: 'seed', details: 'This model does not support the `seed` option.', }); } if (size != null) { warnings.push({ type: 'unsupported-setting', setting: 'size', details: 'This model does not support the `size` option. Use `aspectRatio` instead.', }); } // remove non-request options from providerOptions const { pollIntervalMillis, maxPollAttempts, ...providerRequestOptions } = providerOptions.luma ?? {}; const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const fullHeaders = combineHeaders(this.config.headers(), headers); const { value: generationResponse, responseHeaders } = await postJsonToApi({ url: this.getLumaGenerationsUrl(), headers: fullHeaders, body: { prompt, ...(aspectRatio ? { aspect_ratio: aspectRatio } : {}), model: this.modelId, ...providerRequestOptions, }, abortSignal, fetch: this.config.fetch, failedResponseHandler: this.createLumaErrorHandler(), successfulResponseHandler: createJsonResponseHandler( lumaGenerationResponseSchema, ), }); const imageUrl = await this.pollForImageUrl( generationResponse.id, fullHeaders, abortSignal, providerOptions.luma, ); const downloadedImage = await this.downloadImage(imageUrl, abortSignal); return { images: [downloadedImage], warnings, response: { modelId: this.modelId, timestamp: currentDate, headers: responseHeaders, }, }; } private async pollForImageUrl( generationId: string, headers: Record<string, string | undefined>, abortSignal: AbortSignal | undefined, imageSettings?: LumaImageSettings, ): Promise<string> { const url = this.getLumaGenerationsUrl(generationId); const maxPollAttempts = imageSettings?.maxPollAttempts ?? this.maxPollAttempts; const pollIntervalMillis = imageSettings?.pollIntervalMillis ?? this.pollIntervalMillis; for (let i = 0; i < maxPollAttempts; i++) { const { value: statusResponse } = await getFromApi({ url, headers, abortSignal, fetch: this.config.fetch, failedResponseHandler: this.createLumaErrorHandler(), successfulResponseHandler: createJsonResponseHandler( lumaGenerationResponseSchema, ), }); switch (statusResponse.state) { case 'completed': if (!statusResponse.assets?.image) { throw new InvalidResponseDataError({ data: statusResponse, message: `Image generation completed but no image was found.`, }); } return statusResponse.assets.image; case 'failed': throw new InvalidResponseDataError({ data: statusResponse, message: `Image generation failed.`, }); } await delay(pollIntervalMillis); } throw new Error( `Image generation timed out after ${this.maxPollAttempts} attempts.`, ); } private createLumaErrorHandler() { return createJsonErrorResponseHandler({ errorSchema: lumaErrorSchema, errorToMessage: (error: LumaErrorData) => error.detail[0].msg ?? 'Unknown error', }); } private getLumaGenerationsUrl(generationId?: string) { return `${this.config.baseURL}/dream-machine/v1/generations/${ generationId ?? 'image' }`; } private async downloadImage( url: string, abortSignal: AbortSignal | undefined, ): Promise<Uint8Array> { const { value: response } = await getFromApi({ url, // No specific headers should be needed for this request as it's a // generated image provided by Luma. abortSignal, failedResponseHandler: createStatusCodeErrorResponseHandler(), successfulResponseHandler: createBinaryResponseHandler(), fetch: this.config.fetch, }); return response; } } // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const lumaGenerationResponseSchema = z.object({ id: z.string(), state: z.enum(['queued', 'dreaming', 'completed', 'failed']), failure_reason: z.string().nullish(), assets: z .object({ image: z.string(), // URL of the generated image }) .nullish(), }); const lumaErrorSchema = z.object({ detail: z.array( z.object({ type: z.string(), loc: z.array(z.string()), msg: z.string(), input: z.string(), ctx: z .object({ expected: z.string(), }) .nullish(), }), ), }); export type LumaErrorData = z.infer<typeof lumaErrorSchema>; --- File: /ai/packages/luma/src/luma-image-settings.ts --- // https://luma.ai/models?type=image export type LumaImageModelId = 'photon-1' | 'photon-flash-1' | (string & {}); /** Configuration settings for Luma image generation. Since the Luma API processes images through an asynchronous queue system, these settings allow you to tune the polling behavior when waiting for image generation to complete. */ export interface LumaImageSettings { /** Override the polling interval in milliseconds (default 500). This controls how frequently the API is checked for completed images while they are being processed in Luma's queue. */ pollIntervalMillis?: number; /** Override the maximum number of polling attempts (default 120). Since image generation is queued and processed asynchronously, this limits how long to wait for results before timing out. */ maxPollAttempts?: number; } --- File: /ai/packages/luma/src/luma-provider.test.ts --- import { describe, it, expect, beforeEach, vi } from 'vitest'; import { createLuma } from './luma-provider'; import { LumaImageModel } from './luma-image-model'; vi.mock('./luma-image-model', () => ({ LumaImageModel: vi.fn(), })); describe('createLuma', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('image', () => { it('should construct an image model with default configuration', () => { const provider = createLuma(); const modelId = 'luma-v1'; const model = provider.image(modelId); expect(model).toBeInstanceOf(LumaImageModel); expect(LumaImageModel).toHaveBeenCalledWith( modelId, expect.objectContaining({ provider: 'luma.image', baseURL: 'https://api.lumalabs.ai', }), ); }); it('should respect custom configuration options', () => { const customBaseURL = 'https://custom-api.lumalabs.ai'; const customHeaders = { 'X-Custom-Header': 'value' }; const mockFetch = vi.fn(); const provider = createLuma({ apiKey: 'custom-api-key', baseURL: customBaseURL, headers: customHeaders, fetch: mockFetch, }); const modelId = 'luma-v1'; provider.image(modelId); expect(LumaImageModel).toHaveBeenCalledWith( modelId, expect.objectContaining({ baseURL: customBaseURL, headers: expect.any(Function), fetch: mockFetch, provider: 'luma.image', }), ); }); }); }); --- File: /ai/packages/luma/src/luma-provider.ts --- import { ImageModelV2, NoSuchModelError, ProviderV2 } from '@ai-sdk/provider'; import { FetchFunction, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { LumaImageModel } from './luma-image-model'; import { LumaImageModelId } from './luma-image-settings'; export interface LumaProviderSettings { /** Luma API key. Default value is taken from the `LUMA_API_KEY` environment variable. */ apiKey?: string; /** Base URL for the API calls. */ baseURL?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } export interface LumaProvider extends ProviderV2 { /** Creates a model for image generation. */ image(modelId: LumaImageModelId): ImageModelV2; /** Creates a model for image generation. */ imageModel(modelId: LumaImageModelId): ImageModelV2; } const defaultBaseURL = 'https://api.lumalabs.ai'; export function createLuma(options: LumaProviderSettings = {}): LumaProvider { const baseURL = withoutTrailingSlash(options.baseURL ?? defaultBaseURL); const getHeaders = () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'LUMA_API_KEY', description: 'Luma', })}`, ...options.headers, }); const createImageModel = (modelId: LumaImageModelId) => new LumaImageModel(modelId, { provider: 'luma.image', baseURL: baseURL ?? defaultBaseURL, headers: getHeaders, fetch: options.fetch, }); return { image: createImageModel, imageModel: createImageModel, languageModel: () => { throw new NoSuchModelError({ modelId: 'languageModel', modelType: 'languageModel', }); }, textEmbeddingModel: () => { throw new NoSuchModelError({ modelId: 'textEmbeddingModel', modelType: 'textEmbeddingModel', }); }, }; } export const luma = createLuma(); --- File: /ai/packages/luma/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/luma/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/luma/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/mistral/src/convert-to-mistral-chat-messages.test.ts --- import { convertToMistralChatMessages } from './convert-to-mistral-chat-messages'; describe('user messages', () => { it('should convert messages with image parts', async () => { const result = convertToMistralChatMessages([ { role: 'user', content: [ { type: 'text', text: 'Hello' }, { type: 'file', data: 'AAECAw==', mediaType: 'image/png', }, ], }, ]); expect(result).toMatchSnapshot(); }); it('should convert messages with PDF file parts using URL', () => { const result = convertToMistralChatMessages([ { role: 'user', content: [ { type: 'text', text: 'Please analyze this document' }, { type: 'file', data: new URL('https://example.com/document.pdf'), mediaType: 'application/pdf', }, ], }, ]); expect(result).toMatchSnapshot(); }); }); describe('tool calls', () => { it('should stringify arguments to tool calls', () => { const result = convertToMistralChatMessages([ { role: 'assistant', content: [ { type: 'tool-call', input: { key: 'arg-value' }, toolCallId: 'tool-call-id-1', toolName: 'tool-1', }, ], }, { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'tool-call-id-1', toolName: 'tool-1', output: { type: 'json', value: { key: 'result-value' } }, }, ], }, ]); expect(result).toMatchInlineSnapshot(` [ { "content": "", "prefix": undefined, "role": "assistant", "tool_calls": [ { "function": { "arguments": "{"key":"arg-value"}", "name": "tool-1", }, "id": "tool-call-id-1", "type": "function", }, ], }, { "content": "{"key":"result-value"}", "name": "tool-1", "role": "tool", "tool_call_id": "tool-call-id-1", }, ] `); }); it('should handle text output format', () => { const result = convertToMistralChatMessages([ { role: 'assistant', content: [ { type: 'tool-call', input: { query: 'test' }, toolCallId: 'tool-call-id-2', toolName: 'text-tool', }, ], }, { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'tool-call-id-2', toolName: 'text-tool', output: { type: 'text', value: 'This is a text response' }, }, ], }, ]); expect(result).toMatchInlineSnapshot(` [ { "content": "", "prefix": undefined, "role": "assistant", "tool_calls": [ { "function": { "arguments": "{"query":"test"}", "name": "text-tool", }, "id": "tool-call-id-2", "type": "function", }, ], }, { "content": "This is a text response", "name": "text-tool", "role": "tool", "tool_call_id": "tool-call-id-2", }, ] `); }); it('should handle content output format', () => { const result = convertToMistralChatMessages([ { role: 'assistant', content: [ { type: 'tool-call', input: { query: 'generate image' }, toolCallId: 'tool-call-id-3', toolName: 'image-tool', }, ], }, { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'tool-call-id-3', toolName: 'image-tool', output: { type: 'content', value: [ { type: 'text', text: 'Here is the result:' }, { type: 'media', data: 'base64data', mediaType: 'image/png' }, ], }, }, ], }, ]); expect(result).toMatchInlineSnapshot(` [ { "content": "", "prefix": undefined, "role": "assistant", "tool_calls": [ { "function": { "arguments": "{"query":"generate image"}", "name": "image-tool", }, "id": "tool-call-id-3", "type": "function", }, ], }, { "content": "[{"type":"text","text":"Here is the result:"},{"type":"media","data":"base64data","mediaType":"image/png"}]", "name": "image-tool", "role": "tool", "tool_call_id": "tool-call-id-3", }, ] `); }); it('should handle error output format', () => { const result = convertToMistralChatMessages([ { role: 'assistant', content: [ { type: 'tool-call', input: { query: 'test' }, toolCallId: 'tool-call-id-4', toolName: 'error-tool', }, ], }, { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'tool-call-id-4', toolName: 'error-tool', output: { type: 'error-text', value: 'Invalid input provided' }, }, ], }, ]); expect(result).toMatchInlineSnapshot(` [ { "content": "", "prefix": undefined, "role": "assistant", "tool_calls": [ { "function": { "arguments": "{"query":"test"}", "name": "error-tool", }, "id": "tool-call-id-4", "type": "function", }, ], }, { "content": "Invalid input provided", "name": "error-tool", "role": "tool", "tool_call_id": "tool-call-id-4", }, ] `); }); }); describe('assistant messages', () => { it('should add prefix true to trailing assistant messages', () => { const result = convertToMistralChatMessages([ { role: 'user', content: [{ type: 'text', text: 'Hello' }], }, { role: 'assistant', content: [{ type: 'text', text: 'Hello!' }], }, ]); expect(result).toMatchSnapshot(); }); }); --- File: /ai/packages/mistral/src/convert-to-mistral-chat-messages.ts --- import { LanguageModelV2Prompt, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { MistralPrompt } from './mistral-chat-prompt'; export function convertToMistralChatMessages( prompt: LanguageModelV2Prompt, ): MistralPrompt { const messages: MistralPrompt = []; for (let i = 0; i < prompt.length; i++) { const { role, content } = prompt[i]; const isLastMessage = i === prompt.length - 1; switch (role) { case 'system': { messages.push({ role: 'system', content }); break; } case 'user': { messages.push({ role: 'user', content: content.map(part => { switch (part.type) { case 'text': { return { type: 'text', text: part.text }; } case 'file': { if (part.mediaType.startsWith('image/')) { const mediaType = part.mediaType === 'image/*' ? 'image/jpeg' : part.mediaType; return { type: 'image_url', image_url: part.data instanceof URL ? part.data.toString() : `data:${mediaType};base64,${part.data}`, }; } else if (part.mediaType === 'application/pdf') { return { type: 'document_url', document_url: part.data.toString(), }; } else { throw new UnsupportedFunctionalityError({ functionality: 'Only images and PDF file parts are supported', }); } } } }), }); break; } case 'assistant': { let text = ''; const toolCalls: Array<{ id: string; type: 'function'; function: { name: string; arguments: string }; }> = []; for (const part of content) { switch (part.type) { case 'text': { text += part.text; break; } case 'tool-call': { toolCalls.push({ id: part.toolCallId, type: 'function', function: { name: part.toolName, arguments: JSON.stringify(part.input), }, }); break; } } } messages.push({ role: 'assistant', content: text, prefix: isLastMessage ? true : undefined, tool_calls: toolCalls.length > 0 ? toolCalls : undefined, }); break; } case 'tool': { for (const toolResponse of content) { const output = toolResponse.output; let contentValue: string; switch (output.type) { case 'text': case 'error-text': contentValue = output.value; break; case 'content': case 'json': case 'error-json': contentValue = JSON.stringify(output.value); break; } messages.push({ role: 'tool', name: toolResponse.toolName, tool_call_id: toolResponse.toolCallId, content: contentValue, }); } break; } default: { const _exhaustiveCheck: never = role; throw new Error(`Unsupported role: ${_exhaustiveCheck}`); } } } return messages; } --- File: /ai/packages/mistral/src/get-response-metadata.ts --- export function getResponseMetadata({ id, model, created, }: { id?: string | undefined | null; created?: number | undefined | null; model?: string | undefined | null; }) { return { id: id ?? undefined, modelId: model ?? undefined, timestamp: created != null ? new Date(created * 1000) : undefined, }; } --- File: /ai/packages/mistral/src/index.ts --- export { createMistral, mistral } from './mistral-provider'; export type { MistralProvider, MistralProviderSettings, } from './mistral-provider'; --- File: /ai/packages/mistral/src/map-mistral-finish-reason.ts --- import { LanguageModelV2FinishReason } from '@ai-sdk/provider'; export function mapMistralFinishReason( finishReason: string | null | undefined, ): LanguageModelV2FinishReason { switch (finishReason) { case 'stop': return 'stop'; case 'length': case 'model_length': return 'length'; case 'tool_calls': return 'tool-calls'; default: return 'unknown'; } } --- File: /ai/packages/mistral/src/mistral-chat-language-model.test.ts --- import { LanguageModelV2Prompt } from '@ai-sdk/provider'; import { convertReadableStreamToArray, createTestServer, } from '@ai-sdk/provider-utils/test'; import { createMistral } from './mistral-provider'; import { vi } from 'vitest'; const TEST_PROMPT: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; const provider = createMistral({ apiKey: 'test-api-key' }); const model = provider.chat('mistral-small-latest'); const server = createTestServer({ 'https://api.mistral.ai/v1/chat/completions': {}, }); describe('doGenerate', () => { function prepareJsonResponse({ content = '', usage = { prompt_tokens: 4, total_tokens: 34, completion_tokens: 30, }, id = '16362f24e60340d0994dd205c267a43a', created = 1711113008, model = 'mistral-small-latest', headers, }: { content?: string; usage?: { prompt_tokens: number; total_tokens: number; completion_tokens: number; }; id?: string; created?: number; model?: string; headers?: Record<string, string>; }) { server.urls['https://api.mistral.ai/v1/chat/completions'].response = { type: 'json-value', headers, body: { object: 'chat.completion', id, created, model, choices: [ { index: 0, message: { role: 'assistant', content, tool_calls: null, }, finish_reason: 'stop', logprobs: null, }, ], usage, }, }; } it('should extract text content', async () => { prepareJsonResponse({ content: 'Hello, World!' }); const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "text": "Hello, World!", "type": "text", }, ] `); }); it('should avoid duplication when there is a trailing assistant message', async () => { prepareJsonResponse({ content: 'prefix and more content' }); const { content } = await model.doGenerate({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, { role: 'assistant', content: [{ type: 'text', text: 'prefix ' }], }, ], }); expect(content).toMatchInlineSnapshot(` [ { "text": "and more content", "type": "text", }, ] `); }); it('should extract tool call content', async () => { server.urls['https://api.mistral.ai/v1/chat/completions'].response = { type: 'json-value', body: { id: 'b3999b8c93e04e11bcbff7bcab829667', object: 'chat.completion', created: 1722349660, model: 'mistral-large-latest', choices: [ { index: 0, message: { role: 'assistant', tool_calls: [ { id: 'gSIMJiOkT', function: { name: 'weatherTool', arguments: '{"location": "paris"}', }, }, ], }, finish_reason: 'tool_calls', logprobs: null, }, ], usage: { prompt_tokens: 124, total_tokens: 146, completion_tokens: 22 }, }, }; const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "input": "{"location": "paris"}", "toolCallId": "gSIMJiOkT", "toolName": "weatherTool", "type": "tool-call", }, ] `); }); it('should extract usage', async () => { prepareJsonResponse({ usage: { prompt_tokens: 20, total_tokens: 25, completion_tokens: 5 }, }); const { usage } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(usage).toMatchInlineSnapshot(` { "inputTokens": 20, "outputTokens": 5, "totalTokens": 25, } `); }); it('should send additional response information', async () => { prepareJsonResponse({ id: 'test-id', created: 123, model: 'test-model', }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect({ id: response?.id, timestamp: response?.timestamp, modelId: response?.modelId, }).toStrictEqual({ id: 'test-id', timestamp: new Date(123 * 1000), modelId: 'test-model', }); }); it('should expose the raw response headers', async () => { prepareJsonResponse({ headers: { 'test-header': 'test-value' }, }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-length': '314', 'content-type': 'application/json', // custom header 'test-header': 'test-value', }); }); it('should pass the model and the messages', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'mistral-small-latest', messages: [{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }], }); }); it('should pass tools and toolChoice', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], toolChoice: { type: 'tool', toolName: 'test-tool', }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'mistral-small-latest', messages: [{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }], tools: [ { type: 'function', function: { name: 'test-tool', parameters: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }, ], tool_choice: 'any', }); }); it('should pass headers', async () => { prepareJsonResponse({ content: '' }); const provider = createMistral({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.chat('mistral-small-latest').doGenerate({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, }); const requestHeaders = server.calls[0].requestHeaders; expect(requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should send request body', async () => { prepareJsonResponse({ content: '' }); const { request } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(request).toMatchInlineSnapshot(` { "body": { "document_image_limit": undefined, "document_page_limit": undefined, "max_tokens": undefined, "messages": [ { "content": [ { "text": "Hello", "type": "text", }, ], "role": "user", }, ], "model": "mistral-small-latest", "random_seed": undefined, "response_format": undefined, "safe_prompt": undefined, "temperature": undefined, "tool_choice": undefined, "tools": undefined, "top_p": undefined, }, } `); }); it('should extract content when message content is a content object', async () => { server.urls['https://api.mistral.ai/v1/chat/completions'].response = { type: 'json-value', body: { object: 'chat.completion', id: 'object-id', created: 1711113008, model: 'mistral-small-latest', choices: [ { index: 0, message: { role: 'assistant', content: [ { type: 'text', text: 'Hello from object', }, ], tool_calls: null, }, finish_reason: 'stop', logprobs: null, }, ], usage: { prompt_tokens: 4, total_tokens: 34, completion_tokens: 30 }, }, }; const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "text": "Hello from object", "type": "text", }, ] `); }); it('should return raw text with think tags for reasoning models', async () => { const reasoningModel = provider.chat('magistral-small-2506'); prepareJsonResponse({ content: "<think>\nLet me think about this problem step by step.\nFirst, I need to understand what the user is asking.\nThen I can provide a helpful response.\n</think>\n\nHello! I'm ready to help you with your question.", }); const { content } = await reasoningModel.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "text": "<think> Let me think about this problem step by step. First, I need to understand what the user is asking. Then I can provide a helpful response. </think> Hello! I'm ready to help you with your question.", "type": "text", }, ] `); }); }); describe('doStream', () => { function prepareStreamResponse({ content, headers, }: { content: string[]; headers?: Record<string, string>; }) { server.urls['https://api.mistral.ai/v1/chat/completions'].response = { type: 'stream-chunks', headers, chunks: [ `data: {"id":"53ff663126294946a6b7a4747b70597e","object":"chat.completion.chunk",` + `"created":1750537996,"model":"mistral-small-latest","choices":[{"index":0,` + `"delta":{"role":"assistant","content":""},"finish_reason":null,"logprobs":null}]}\n\n`, ...content.map(text => { return ( `data: {"id":"53ff663126294946a6b7a4747b70597e","object":"chat.completion.chunk",` + `"created":1750537996,"model":"mistral-small-latest","choices":[{"index":0,` + `"delta":{"role":"assistant","content":"${text}"},"finish_reason":null,"logprobs":null}]}\n\n` ); }), `data: {"id":"53ff663126294946a6b7a4747b70597e","object":"chat.completion.chunk",` + `"created":1750537996,"model":"mistral-small-latest","choices":[{"index":0,` + `"delta":{"content":""},"finish_reason":"stop","logprobs":null}],` + `"usage":{"prompt_tokens":4,"total_tokens":36,"completion_tokens":32}}\n\n`, `data: [DONE]\n\n`, ], }; } it('should stream text deltas', async () => { prepareStreamResponse({ content: ['Hello', ', ', 'world!'] }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "53ff663126294946a6b7a4747b70597e", "modelId": "mistral-small-latest", "timestamp": 2025-06-21T20:33:16.000Z, "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "delta": ", ", "id": "0", "type": "text-delta", }, { "delta": "world!", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 4, "outputTokens": 32, "totalTokens": 36, }, }, ] `); }); it('should avoid duplication when there is a trailing assistant message', async () => { prepareStreamResponse({ content: ['prefix', ' and', ' more content'] }); const { stream } = await model.doStream({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, { role: 'assistant', content: [{ type: 'text', text: 'prefix ' }], }, ], includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "53ff663126294946a6b7a4747b70597e", "modelId": "mistral-small-latest", "timestamp": 2025-06-21T20:33:16.000Z, "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "prefix", "id": "0", "type": "text-delta", }, { "delta": " and", "id": "0", "type": "text-delta", }, { "delta": " more content", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 4, "outputTokens": 32, "totalTokens": 36, }, }, ] `); }); it('should stream tool deltas', async () => { server.urls['https://api.mistral.ai/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"a8f32d91e5b64c2f9e7b3a8d4f6c1e5a","object":"chat.completion.chunk","created":1750538400,"model":"mistral-large-latest",` + `"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"logprobs":null}]}\n\n`, `data: {"id":"a8f32d91e5b64c2f9e7b3a8d4f6c1e5a","object":"chat.completion.chunk","created":1750538400,"model":"mistral-large-latest",` + `"choices":[{"index":0,"delta":{"content":null,"tool_calls":[{"id":"call_9K8xFjN2mP3qR7sT","function":{"name":"test-tool","arguments":` + `"{\\"value\\":\\"Sparkle Day\\"}"` + `}}]},"finish_reason":"tool_calls","logprobs":null}],"usage":{"prompt_tokens":183,"total_tokens":316,"completion_tokens":133}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await createMistral({ apiKey: 'test-api-key', }) .chat('mistral-large-latest') .doStream({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "a8f32d91e5b64c2f9e7b3a8d4f6c1e5a", "modelId": "mistral-large-latest", "timestamp": 2025-06-21T20:40:00.000Z, "type": "response-metadata", }, { "id": "call_9K8xFjN2mP3qR7sT", "toolName": "test-tool", "type": "tool-input-start", }, { "delta": "{"value":"Sparkle Day"}", "id": "call_9K8xFjN2mP3qR7sT", "type": "tool-input-delta", }, { "id": "call_9K8xFjN2mP3qR7sT", "type": "tool-input-end", }, { "input": "{"value":"Sparkle Day"}", "toolCallId": "call_9K8xFjN2mP3qR7sT", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "tool-calls", "type": "finish", "usage": { "inputTokens": 183, "outputTokens": 133, "totalTokens": 316, }, }, ] `); }); it('should expose the raw response headers', async () => { prepareStreamResponse({ content: [], headers: { 'test-header': 'test-value' }, }); const { response } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', // custom header 'test-header': 'test-value', }); }); it('should pass the messages', async () => { prepareStreamResponse({ content: [''] }); await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ stream: true, model: 'mistral-small-latest', messages: [{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }], }); }); it('should pass headers', async () => { prepareStreamResponse({ content: [] }); const provider = createMistral({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.chat('mistral-small-latest').doStream({ prompt: TEST_PROMPT, includeRawChunks: false, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should send request body', async () => { prepareStreamResponse({ content: [] }); const { request } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(request).toMatchInlineSnapshot(` { "body": { "document_image_limit": undefined, "document_page_limit": undefined, "max_tokens": undefined, "messages": [ { "content": [ { "text": "Hello", "type": "text", }, ], "role": "user", }, ], "model": "mistral-small-latest", "random_seed": undefined, "response_format": undefined, "safe_prompt": undefined, "stream": true, "temperature": undefined, "tool_choice": undefined, "tools": undefined, "top_p": undefined, }, } `); }); it('should stream text with content objects', async () => { // Instead of using prepareStreamResponse (which sends strings), // we set the chunks manually so that each delta's content is an object. server.urls['https://api.mistral.ai/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"b9e43f82d6c74a1e9f5b2c8e7a9d4f6b","object":"chat.completion.chunk","created":1750538500,"model":"mistral-small-latest","choices":[{"index":0,"delta":{"role":"assistant","content":[{"type":"text","text":""}]},"finish_reason":null,"logprobs":null}]}\n\n`, `data: {"id":"b9e43f82d6c74a1e9f5b2c8e7a9d4f6b","object":"chat.completion.chunk","created":1750538500,"model":"mistral-small-latest","choices":[{"index":0,"delta":{"content":[{"type":"text","text":"Hello"}]},"finish_reason":null,"logprobs":null}]}\n\n`, `data: {"id":"b9e43f82d6c74a1e9f5b2c8e7a9d4f6b","object":"chat.completion.chunk","created":1750538500,"model":"mistral-small-latest","choices":[{"index":0,"delta":{"content":[{"type":"text","text":", world!"}]},"finish_reason":"stop","logprobs":null}],"usage":{"prompt_tokens":4,"total_tokens":36,"completion_tokens":32}}\n\n`, `data: [DONE]\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "b9e43f82d6c74a1e9f5b2c8e7a9d4f6b", "modelId": "mistral-small-latest", "timestamp": 2025-06-21T20:41:40.000Z, "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "delta": ", world!", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 4, "outputTokens": 32, "totalTokens": 36, }, }, ] `); }); }); describe('doStream with raw chunks', () => { it('should stream raw chunks when includeRawChunks is true', async () => { server.urls['https://api.mistral.ai/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"c7d54e93f8a64b2e9c1f5a8b7d9e2f4c","object":"chat.completion.chunk","created":1750538600,"model":"mistral-large-latest","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null,"logprobs":null}]}\n\n`, `data: {"id":"d8e65fa4g9b75c3f0d2g6b9c8e0f3g5d","object":"chat.completion.chunk","created":1750538601,"model":"mistral-large-latest","choices":[{"index":0,"delta":{"content":" world"},"finish_reason":null,"logprobs":null}]}\n\n`, `data: {"id":"e9f76gb5h0c86d4g1e3h7c0d9f1g4h6e","object":"chat.completion.chunk","created":1750538602,"model":"mistral-large-latest","choices":[{"index":0,"delta":{},"finish_reason":"stop","logprobs":null}],"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: true, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "rawValue": { "choices": [ { "delta": { "content": "Hello", "role": "assistant", }, "finish_reason": null, "index": 0, "logprobs": null, }, ], "created": 1750538600, "id": "c7d54e93f8a64b2e9c1f5a8b7d9e2f4c", "model": "mistral-large-latest", "object": "chat.completion.chunk", }, "type": "raw", }, { "id": "c7d54e93f8a64b2e9c1f5a8b7d9e2f4c", "modelId": "mistral-large-latest", "timestamp": 2025-06-21T20:43:20.000Z, "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "rawValue": { "choices": [ { "delta": { "content": " world", }, "finish_reason": null, "index": 0, "logprobs": null, }, ], "created": 1750538601, "id": "d8e65fa4g9b75c3f0d2g6b9c8e0f3g5d", "model": "mistral-large-latest", "object": "chat.completion.chunk", }, "type": "raw", }, { "delta": " world", "id": "0", "type": "text-delta", }, { "rawValue": { "choices": [ { "delta": {}, "finish_reason": "stop", "index": 0, "logprobs": null, }, ], "created": 1750538602, "id": "e9f76gb5h0c86d4g1e3h7c0d9f1g4h6e", "model": "mistral-large-latest", "object": "chat.completion.chunk", "usage": { "completion_tokens": 5, "prompt_tokens": 10, "total_tokens": 15, }, }, "type": "raw", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 10, "outputTokens": 5, "totalTokens": 15, }, }, ] `); }); }); describe('tool result format support', () => { it('should handle new LanguageModelV2ToolResultOutput format', async () => { server.urls['https://api.mistral.ai/v1/chat/completions'].response = { type: 'json-value', body: { id: 'test-id', object: 'chat.completion', created: 1234567890, model: 'mistral-small', choices: [ { index: 0, message: { role: 'assistant', content: 'Here is the result', tool_calls: null, }, finish_reason: 'stop', }, ], usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15, }, }, }; const result = await model.doGenerate({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'Hello' }], }, { role: 'assistant', content: [ { type: 'tool-call', toolCallId: 'call-1', toolName: 'test-tool', input: { query: 'test' }, }, ], }, { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'call-1', toolName: 'test-tool', output: { type: 'json', value: { result: 'success' } }, }, ], }, ], }); expect(result.content).toEqual([ { type: 'text', text: 'Here is the result' }, ]); expect(result.finishReason).toBe('stop'); }); }); --- File: /ai/packages/mistral/src/mistral-chat-language-model.ts --- import { LanguageModelV2, LanguageModelV2CallWarning, LanguageModelV2Content, LanguageModelV2FinishReason, LanguageModelV2StreamPart, LanguageModelV2Usage, } from '@ai-sdk/provider'; import { combineHeaders, createEventSourceResponseHandler, createJsonResponseHandler, FetchFunction, parseProviderOptions, ParseResult, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { convertToMistralChatMessages } from './convert-to-mistral-chat-messages'; import { getResponseMetadata } from './get-response-metadata'; import { mapMistralFinishReason } from './map-mistral-finish-reason'; import { MistralChatModelId, mistralProviderOptions, } from './mistral-chat-options'; import { mistralFailedResponseHandler } from './mistral-error'; import { prepareTools } from './mistral-prepare-tools'; type MistralChatConfig = { provider: string; baseURL: string; headers: () => Record<string, string | undefined>; fetch?: FetchFunction; }; export class MistralChatLanguageModel implements LanguageModelV2 { readonly specificationVersion = 'v2'; readonly modelId: MistralChatModelId; private readonly config: MistralChatConfig; constructor(modelId: MistralChatModelId, config: MistralChatConfig) { this.modelId = modelId; this.config = config; } get provider(): string { return this.config.provider; } readonly supportedUrls: Record<string, RegExp[]> = { 'application/pdf': [/^https:\/\/.*$/], }; private async getArgs({ prompt, maxOutputTokens, temperature, topP, topK, frequencyPenalty, presencePenalty, stopSequences, responseFormat, seed, providerOptions, tools, toolChoice, }: Parameters<LanguageModelV2['doGenerate']>[0]) { const warnings: LanguageModelV2CallWarning[] = []; const options = (await parseProviderOptions({ provider: 'mistral', providerOptions, schema: mistralProviderOptions, })) ?? {}; if (topK != null) { warnings.push({ type: 'unsupported-setting', setting: 'topK', }); } if (frequencyPenalty != null) { warnings.push({ type: 'unsupported-setting', setting: 'frequencyPenalty', }); } if (presencePenalty != null) { warnings.push({ type: 'unsupported-setting', setting: 'presencePenalty', }); } if (stopSequences != null) { warnings.push({ type: 'unsupported-setting', setting: 'stopSequences', }); } if ( responseFormat != null && responseFormat.type === 'json' && responseFormat.schema != null ) { warnings.push({ type: 'unsupported-setting', setting: 'responseFormat', details: 'JSON response format schema is not supported', }); } const baseArgs = { // model id: model: this.modelId, // model specific settings: safe_prompt: options.safePrompt, // standardized settings: max_tokens: maxOutputTokens, temperature, top_p: topP, random_seed: seed, // response format: response_format: responseFormat?.type === 'json' ? { type: 'json_object' } : undefined, // mistral-specific provider options: document_image_limit: options.documentImageLimit, document_page_limit: options.documentPageLimit, // messages: messages: convertToMistralChatMessages(prompt), }; const { tools: mistralTools, toolChoice: mistralToolChoice, toolWarnings, } = prepareTools({ tools, toolChoice, }); return { args: { ...baseArgs, tools: mistralTools, tool_choice: mistralToolChoice, }, warnings: [...warnings, ...toolWarnings], }; } async doGenerate( options: Parameters<LanguageModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>> { const { args: body, warnings } = await this.getArgs(options); const { responseHeaders, value: response, rawValue: rawResponse, } = await postJsonToApi({ url: `${this.config.baseURL}/chat/completions`, headers: combineHeaders(this.config.headers(), options.headers), body, failedResponseHandler: mistralFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( mistralChatResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const choice = response.choices[0]; const content: Array<LanguageModelV2Content> = []; // text content: let text = extractTextContent(choice.message.content); // when there is a trailing assistant message, mistral will send the // content of that message again. we skip this repeated content to // avoid duplication, e.g. in continuation mode. const lastMessage = body.messages[body.messages.length - 1]; if ( lastMessage.role === 'assistant' && text?.startsWith(lastMessage.content) ) { text = text.slice(lastMessage.content.length); } if (text != null && text.length > 0) { content.push({ type: 'text', text }); } // tool calls: if (choice.message.tool_calls != null) { for (const toolCall of choice.message.tool_calls) { content.push({ type: 'tool-call', toolCallId: toolCall.id, toolName: toolCall.function.name, input: toolCall.function.arguments!, }); } } return { content, finishReason: mapMistralFinishReason(choice.finish_reason), usage: { inputTokens: response.usage.prompt_tokens, outputTokens: response.usage.completion_tokens, totalTokens: response.usage.total_tokens, }, request: { body }, response: { ...getResponseMetadata(response), headers: responseHeaders, body: rawResponse, }, warnings, }; } async doStream( options: Parameters<LanguageModelV2['doStream']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>> { const { args, warnings } = await this.getArgs(options); const body = { ...args, stream: true }; const { responseHeaders, value: response } = await postJsonToApi({ url: `${this.config.baseURL}/chat/completions`, headers: combineHeaders(this.config.headers(), options.headers), body, failedResponseHandler: mistralFailedResponseHandler, successfulResponseHandler: createEventSourceResponseHandler( mistralChatChunkSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); let finishReason: LanguageModelV2FinishReason = 'unknown'; const usage: LanguageModelV2Usage = { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, }; let isFirstChunk = true; let activeText = false; return { stream: response.pipeThrough( new TransformStream< ParseResult<z.infer<typeof mistralChatChunkSchema>>, LanguageModelV2StreamPart >({ start(controller) { controller.enqueue({ type: 'stream-start', warnings }); }, transform(chunk, controller) { // Emit raw chunk if requested (before anything else) if (options.includeRawChunks) { controller.enqueue({ type: 'raw', rawValue: chunk.rawValue }); } if (!chunk.success) { controller.enqueue({ type: 'error', error: chunk.error }); return; } const value = chunk.value; if (isFirstChunk) { isFirstChunk = false; controller.enqueue({ type: 'response-metadata', ...getResponseMetadata(value), }); } if (value.usage != null) { usage.inputTokens = value.usage.prompt_tokens; usage.outputTokens = value.usage.completion_tokens; usage.totalTokens = value.usage.total_tokens; } const choice = value.choices[0]; const delta = choice.delta; const textContent = extractTextContent(delta.content); if (textContent != null && textContent.length > 0) { if (!activeText) { controller.enqueue({ type: 'text-start', id: '0' }); activeText = true; } controller.enqueue({ type: 'text-delta', id: '0', delta: textContent, }); } if (delta?.tool_calls != null) { for (const toolCall of delta.tool_calls) { const toolCallId = toolCall.id; const toolName = toolCall.function.name; const input = toolCall.function.arguments; controller.enqueue({ type: 'tool-input-start', id: toolCallId, toolName, }); controller.enqueue({ type: 'tool-input-delta', id: toolCallId, delta: input, }); controller.enqueue({ type: 'tool-input-end', id: toolCallId, }); controller.enqueue({ type: 'tool-call', toolCallId, toolName, input, }); } } if (choice.finish_reason != null) { finishReason = mapMistralFinishReason(choice.finish_reason); } }, flush(controller) { if (activeText) { controller.enqueue({ type: 'text-end', id: '0' }); } controller.enqueue({ type: 'finish', finishReason, usage, }); }, }), ), request: { body }, response: { headers: responseHeaders }, }; } } function extractTextContent(content: z.infer<typeof mistralContentSchema>) { if (typeof content === 'string') { return content; } if (content == null) { return undefined; } const textContent: string[] = []; for (const chunk of content) { const { type } = chunk; switch (type) { case 'text': textContent.push(chunk.text); break; case 'image_url': case 'reference': // image content or reference content is currently ignored. break; default: { const _exhaustiveCheck: never = type; throw new Error(`Unsupported type: ${_exhaustiveCheck}`); } } } return textContent.length ? textContent.join('') : undefined; } const mistralContentSchema = z .union([ z.string(), z.array( z.discriminatedUnion('type', [ z.object({ type: z.literal('text'), text: z.string(), }), z.object({ type: z.literal('image_url'), image_url: z.union([ z.string(), z.object({ url: z.string(), detail: z.string().nullable(), }), ]), }), z.object({ type: z.literal('reference'), reference_ids: z.array(z.number()), }), ]), ), ]) .nullish(); const mistralUsageSchema = z.object({ prompt_tokens: z.number(), completion_tokens: z.number(), total_tokens: z.number(), }); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const mistralChatResponseSchema = z.object({ id: z.string().nullish(), created: z.number().nullish(), model: z.string().nullish(), choices: z.array( z.object({ message: z.object({ role: z.literal('assistant'), content: mistralContentSchema, tool_calls: z .array( z.object({ id: z.string(), function: z.object({ name: z.string(), arguments: z.string() }), }), ) .nullish(), }), index: z.number(), finish_reason: z.string().nullish(), }), ), object: z.literal('chat.completion'), usage: mistralUsageSchema, }); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const mistralChatChunkSchema = z.object({ id: z.string().nullish(), created: z.number().nullish(), model: z.string().nullish(), choices: z.array( z.object({ delta: z.object({ role: z.enum(['assistant']).optional(), content: mistralContentSchema, tool_calls: z .array( z.object({ id: z.string(), function: z.object({ name: z.string(), arguments: z.string() }), }), ) .nullish(), }), finish_reason: z.string().nullish(), index: z.number(), }), ), usage: mistralUsageSchema.nullish(), }); --- File: /ai/packages/mistral/src/mistral-chat-options.ts --- import { z } from 'zod/v4'; // https://docs.mistral.ai/getting-started/models/models_overview/ export type MistralChatModelId = // premier | 'ministral-3b-latest' | 'ministral-8b-latest' | 'mistral-large-latest' | 'mistral-medium-latest' | 'mistral-medium-2505' | 'mistral-small-latest' | 'pixtral-large-latest' // reasoning models | 'magistral-small-2506' | 'magistral-medium-2506' // free | 'pixtral-12b-2409' // legacy | 'open-mistral-7b' | 'open-mixtral-8x7b' | 'open-mixtral-8x22b' | (string & {}); export const mistralProviderOptions = z.object({ /** Whether to inject a safety prompt before all conversations. Defaults to `false`. */ safePrompt: z.boolean().optional(), documentImageLimit: z.number().optional(), documentPageLimit: z.number().optional(), }); export type MistralProviderOptions = z.infer<typeof mistralProviderOptions>; --- File: /ai/packages/mistral/src/mistral-chat-prompt.ts --- export type MistralPrompt = Array<MistralMessage>; export type MistralMessage = | MistralSystemMessage | MistralUserMessage | MistralAssistantMessage | MistralToolMessage; export interface MistralSystemMessage { role: 'system'; content: string; } export interface MistralUserMessage { role: 'user'; content: Array<MistralUserMessageContent>; } export type MistralUserMessageContent = | { type: 'text'; text: string } | { type: 'image_url'; image_url: string } | { type: 'document_url'; document_url: string }; export interface MistralAssistantMessage { role: 'assistant'; content: string; prefix?: boolean; tool_calls?: Array<{ id: string; type: 'function'; function: { name: string; arguments: string }; }>; } export interface MistralToolMessage { role: 'tool'; name: string; content: string; tool_call_id: string; } export type MistralToolChoice = | { type: 'function'; function: { name: string } } | 'auto' | 'none' | 'any'; --- File: /ai/packages/mistral/src/mistral-embedding-model.test.ts --- import { EmbeddingModelV2Embedding } from '@ai-sdk/provider'; import { createTestServer } from '@ai-sdk/provider-utils/test'; import { createMistral } from './mistral-provider'; const dummyEmbeddings = [ [0.1, 0.2, 0.3, 0.4, 0.5], [0.6, 0.7, 0.8, 0.9, 1.0], ]; const testValues = ['sunny day at the beach', 'rainy day in the city']; const provider = createMistral({ apiKey: 'test-api-key' }); const model = provider.textEmbeddingModel('mistral-embed'); const server = createTestServer({ 'https://api.mistral.ai/v1/embeddings': {}, }); describe('doEmbed', () => { function prepareJsonResponse({ embeddings = dummyEmbeddings, usage = { prompt_tokens: 8, total_tokens: 8 }, headers, }: { embeddings?: EmbeddingModelV2Embedding[]; usage?: { prompt_tokens: number; total_tokens: number }; headers?: Record<string, string>; } = {}) { server.urls['https://api.mistral.ai/v1/embeddings'].response = { type: 'json-value', headers, body: { id: 'b322cfc2b9d34e2f8e14fc99874faee5', object: 'list', data: embeddings.map((embedding, i) => ({ object: 'embedding', embedding, index: i, })), model: 'mistral-embed', usage, }, }; } it('should extract embedding', async () => { prepareJsonResponse(); const { embeddings } = await model.doEmbed({ values: testValues }); expect(embeddings).toStrictEqual(dummyEmbeddings); }); it('should extract usage', async () => { prepareJsonResponse({ usage: { prompt_tokens: 20, total_tokens: 20 }, }); const { usage } = await model.doEmbed({ values: testValues }); expect(usage).toStrictEqual({ tokens: 20 }); }); it('should expose the raw response', async () => { prepareJsonResponse({ headers: { 'test-header': 'test-value' }, }); const { response } = await model.doEmbed({ values: testValues }); expect(response?.headers).toStrictEqual({ // default headers: 'content-length': '267', 'content-type': 'application/json', // custom header 'test-header': 'test-value', }); expect(response).toMatchSnapshot(); }); it('should pass the model and the values', async () => { prepareJsonResponse(); await model.doEmbed({ values: testValues }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'mistral-embed', input: testValues, encoding_format: 'float', }); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createMistral({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.embedding('mistral-embed').doEmbed({ values: testValues, headers: { 'Custom-Request-Header': 'request-header-value', }, }); const requestHeaders = server.calls[0].requestHeaders; expect(requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); }); --- File: /ai/packages/mistral/src/mistral-embedding-model.ts --- import { EmbeddingModelV2, TooManyEmbeddingValuesForCallError, } from '@ai-sdk/provider'; import { combineHeaders, createJsonResponseHandler, FetchFunction, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { MistralEmbeddingModelId } from './mistral-embedding-options'; import { mistralFailedResponseHandler } from './mistral-error'; type MistralEmbeddingConfig = { provider: string; baseURL: string; headers: () => Record<string, string | undefined>; fetch?: FetchFunction; }; export class MistralEmbeddingModel implements EmbeddingModelV2<string> { readonly specificationVersion = 'v2'; readonly modelId: MistralEmbeddingModelId; readonly maxEmbeddingsPerCall = 32; readonly supportsParallelCalls = false; private readonly config: MistralEmbeddingConfig; get provider(): string { return this.config.provider; } constructor( modelId: MistralEmbeddingModelId, config: MistralEmbeddingConfig, ) { this.modelId = modelId; this.config = config; } async doEmbed({ values, abortSignal, headers, }: Parameters<EmbeddingModelV2<string>['doEmbed']>[0]): Promise< Awaited<ReturnType<EmbeddingModelV2<string>['doEmbed']>> > { if (values.length > this.maxEmbeddingsPerCall) { throw new TooManyEmbeddingValuesForCallError({ provider: this.provider, modelId: this.modelId, maxEmbeddingsPerCall: this.maxEmbeddingsPerCall, values, }); } const { responseHeaders, value: response, rawValue, } = await postJsonToApi({ url: `${this.config.baseURL}/embeddings`, headers: combineHeaders(this.config.headers(), headers), body: { model: this.modelId, input: values, encoding_format: 'float', }, failedResponseHandler: mistralFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( MistralTextEmbeddingResponseSchema, ), abortSignal, fetch: this.config.fetch, }); return { embeddings: response.data.map(item => item.embedding), usage: response.usage ? { tokens: response.usage.prompt_tokens } : undefined, response: { headers: responseHeaders, body: rawValue }, }; } } // minimal version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const MistralTextEmbeddingResponseSchema = z.object({ data: z.array(z.object({ embedding: z.array(z.number()) })), usage: z.object({ prompt_tokens: z.number() }).nullish(), }); --- File: /ai/packages/mistral/src/mistral-embedding-options.ts --- export type MistralEmbeddingModelId = 'mistral-embed' | (string & {}); --- File: /ai/packages/mistral/src/mistral-error.ts --- import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; const mistralErrorDataSchema = z.object({ object: z.literal('error'), message: z.string(), type: z.string(), param: z.string().nullable(), code: z.string().nullable(), }); export type MistralErrorData = z.infer<typeof mistralErrorDataSchema>; export const mistralFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: mistralErrorDataSchema, errorToMessage: data => data.message, }); --- File: /ai/packages/mistral/src/mistral-prepare-tools.ts --- import { LanguageModelV2CallOptions, LanguageModelV2CallWarning, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { MistralToolChoice } from './mistral-chat-prompt'; export function prepareTools({ tools, toolChoice, }: { tools: LanguageModelV2CallOptions['tools']; toolChoice?: LanguageModelV2CallOptions['toolChoice']; }): { tools: | Array<{ type: 'function'; function: { name: string; description: string | undefined; parameters: unknown; }; }> | undefined; toolChoice: MistralToolChoice | undefined; toolWarnings: LanguageModelV2CallWarning[]; } { // when the tools array is empty, change it to undefined to prevent errors: tools = tools?.length ? tools : undefined; const toolWarnings: LanguageModelV2CallWarning[] = []; if (tools == null) { return { tools: undefined, toolChoice: undefined, toolWarnings }; } const mistralTools: Array<{ type: 'function'; function: { name: string; description: string | undefined; parameters: unknown; }; }> = []; for (const tool of tools) { if (tool.type === 'provider-defined') { toolWarnings.push({ type: 'unsupported-tool', tool }); } else { mistralTools.push({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.inputSchema, }, }); } } if (toolChoice == null) { return { tools: mistralTools, toolChoice: undefined, toolWarnings }; } const type = toolChoice.type; switch (type) { case 'auto': case 'none': return { tools: mistralTools, toolChoice: type, toolWarnings }; case 'required': return { tools: mistralTools, toolChoice: 'any', toolWarnings }; // mistral does not support tool mode directly, // so we filter the tools and force the tool choice through 'any' case 'tool': return { tools: mistralTools.filter( tool => tool.function.name === toolChoice.toolName, ), toolChoice: 'any', toolWarnings, }; default: { const _exhaustiveCheck: never = type; throw new UnsupportedFunctionalityError({ functionality: `tool choice type: ${_exhaustiveCheck}`, }); } } } --- File: /ai/packages/mistral/src/mistral-provider.ts --- import { EmbeddingModelV2, LanguageModelV2, NoSuchModelError, ProviderV2, } from '@ai-sdk/provider'; import { FetchFunction, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { MistralChatLanguageModel } from './mistral-chat-language-model'; import { MistralChatModelId } from './mistral-chat-options'; import { MistralEmbeddingModel } from './mistral-embedding-model'; import { MistralEmbeddingModelId } from './mistral-embedding-options'; export interface MistralProvider extends ProviderV2 { (modelId: MistralChatModelId): LanguageModelV2; /** Creates a model for text generation. */ languageModel(modelId: MistralChatModelId): LanguageModelV2; /** Creates a model for text generation. */ chat(modelId: MistralChatModelId): LanguageModelV2; /** @deprecated Use `textEmbedding()` instead. */ embedding(modelId: MistralEmbeddingModelId): EmbeddingModelV2<string>; textEmbedding(modelId: MistralEmbeddingModelId): EmbeddingModelV2<string>; textEmbeddingModel: ( modelId: MistralEmbeddingModelId, ) => EmbeddingModelV2<string>; } export interface MistralProviderSettings { /** Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.mistral.ai/v1`. */ baseURL?: string; /** API key that is being send using the `Authorization` header. It defaults to the `MISTRAL_API_KEY` environment variable. */ apiKey?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } /** Create a Mistral AI provider instance. */ export function createMistral( options: MistralProviderSettings = {}, ): MistralProvider { const baseURL = withoutTrailingSlash(options.baseURL) ?? 'https://api.mistral.ai/v1'; const getHeaders = () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'MISTRAL_API_KEY', description: 'Mistral', })}`, ...options.headers, }); const createChatModel = (modelId: MistralChatModelId) => new MistralChatLanguageModel(modelId, { provider: 'mistral.chat', baseURL, headers: getHeaders, fetch: options.fetch, }); const createEmbeddingModel = (modelId: MistralEmbeddingModelId) => new MistralEmbeddingModel(modelId, { provider: 'mistral.embedding', baseURL, headers: getHeaders, fetch: options.fetch, }); const provider = function (modelId: MistralChatModelId) { if (new.target) { throw new Error( 'The Mistral model function cannot be called with the new keyword.', ); } return createChatModel(modelId); }; provider.languageModel = createChatModel; provider.chat = createChatModel; provider.embedding = createEmbeddingModel; provider.textEmbedding = createEmbeddingModel; provider.textEmbeddingModel = createEmbeddingModel; provider.imageModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'imageModel' }); }; return provider; } /** Default Mistral provider instance. */ export const mistral = createMistral(); --- File: /ai/packages/mistral/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/mistral/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/mistral/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/openai/src/internal/index.ts --- export * from '../openai-chat-language-model'; export * from '../openai-chat-options'; export * from '../openai-completion-language-model'; export * from '../openai-completion-options'; export * from '../openai-embedding-model'; export * from '../openai-embedding-options'; export * from '../openai-image-model'; export * from '../openai-image-settings'; export * from '../openai-transcription-model'; export * from '../openai-transcription-options'; export * from '../openai-speech-model'; export * from '../openai-speech-options'; export * from '../responses/openai-responses-language-model'; --- File: /ai/packages/openai/src/responses/convert-to-openai-responses-messages.test.ts --- import { convertToOpenAIResponsesMessages } from './convert-to-openai-responses-messages'; describe('convertToOpenAIResponsesMessages', () => { describe('system messages', () => { it('should convert system messages to system role', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [{ role: 'system', content: 'Hello' }], systemMessageMode: 'system', }); expect(result.messages).toEqual([{ role: 'system', content: 'Hello' }]); }); it('should convert system messages to developer role', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [{ role: 'system', content: 'Hello' }], systemMessageMode: 'developer', }); expect(result.messages).toEqual([ { role: 'developer', content: 'Hello' }, ]); }); it('should remove system messages', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [{ role: 'system', content: 'Hello' }], systemMessageMode: 'remove', }); expect(result.messages).toEqual([]); }); }); describe('user messages', () => { it('should convert messages with only a text part to a string content', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'Hello' }], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ]); }); it('should convert messages with image parts using URL', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'user', content: [ { type: 'text', text: 'Hello' }, { type: 'file', mediaType: 'image/*', data: new URL('https://example.com/image.jpg'), }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'input_text', text: 'Hello' }, { type: 'input_image', image_url: 'https://example.com/image.jpg', }, ], }, ]); }); it('should convert messages with image parts using binary data', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'user', content: [ { type: 'file', mediaType: 'image/png', data: Buffer.from([0, 1, 2, 3]).toString('base64'), }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'input_image', image_url: 'data:image/png;base64,AAECAw==', }, ], }, ]); }); it('should convert messages with image parts using file_id', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'user', content: [ { type: 'file', mediaType: 'image/png', data: 'file-12345', }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'input_image', file_id: 'file-12345', }, ], }, ]); }); it('should use default mime type for binary images', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'user', content: [ { type: 'file', mediaType: 'image/*', data: Buffer.from([0, 1, 2, 3]).toString('base64'), }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'input_image', image_url: 'data:image/jpeg;base64,AAECAw==', }, ], }, ]); }); it('should add image detail when specified through extension', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'user', content: [ { type: 'file', mediaType: 'image/png', data: Buffer.from([0, 1, 2, 3]).toString('base64'), providerOptions: { openai: { imageDetail: 'low', }, }, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'input_image', image_url: 'data:image/png;base64,AAECAw==', detail: 'low', }, ], }, ]); }); it('should convert messages with PDF file parts', async () => { const base64Data = 'AQIDBAU='; // Base64 encoding of pdfData const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data: base64Data, filename: 'document.pdf', }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'input_file', filename: 'document.pdf', file_data: 'data:application/pdf;base64,AQIDBAU=', }, ], }, ]); }); it('should convert messages with PDF file parts using file_id', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data: 'file-pdf-12345', }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'input_file', file_id: 'file-pdf-12345', }, ], }, ]); }); it('should use default filename for PDF file parts when not provided', async () => { const base64Data = 'AQIDBAU='; const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data: base64Data, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'input_file', filename: 'part-0.pdf', file_data: 'data:application/pdf;base64,AQIDBAU=', }, ], }, ]); }); it('should throw error for unsupported file types', async () => { const base64Data = 'AQIDBAU='; await expect( convertToOpenAIResponsesMessages({ prompt: [ { role: 'user', content: [ { type: 'file', mediaType: 'text/plain', data: base64Data, }, ], }, ], systemMessageMode: 'system', }), ).rejects.toThrow('file part media type text/plain'); }); it('should throw error for file URLs', async () => { await expect( convertToOpenAIResponsesMessages({ prompt: [ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data: new URL('https://example.com/document.pdf'), }, ], }, ], systemMessageMode: 'system', }), ).rejects.toThrow('PDF file parts with URLs'); }); }); describe('assistant messages', () => { it('should convert messages with only a text part to a string content', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [{ type: 'text', text: 'Hello' }] }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { role: 'assistant', content: [{ type: 'output_text', text: 'Hello' }], }, ]); }); it('should convert messages with tool call parts', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [ { type: 'text', text: 'I will search for that information.' }, { type: 'tool-call', toolCallId: 'call_123', toolName: 'search', input: { query: 'weather in San Francisco' }, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { role: 'assistant', content: [ { type: 'output_text', text: 'I will search for that information.', }, ], }, { type: 'function_call', call_id: 'call_123', name: 'search', arguments: JSON.stringify({ query: 'weather in San Francisco' }), }, ]); }); it('should convert messages with tool call parts that have ids', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [ { type: 'text', text: 'I will search for that information.', providerOptions: { openai: { itemId: 'id_123', }, }, }, { type: 'tool-call', toolCallId: 'call_123', toolName: 'search', input: { query: 'weather in San Francisco' }, providerOptions: { openai: { itemId: 'id_456', }, }, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toMatchInlineSnapshot(` [ { "content": [ { "text": "I will search for that information.", "type": "output_text", }, ], "id": "id_123", "role": "assistant", }, { "arguments": "{"query":"weather in San Francisco"}", "call_id": "call_123", "id": "id_456", "name": "search", "type": "function_call", }, ] `); }); it('should convert multiple tool call parts in a single message', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [ { type: 'tool-call', toolCallId: 'call_123', toolName: 'search', input: { query: 'weather in San Francisco' }, }, { type: 'tool-call', toolCallId: 'call_456', toolName: 'calculator', input: { expression: '2 + 2' }, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { type: 'function_call', call_id: 'call_123', name: 'search', arguments: JSON.stringify({ query: 'weather in San Francisco' }), }, { type: 'function_call', call_id: 'call_456', name: 'calculator', arguments: JSON.stringify({ expression: '2 + 2' }), }, ]); }); describe('reasoning messages', () => { describe('basic conversion', () => { it('should convert single reasoning part with text', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [ { type: 'reasoning', text: 'Analyzing the problem step by step', providerOptions: { openai: { itemId: 'reasoning_001', }, }, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { type: 'reasoning', id: 'reasoning_001', encrypted_content: undefined, summary: [ { type: 'summary_text', text: 'Analyzing the problem step by step', }, ], }, ]); expect(result.warnings).toHaveLength(0); }); it('should convert single reasoning part with encrypted content', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [ { type: 'reasoning', text: 'Analyzing the problem step by step', providerOptions: { openai: { itemId: 'reasoning_001', reasoningEncryptedContent: 'encrypted_content_001', }, }, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { type: 'reasoning', id: 'reasoning_001', encrypted_content: 'encrypted_content_001', summary: [ { type: 'summary_text', text: 'Analyzing the problem step by step', }, ], }, ]); expect(result.warnings).toHaveLength(0); }); it('should convert single reasoning part with null encrypted content', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [ { type: 'reasoning', text: 'Analyzing the problem step by step', providerOptions: { openai: { itemId: 'reasoning_001', reasoningEncryptedContent: null, }, }, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { type: 'reasoning', id: 'reasoning_001', encrypted_content: null, summary: [ { type: 'summary_text', text: 'Analyzing the problem step by step', }, ], }, ]); expect(result.warnings).toHaveLength(0); }); }); describe('empty text handling', () => { it('should create empty summary for initial empty text', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [ { type: 'reasoning', text: '', // Empty text should NOT generate warning when it's the first reasoning part providerOptions: { openai: { itemId: 'reasoning_001', }, }, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { type: 'reasoning', id: 'reasoning_001', encrypted_content: undefined, summary: [], }, ]); expect(result.warnings).toHaveLength(0); }); it('should create empty summary for initial empty text with encrypted content', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [ { type: 'reasoning', text: '', // Empty text should NOT generate warning when it's the first reasoning part providerOptions: { openai: { itemId: 'reasoning_001', reasoningEncryptedContent: 'encrypted_content_001', }, }, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { type: 'reasoning', id: 'reasoning_001', encrypted_content: 'encrypted_content_001', summary: [], }, ]); expect(result.warnings).toHaveLength(0); }); it('should warn when appending empty text to existing sequence', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [ { type: 'reasoning', text: 'First reasoning step', providerOptions: { openai: { itemId: 'reasoning_001', }, }, }, { type: 'reasoning', text: '', // Empty text should generate warning when appending to existing reasoning sequence providerOptions: { openai: { itemId: 'reasoning_001', }, }, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { type: 'reasoning', id: 'reasoning_001', encrypted_content: undefined, summary: [ { type: 'summary_text', text: 'First reasoning step', }, ], }, ]); expect(result.warnings).toMatchInlineSnapshot(` [ { "message": "Cannot append empty reasoning part to existing reasoning sequence. Skipping reasoning part: {"type":"reasoning","text":"","providerOptions":{"openai":{"itemId":"reasoning_001"}}}.", "type": "other", }, ] `); }); }); describe('merging and sequencing', () => { it('should merge consecutive parts with same reasoning ID', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [ { type: 'reasoning', text: 'First reasoning step', providerOptions: { openai: { itemId: 'reasoning_001', }, }, }, { type: 'reasoning', text: 'Second reasoning step', providerOptions: { openai: { itemId: 'reasoning_001', }, }, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { type: 'reasoning', id: 'reasoning_001', encrypted_content: undefined, summary: [ { type: 'summary_text', text: 'First reasoning step', }, { type: 'summary_text', text: 'Second reasoning step', }, ], }, ]); expect(result.warnings).toHaveLength(0); }); it('should create separate messages for different reasoning IDs', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [ { type: 'reasoning', text: 'First reasoning block', providerOptions: { openai: { itemId: 'reasoning_001', }, }, }, { type: 'reasoning', text: 'Second reasoning block', providerOptions: { openai: { itemId: 'reasoning_002', }, }, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { type: 'reasoning', id: 'reasoning_001', encrypted_content: undefined, summary: [ { type: 'summary_text', text: 'First reasoning block', }, ], }, { type: 'reasoning', id: 'reasoning_002', encrypted_content: undefined, summary: [ { type: 'summary_text', text: 'Second reasoning block', }, ], }, ]); expect(result.warnings).toHaveLength(0); }); it('should handle reasoning across multiple assistant messages', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'First user question' }], }, { role: 'assistant', content: [ { type: 'reasoning', text: 'First reasoning step (message 1)', providerOptions: { openai: { itemId: 'reasoning_001', }, }, }, { type: 'reasoning', text: 'Second reasoning step (message 1)', providerOptions: { openai: { itemId: 'reasoning_001', }, }, }, { type: 'text', text: 'First response' }, ], }, { role: 'user', content: [{ type: 'text', text: 'Second user question' }], }, { role: 'assistant', content: [ { type: 'reasoning', text: 'First reasoning step (message 2)', providerOptions: { openai: { itemId: 'reasoning_002', }, }, }, { type: 'text', text: 'Second response' }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { role: 'user', content: [{ type: 'input_text', text: 'First user question' }], }, { type: 'reasoning', id: 'reasoning_001', encrypted_content: undefined, summary: [ { type: 'summary_text', text: 'First reasoning step (message 1)', }, { type: 'summary_text', text: 'Second reasoning step (message 1)', }, ], }, { role: 'assistant', content: [{ type: 'output_text', text: 'First response' }], }, { role: 'user', content: [{ type: 'input_text', text: 'Second user question' }], }, { type: 'reasoning', id: 'reasoning_002', encrypted_content: undefined, summary: [ { type: 'summary_text', text: 'First reasoning step (message 2)', }, ], }, { role: 'assistant', content: [{ type: 'output_text', text: 'Second response' }], }, ]); expect(result.warnings).toHaveLength(0); }); it('should handle complex reasoning sequences with tool interactions', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [ // First reasoning block: reasoning → reasoning { type: 'reasoning', text: 'Initial analysis step 1', providerOptions: { openai: { itemId: 'reasoning_001', reasoningEncryptedContent: 'encrypted_content_001', }, }, }, { type: 'reasoning', text: 'Initial analysis step 2', providerOptions: { openai: { itemId: 'reasoning_001', reasoningEncryptedContent: 'encrypted_content_001', }, }, }, // First tool interaction: tool-call { type: 'tool-call', toolCallId: 'call_001', toolName: 'search', input: { query: 'initial search' }, }, ], }, // Tool result comes as separate message { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'call_001', toolName: 'search', output: { type: 'json', value: { results: ['result1', 'result2'] }, }, }, ], }, { role: 'assistant', content: [ // Second reasoning block: reasoning → reasoning → reasoning { type: 'reasoning', text: 'Processing results step 1', providerOptions: { openai: { itemId: 'reasoning_002', reasoningEncryptedContent: 'encrypted_content_002', }, }, }, { type: 'reasoning', text: 'Processing results step 2', providerOptions: { openai: { itemId: 'reasoning_002', reasoningEncryptedContent: 'encrypted_content_002', }, }, }, { type: 'reasoning', text: 'Processing results step 3', providerOptions: { openai: { itemId: 'reasoning_002', reasoningEncryptedContent: 'encrypted_content_002', }, }, }, // Second tool interaction: tool-call { type: 'tool-call', toolCallId: 'call_002', toolName: 'calculator', input: { expression: '2 + 2' }, }, ], }, // Second tool result { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'call_002', toolName: 'calculator', output: { type: 'json', value: { result: 4 }, }, }, ], }, { role: 'assistant', content: [ // Final text output { type: 'text', text: 'Based on my analysis and calculations, here is the final answer.', }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ // First reasoning block (2 parts merged) { type: 'reasoning', id: 'reasoning_001', encrypted_content: 'encrypted_content_001', summary: [ { type: 'summary_text', text: 'Initial analysis step 1', }, { type: 'summary_text', text: 'Initial analysis step 2', }, ], }, // First tool call { type: 'function_call', call_id: 'call_001', name: 'search', arguments: JSON.stringify({ query: 'initial search' }), }, // First tool result { type: 'function_call_output', call_id: 'call_001', output: JSON.stringify({ results: ['result1', 'result2'] }), }, // Second reasoning block (3 parts merged) { type: 'reasoning', id: 'reasoning_002', encrypted_content: 'encrypted_content_002', summary: [ { type: 'summary_text', text: 'Processing results step 1', }, { type: 'summary_text', text: 'Processing results step 2', }, { type: 'summary_text', text: 'Processing results step 3', }, ], }, // Second tool call { type: 'function_call', call_id: 'call_002', name: 'calculator', arguments: JSON.stringify({ expression: '2 + 2' }), }, // Second tool result { type: 'function_call_output', call_id: 'call_002', output: JSON.stringify({ result: 4 }), }, // Final text output { role: 'assistant', content: [ { type: 'output_text', text: 'Based on my analysis and calculations, here is the final answer.', }, ], }, ]); expect(result.warnings).toHaveLength(0); }); }); describe('error handling', () => { it('should warn when reasoning part has no provider options', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [ { type: 'reasoning', text: 'This is a reasoning part without any provider options', }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toHaveLength(0); expect(result.warnings).toMatchInlineSnapshot(` [ { "message": "Non-OpenAI reasoning parts are not supported. Skipping reasoning part: {"type":"reasoning","text":"This is a reasoning part without any provider options"}.", "type": "other", }, ] `); }); it('should warn when reasoning part lacks OpenAI-specific reasoning ID provider options', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [ { type: 'reasoning', text: 'This is a reasoning part without OpenAI-specific reasoning id provider options', providerOptions: { openai: { reasoning: { encryptedContent: 'encrypted_content_001', }, }, }, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toHaveLength(0); expect(result.warnings).toMatchInlineSnapshot(` [ { "message": "Non-OpenAI reasoning parts are not supported. Skipping reasoning part: {"type":"reasoning","text":"This is a reasoning part without OpenAI-specific reasoning id provider options","providerOptions":{"openai":{"reasoning":{"encryptedContent":"encrypted_content_001"}}}}.", "type": "other", }, ] `); }); }); }); }); describe('tool messages', () => { it('should convert tool result parts', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'call_123', toolName: 'search', output: { type: 'json', value: { temperature: '72°F', condition: 'Sunny' }, }, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { type: 'function_call_output', call_id: 'call_123', output: JSON.stringify({ temperature: '72°F', condition: 'Sunny' }), }, ]); }); it('should convert multiple tool result parts in a single message', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'call_123', toolName: 'search', output: { type: 'json', value: { temperature: '72°F', condition: 'Sunny' }, }, }, { type: 'tool-result', toolCallId: 'call_456', toolName: 'calculator', output: { type: 'json', value: 4 }, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { type: 'function_call_output', call_id: 'call_123', output: JSON.stringify({ temperature: '72°F', condition: 'Sunny' }), }, { type: 'function_call_output', call_id: 'call_456', output: JSON.stringify(4), }, ]); }); }); describe('provider-executed tool calls', () => { it('should exclude provider-executed tool calls and results from prompt', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [ { type: 'text', text: 'Let me search for recent news from San Francisco.', }, { type: 'tool-call', toolCallId: 'ws_67cf2b3051e88190b006770db6fdb13d', toolName: 'web_search_preview', input: { query: 'San Francisco major news events June 22 2025', }, providerExecuted: true, }, { type: 'tool-result', toolCallId: 'ws_67cf2b3051e88190b006770db6fdb13d', toolName: 'web_search_preview', output: { type: 'json', value: [ { url: 'https://patch.com/california/san-francisco/calendar', }, ], }, }, { type: 'text', text: 'Based on the search results, several significant events took place in San Francisco yesterday (June 22, 2025).', }, ], }, ], systemMessageMode: 'system', }); expect(result).toMatchInlineSnapshot(` { "messages": [ { "content": [ { "text": "Let me search for recent news from San Francisco.", "type": "output_text", }, ], "id": undefined, "role": "assistant", }, { "content": [ { "text": "Based on the search results, several significant events took place in San Francisco yesterday (June 22, 2025).", "type": "output_text", }, ], "id": undefined, "role": "assistant", }, ], "warnings": [ { "message": "tool result parts in assistant messages are not supported for OpenAI responses", "type": "other", }, ], } `); }); it('should include client-side tool calls in prompt', async () => { const result = await convertToOpenAIResponsesMessages({ prompt: [ { role: 'assistant', content: [ { type: 'tool-call', toolCallId: 'call-1', toolName: 'calculator', input: { a: 1, b: 2 }, providerExecuted: false, }, ], }, ], systemMessageMode: 'system', }); expect(result).toMatchInlineSnapshot(` { "messages": [ { "arguments": "{"a":1,"b":2}", "call_id": "call-1", "id": undefined, "name": "calculator", "type": "function_call", }, ], "warnings": [], } `); }); }); }); --- File: /ai/packages/openai/src/responses/convert-to-openai-responses-messages.ts --- import { LanguageModelV2CallWarning, LanguageModelV2Prompt, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { parseProviderOptions } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { OpenAIResponsesPrompt, OpenAIResponsesReasoning, } from './openai-responses-api-types'; import { convertToBase64 } from '@ai-sdk/provider-utils'; export async function convertToOpenAIResponsesMessages({ prompt, systemMessageMode, }: { prompt: LanguageModelV2Prompt; systemMessageMode: 'system' | 'developer' | 'remove'; }): Promise<{ messages: OpenAIResponsesPrompt; warnings: Array<LanguageModelV2CallWarning>; }> { const messages: OpenAIResponsesPrompt = []; const warnings: Array<LanguageModelV2CallWarning> = []; for (const { role, content } of prompt) { switch (role) { case 'system': { switch (systemMessageMode) { case 'system': { messages.push({ role: 'system', content }); break; } case 'developer': { messages.push({ role: 'developer', content }); break; } case 'remove': { warnings.push({ type: 'other', message: 'system messages are removed for this model', }); break; } default: { const _exhaustiveCheck: never = systemMessageMode; throw new Error( `Unsupported system message mode: ${_exhaustiveCheck}`, ); } } break; } case 'user': { messages.push({ role: 'user', content: content.map((part, index) => { switch (part.type) { case 'text': { return { type: 'input_text', text: part.text }; } case 'file': { if (part.mediaType.startsWith('image/')) { const mediaType = part.mediaType === 'image/*' ? 'image/jpeg' : part.mediaType; return { type: 'input_image', ...(part.data instanceof URL ? { image_url: part.data.toString() } : typeof part.data === 'string' && part.data.startsWith('file-') ? { file_id: part.data } : { image_url: `data:${mediaType};base64,${part.data}`, }), detail: part.providerOptions?.openai?.imageDetail, }; } else if (part.mediaType === 'application/pdf') { if (part.data instanceof URL) { // The AI SDK automatically downloads files for user file parts with URLs throw new UnsupportedFunctionalityError({ functionality: 'PDF file parts with URLs', }); } return { type: 'input_file', ...(typeof part.data === 'string' && part.data.startsWith('file-') ? { file_id: part.data } : { filename: part.filename ?? `part-${index}.pdf`, file_data: `data:application/pdf;base64,${convertToBase64(part.data)}`, }), }; } else { throw new UnsupportedFunctionalityError({ functionality: `file part media type ${part.mediaType}`, }); } } } }), }); break; } case 'assistant': { const reasoningMessages: Record<string, OpenAIResponsesReasoning> = {}; for (const part of content) { switch (part.type) { case 'text': { messages.push({ role: 'assistant', content: [{ type: 'output_text', text: part.text }], id: (part.providerOptions?.openai?.itemId as string) ?? undefined, }); break; } case 'tool-call': { if (part.providerExecuted) { break; } messages.push({ type: 'function_call', call_id: part.toolCallId, name: part.toolName, arguments: JSON.stringify(part.input), id: (part.providerOptions?.openai?.itemId as string) ?? undefined, }); break; } case 'tool-result': { warnings.push({ type: 'other', message: `tool result parts in assistant messages are not supported for OpenAI responses`, }); break; } case 'reasoning': { const providerOptions = await parseProviderOptions({ provider: 'openai', providerOptions: part.providerOptions, schema: openaiResponsesReasoningProviderOptionsSchema, }); const reasoningId = providerOptions?.itemId; if (reasoningId != null) { const existingReasoningMessage = reasoningMessages[reasoningId]; const summaryParts: Array<{ type: 'summary_text'; text: string; }> = []; if (part.text.length > 0) { summaryParts.push({ type: 'summary_text', text: part.text }); } else if (existingReasoningMessage !== undefined) { warnings.push({ type: 'other', message: `Cannot append empty reasoning part to existing reasoning sequence. Skipping reasoning part: ${JSON.stringify(part)}.`, }); } if (existingReasoningMessage === undefined) { reasoningMessages[reasoningId] = { type: 'reasoning', id: reasoningId, encrypted_content: providerOptions?.reasoningEncryptedContent, summary: summaryParts, }; messages.push(reasoningMessages[reasoningId]); } else { existingReasoningMessage.summary.push(...summaryParts); } } else { warnings.push({ type: 'other', message: `Non-OpenAI reasoning parts are not supported. Skipping reasoning part: ${JSON.stringify(part)}.`, }); } break; } } } break; } case 'tool': { for (const part of content) { const output = part.output; let contentValue: string; switch (output.type) { case 'text': case 'error-text': contentValue = output.value; break; case 'content': case 'json': case 'error-json': contentValue = JSON.stringify(output.value); break; } messages.push({ type: 'function_call_output', call_id: part.toolCallId, output: contentValue, }); } break; } default: { const _exhaustiveCheck: never = role; throw new Error(`Unsupported role: ${_exhaustiveCheck}`); } } } return { messages, warnings }; } const openaiResponsesReasoningProviderOptionsSchema = z.object({ itemId: z.string().nullish(), reasoningEncryptedContent: z.string().nullish(), }); export type OpenAIResponsesReasoningProviderOptions = z.infer< typeof openaiResponsesReasoningProviderOptionsSchema >; --- File: /ai/packages/openai/src/responses/map-openai-responses-finish-reason.ts --- import { LanguageModelV2FinishReason } from '@ai-sdk/provider'; export function mapOpenAIResponseFinishReason({ finishReason, hasToolCalls, }: { finishReason: string | null | undefined; hasToolCalls: boolean; }): LanguageModelV2FinishReason { switch (finishReason) { case undefined: case null: return hasToolCalls ? 'tool-calls' : 'stop'; case 'max_output_tokens': return 'length'; case 'content_filter': return 'content-filter'; default: return hasToolCalls ? 'tool-calls' : 'unknown'; } } --- File: /ai/packages/openai/src/responses/openai-responses-api-types.ts --- import { JSONSchema7 } from '@ai-sdk/provider'; export type OpenAIResponsesPrompt = Array<OpenAIResponsesMessage>; export type OpenAIResponsesMessage = | OpenAIResponsesSystemMessage | OpenAIResponsesUserMessage | OpenAIResponsesAssistantMessage | OpenAIResponsesFunctionCall | OpenAIResponsesFunctionCallOutput | OpenAIWebSearchCall | OpenAIComputerCall | OpenAIFileSearchCall | OpenAIResponsesReasoning; export type OpenAIResponsesSystemMessage = { role: 'system' | 'developer'; content: string; }; export type OpenAIResponsesUserMessage = { role: 'user'; content: Array< | { type: 'input_text'; text: string } | { type: 'input_image'; image_url: string } | { type: 'input_image'; file_id: string } | { type: 'input_file'; filename: string; file_data: string } | { type: 'input_file'; file_id: string } >; }; export type OpenAIResponsesAssistantMessage = { role: 'assistant'; content: Array< | { type: 'output_text'; text: string } | OpenAIWebSearchCall | OpenAIComputerCall | OpenAIFileSearchCall >; id?: string; }; export type OpenAIResponsesFunctionCall = { type: 'function_call'; call_id: string; name: string; arguments: string; id?: string; }; export type OpenAIResponsesFunctionCallOutput = { type: 'function_call_output'; call_id: string; output: string; }; export type OpenAIWebSearchCall = { type: 'web_search_call'; id: string; status?: string; }; export type OpenAIComputerCall = { type: 'computer_call'; id: string; status?: string; }; export type OpenAIFileSearchCall = { type: 'file_search_call'; id: string; status?: string; }; export type OpenAIResponsesTool = | { type: 'function'; name: string; description: string | undefined; parameters: JSONSchema7; strict?: boolean; } | { type: 'web_search_preview'; search_context_size: 'low' | 'medium' | 'high'; user_location: { type: 'approximate'; city: string; region: string; }; } | { type: 'file_search'; vector_store_ids?: string[]; max_num_results?: number; ranking_options?: { ranker?: 'auto' | 'default-2024-08-21'; }; filters?: | { key: string; type: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte'; value: string | number | boolean; } | { type: 'and' | 'or'; filters: any[]; }; }; export type OpenAIResponsesReasoning = { type: 'reasoning'; id: string; encrypted_content?: string | null; summary: Array<{ type: 'summary_text'; text: string; }>; }; --- File: /ai/packages/openai/src/responses/openai-responses-language-model.test.ts --- import { LanguageModelV2FunctionTool, LanguageModelV2Prompt, } from '@ai-sdk/provider'; import { convertReadableStreamToArray, createTestServer, mockId, } from '@ai-sdk/provider-utils/test'; import { OpenAIResponsesLanguageModel } from './openai-responses-language-model'; import { openaiResponsesModelIds, openaiResponsesReasoningModelIds, } from './openai-responses-settings'; const TEST_PROMPT: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; const TEST_TOOLS: Array<LanguageModelV2FunctionTool> = [ { type: 'function', name: 'weather', inputSchema: { type: 'object', properties: { location: { type: 'string' } }, required: ['location'], additionalProperties: false, }, }, { type: 'function', name: 'cityAttractions', inputSchema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'], additionalProperties: false, }, }, ]; const nonReasoningModelIds = openaiResponsesModelIds.filter( modelId => !openaiResponsesReasoningModelIds.includes( modelId as (typeof openaiResponsesReasoningModelIds)[number], ), ); function createModel(modelId: string) { return new OpenAIResponsesLanguageModel(modelId, { provider: 'openai', url: ({ path }) => `https://api.openai.com/v1${path}`, headers: () => ({ Authorization: `Bearer APIKEY` }), generateId: mockId(), }); } describe('OpenAIResponsesLanguageModel', () => { const server = createTestServer({ 'https://api.openai.com/v1/responses': {}, }); describe('doGenerate', () => { describe('basic text response', () => { beforeEach(() => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'json-value', body: { id: 'resp_67c97c0203188190a025beb4a75242bc', object: 'response', created_at: 1741257730, status: 'completed', error: null, incomplete_details: null, input: [], instructions: null, max_output_tokens: null, model: 'gpt-4o-2024-07-18', output: [ { id: 'msg_67c97c02656c81908e080dfdf4a03cd1', type: 'message', status: 'completed', role: 'assistant', content: [ { type: 'output_text', text: 'answer text', annotations: [], }, ], }, ], parallel_tool_calls: true, previous_response_id: null, reasoning: { effort: null, summary: null, }, store: true, temperature: 1, text: { format: { type: 'text', }, }, tool_choice: 'auto', tools: [], top_p: 1, truncation: 'disabled', usage: { input_tokens: 345, input_tokens_details: { cached_tokens: 234, }, output_tokens: 538, output_tokens_details: { reasoning_tokens: 123, }, total_tokens: 572, }, user: null, metadata: {}, }, }; }); it('should generate text', async () => { const result = await createModel('gpt-4o').doGenerate({ prompt: TEST_PROMPT, }); expect(result.content).toMatchInlineSnapshot(` [ { "providerMetadata": { "openai": { "itemId": "msg_67c97c02656c81908e080dfdf4a03cd1", }, }, "text": "answer text", "type": "text", }, ] `); }); it('should extract usage', async () => { const result = await createModel('gpt-4o').doGenerate({ prompt: TEST_PROMPT, }); expect(result.usage).toMatchInlineSnapshot(` { "cachedInputTokens": 234, "inputTokens": 345, "outputTokens": 538, "reasoningTokens": 123, "totalTokens": 883, } `); }); it('should extract response id metadata ', async () => { const result = await createModel('gpt-4o').doGenerate({ prompt: TEST_PROMPT, }); expect(result.providerMetadata).toStrictEqual({ openai: { responseId: 'resp_67c97c0203188190a025beb4a75242bc', }, }); }); it('should send model id, settings, and input', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ prompt: [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ], temperature: 0.5, topP: 0.3, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o', temperature: 0.5, top_p: 0.3, input: [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ], }); expect(warnings).toStrictEqual([]); }); it('should remove unsupported settings for o1', async () => { const { warnings } = await createModel('o1-mini').doGenerate({ prompt: [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ], temperature: 0.5, topP: 0.3, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'o1-mini', input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ], }); expect(warnings).toStrictEqual([ { type: 'other', message: 'system messages are removed for this model', }, { details: 'temperature is not supported for reasoning models', setting: 'temperature', type: 'unsupported-setting', }, { details: 'topP is not supported for reasoning models', setting: 'topP', type: 'unsupported-setting', }, ]); }); it.each(openaiResponsesReasoningModelIds)( 'should remove and warn about unsupported settings for reasoning model %s', async modelId => { const { warnings } = await createModel(modelId).doGenerate({ prompt: [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ], temperature: 0.5, topP: 0.3, }); const expectedMessages = [ // o1 models prior to o1-2024-12-17 should remove system messages, all other models should replace // them with developer messages ...(![ 'o1-mini', 'o1-mini-2024-09-12', 'o1-preview', 'o1-preview-2024-09-12', ].includes(modelId) ? [ { role: 'developer', content: 'You are a helpful assistant.', }, ] : []), { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ]; expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: modelId, input: expectedMessages, }); expect(warnings).toStrictEqual([ // o1 models prior to o1-2024-12-17 should remove system messages, all other models should replace // them with developer messages ...([ 'o1-mini', 'o1-mini-2024-09-12', 'o1-preview', 'o1-preview-2024-09-12', ].includes(modelId) ? [ { message: 'system messages are removed for this model', type: 'other', }, ] : []), { details: 'temperature is not supported for reasoning models', setting: 'temperature', type: 'unsupported-setting', }, { details: 'topP is not supported for reasoning models', setting: 'topP', type: 'unsupported-setting', }, ]); }, ); it('should send response format json schema', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ prompt: TEST_PROMPT, responseFormat: { type: 'json', name: 'response', description: 'A response', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "input": [ { "content": [ { "text": "Hello", "type": "input_text", }, ], "role": "user", }, ], "model": "gpt-4o", "text": { "format": { "description": "A response", "name": "response", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "value": { "type": "string", }, }, "required": [ "value", ], "type": "object", }, "strict": false, "type": "json_schema", }, }, } `); expect(warnings).toStrictEqual([]); }); it('should send response format json object', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ prompt: TEST_PROMPT, responseFormat: { type: 'json', }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o', text: { format: { type: 'json_object', }, }, input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ], }); expect(warnings).toStrictEqual([]); }); it('should send parallelToolCalls provider option', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { parallelToolCalls: false, }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o', input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ], parallel_tool_calls: false, }); expect(warnings).toStrictEqual([]); }); it('should send store provider option', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { store: false, }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o', input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ], store: false, }); expect(warnings).toStrictEqual([]); }); it('should send user provider option', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { store: false, }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o', input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ], store: false, }); expect(warnings).toStrictEqual([]); }); it('should send previous response id provider option', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { previousResponseId: 'resp_123', }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o', input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ], previous_response_id: 'resp_123', }); expect(warnings).toStrictEqual([]); }); it('should send metadata provider option', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { user: 'user_123', }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o', input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ], user: 'user_123', }); expect(warnings).toStrictEqual([]); }); it.each(openaiResponsesReasoningModelIds)( 'should send reasoningEffort and reasoningSummary provider options for %s', async modelId => { const { warnings } = await createModel(modelId).doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { reasoningEffort: 'low', reasoningSummary: 'auto', }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: modelId, input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }], }, ], reasoning: { effort: 'low', summary: 'auto', }, }); expect(warnings).toStrictEqual([]); }, ); it.each(nonReasoningModelIds)( 'should not send and warn about unsupported reasoningEffort and reasoningSummary provider options for %s', async modelId => { const { warnings } = await createModel(modelId).doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { reasoningEffort: 'low', }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: modelId, input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }], }, ], }); expect(warnings).toStrictEqual([ { type: 'unsupported-setting', setting: 'reasoningEffort', details: 'reasoningEffort is not supported for non-reasoning models', }, ]); }, ); it('should send instructions provider option', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { instructions: 'You are a friendly assistant.', }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o', input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ], instructions: 'You are a friendly assistant.', }); expect(warnings).toStrictEqual([]); }); it('should send include provider option', async () => { const { warnings } = await createModel('o3-mini').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { include: ['reasoning.encrypted_content'], }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'o3-mini', input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ], include: ['reasoning.encrypted_content'], }); expect(warnings).toStrictEqual([]); }); it('should send include provider option for file search results', async () => { const { warnings } = await createModel('gpt-4o-mini').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { include: ['file_search_call.results'], }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o-mini', input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ], include: ['file_search_call.results'], }); expect(warnings).toStrictEqual([]); }); it('should send include provider option with multiple values', async () => { const { warnings } = await createModel('o3-mini').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { include: [ 'reasoning.encrypted_content', 'file_search_call.results', ], }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'o3-mini', input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ], include: ['reasoning.encrypted_content', 'file_search_call.results'], }); expect(warnings).toStrictEqual([]); }); it('should send responseFormat json format', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ responseFormat: { type: 'json' }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o', text: { format: { type: 'json_object' } }, input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ], }); expect(warnings).toStrictEqual([]); }); it('should send responseFormat json_schema format', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ responseFormat: { type: 'json', name: 'response', description: 'A response', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "input": [ { "content": [ { "text": "Hello", "type": "input_text", }, ], "role": "user", }, ], "model": "gpt-4o", "text": { "format": { "description": "A response", "name": "response", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "value": { "type": "string", }, }, "required": [ "value", ], "type": "object", }, "strict": false, "type": "json_schema", }, }, } `); expect(warnings).toStrictEqual([]); }); it('should send responseFormat json_schema format with strictSchemas false', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ responseFormat: { type: 'json', name: 'response', description: 'A response', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, prompt: TEST_PROMPT, providerOptions: { openai: { strictSchemas: false, }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o', text: { format: { type: 'json_schema', strict: false, name: 'response', description: 'A response', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }, input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }], }, ], }); expect(warnings).toStrictEqual([]); }); it('should send web_search tool', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ tools: [ { type: 'provider-defined', id: 'openai.web_search_preview', name: 'web_search_preview', args: { searchContextSize: 'high', userLocation: { type: 'approximate', city: 'San Francisco', }, }, }, ], prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o', tools: [ { type: 'web_search_preview', search_context_size: 'high', user_location: { type: 'approximate', city: 'San Francisco' }, }, ], input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ], }); expect(warnings).toStrictEqual([]); }); it('should send web_search tool as tool_choice', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ toolChoice: { type: 'tool', toolName: 'web_search_preview', }, tools: [ { type: 'provider-defined', id: 'openai.web_search_preview', name: 'web_search_preview', args: { searchContextSize: 'high', userLocation: { type: 'approximate', city: 'San Francisco', }, }, }, ], prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o', tool_choice: { type: 'web_search_preview' }, tools: [ { type: 'web_search_preview', search_context_size: 'high', user_location: { type: 'approximate', city: 'San Francisco' }, }, ], input: [ { role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }, ], }); expect(warnings).toStrictEqual([]); }); it('should send file_search tool', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ tools: [ { type: 'provider-defined', id: 'openai.file_search', name: 'file_search', args: { vectorStoreIds: ['vs_123', 'vs_456'], maxNumResults: 10, ranking: { ranker: 'auto', }, }, }, ], prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "input": [ { "content": [ { "text": "Hello", "type": "input_text", }, ], "role": "user", }, ], "model": "gpt-4o", "tools": [ { "max_num_results": 10, "ranking_options": { "ranker": "auto", }, "type": "file_search", "vector_store_ids": [ "vs_123", "vs_456", ], }, ], } `); expect(warnings).toStrictEqual([]); }); it('should send file_search tool as tool_choice', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ toolChoice: { type: 'tool', toolName: 'file_search', }, tools: [ { type: 'provider-defined', id: 'openai.file_search', name: 'file_search', args: { vectorStoreIds: ['vs_789'], maxNumResults: 5, }, }, ], prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "input": [ { "content": [ { "text": "Hello", "type": "input_text", }, ], "role": "user", }, ], "model": "gpt-4o", "tool_choice": { "type": "file_search", }, "tools": [ { "max_num_results": 5, "type": "file_search", "vector_store_ids": [ "vs_789", ], }, ], } `); expect(warnings).toStrictEqual([]); }); it('should send file_search tool with filters', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ tools: [ { type: 'provider-defined', id: 'openai.file_search', name: 'file_search', args: { vectorStoreIds: ['vs_123'], maxNumResults: 5, filters: { key: 'author', type: 'eq', value: 'Jane Smith', }, }, }, ], prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "input": [ { "content": [ { "text": "Hello", "type": "input_text", }, ], "role": "user", }, ], "model": "gpt-4o", "tools": [ { "filters": { "key": "author", "type": "eq", "value": "Jane Smith", }, "max_num_results": 5, "type": "file_search", "vector_store_ids": [ "vs_123", ], }, ], } `); expect(warnings).toStrictEqual([]); }); it('should send file_search tool with minimal args', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ tools: [ { type: 'provider-defined', id: 'openai.file_search', name: 'file_search', args: {}, }, ], prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "input": [ { "content": [ { "text": "Hello", "type": "input_text", }, ], "role": "user", }, ], "model": "gpt-4o", "tools": [ { "type": "file_search", }, ], } `); expect(warnings).toStrictEqual([]); }); it('should warn about unsupported settings', async () => { const { warnings } = await createModel('gpt-4o').doGenerate({ prompt: TEST_PROMPT, stopSequences: ['\n\n'], topK: 0.1, presencePenalty: 0, frequencyPenalty: 0, seed: 42, }); expect(warnings).toStrictEqual([ { type: 'unsupported-setting', setting: 'topK' }, { type: 'unsupported-setting', setting: 'seed' }, { type: 'unsupported-setting', setting: 'presencePenalty' }, { type: 'unsupported-setting', setting: 'frequencyPenalty' }, { type: 'unsupported-setting', setting: 'stopSequences' }, ]); }); }); describe('reasoning', () => { it('should handle reasoning with summary', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'json-value', body: { id: 'resp_67c97c0203188190a025beb4a75242bc', object: 'response', created_at: 1741257730, status: 'completed', error: null, incomplete_details: null, input: [], instructions: null, max_output_tokens: null, model: 'o3-mini-2025-01-31', output: [ { id: 'rs_6808709f6fcc8191ad2e2fdd784017b3', type: 'reasoning', summary: [ { type: 'summary_text', text: '**Exploring burrito origins**\n\nThe user is curious about the debate regarding Taqueria La Cumbre and El Farolito.', }, { type: 'summary_text', text: "**Investigating burrito origins**\n\nThere's a fascinating debate about who created the Mission burrito.", }, ], }, { id: 'msg_67c97c02656c81908e080dfdf4a03cd1', type: 'message', status: 'completed', role: 'assistant', content: [ { type: 'output_text', text: 'answer text', annotations: [], }, ], }, ], parallel_tool_calls: true, previous_response_id: null, reasoning: { effort: 'low', summary: 'auto', }, store: true, temperature: 1, text: { format: { type: 'text', }, }, tool_choice: 'auto', tools: [], top_p: 1, truncation: 'disabled', usage: { input_tokens: 34, input_tokens_details: { cached_tokens: 0, }, output_tokens: 538, output_tokens_details: { reasoning_tokens: 320, }, total_tokens: 572, }, user: null, metadata: {}, }, }; const result = await createModel('o3-mini').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { reasoningEffort: 'low', reasoningSummary: 'auto', }, }, }); expect(result.content).toMatchInlineSnapshot(` [ { "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": null, }, }, "text": "**Exploring burrito origins** The user is curious about the debate regarding Taqueria La Cumbre and El Farolito.", "type": "reasoning", }, { "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": null, }, }, "text": "**Investigating burrito origins** There's a fascinating debate about who created the Mission burrito.", "type": "reasoning", }, { "providerMetadata": { "openai": { "itemId": "msg_67c97c02656c81908e080dfdf4a03cd1", }, }, "text": "answer text", "type": "text", }, ] `); expect(await server.calls[0].requestBodyJson).toMatchObject({ model: 'o3-mini', reasoning: { effort: 'low', summary: 'auto', }, }); }); it('should handle reasoning with empty summary', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'json-value', body: { id: 'resp_67c97c0203188190a025beb4a75242bc', object: 'response', created_at: 1741257730, status: 'completed', error: null, incomplete_details: null, input: [], instructions: null, max_output_tokens: null, model: 'o3-mini-2025-01-31', output: [ { id: 'rs_6808709f6fcc8191ad2e2fdd784017b3', type: 'reasoning', summary: [], }, { id: 'msg_67c97c02656c81908e080dfdf4a03cd1', type: 'message', status: 'completed', role: 'assistant', content: [ { type: 'output_text', text: 'answer text', annotations: [], }, ], }, ], parallel_tool_calls: true, previous_response_id: null, reasoning: { effort: 'low', summary: 'auto', }, store: true, temperature: 1, text: { format: { type: 'text', }, }, tool_choice: 'auto', tools: [], top_p: 1, truncation: 'disabled', usage: { input_tokens: 34, input_tokens_details: { cached_tokens: 0, }, output_tokens: 538, output_tokens_details: { reasoning_tokens: 320, }, total_tokens: 572, }, user: null, metadata: {}, }, }; const result = await createModel('o3-mini').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { reasoningEffort: 'low', reasoningSummary: null, }, }, }); expect(result.content).toMatchInlineSnapshot(` [ { "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": null, }, }, "text": "", "type": "reasoning", }, { "providerMetadata": { "openai": { "itemId": "msg_67c97c02656c81908e080dfdf4a03cd1", }, }, "text": "answer text", "type": "text", }, ] `); expect(await server.calls[0].requestBodyJson).toMatchObject({ model: 'o3-mini', reasoning: { effort: 'low', }, }); }); it('should handle encrypted content with summary', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'json-value', body: { id: 'resp_67c97c0203188190a025beb4a75242bc', object: 'response', created_at: 1741257730, status: 'completed', error: null, incomplete_details: null, input: [], instructions: null, max_output_tokens: null, model: 'o3-mini-2025-01-31', output: [ { id: 'rs_6808709f6fcc8191ad2e2fdd784017b3', type: 'reasoning', encrypted_content: 'encrypted_reasoning_data_abc123', summary: [ { type: 'summary_text', text: '**Exploring burrito origins**\n\nThe user is curious about the debate regarding Taqueria La Cumbre and El Farolito.', }, { type: 'summary_text', text: "**Investigating burrito origins**\n\nThere's a fascinating debate about who created the Mission burrito.", }, ], }, { id: 'msg_67c97c02656c81908e080dfdf4a03cd1', type: 'message', status: 'completed', role: 'assistant', content: [ { type: 'output_text', text: 'answer text', annotations: [], }, ], }, ], parallel_tool_calls: true, previous_response_id: null, reasoning: { effort: 'low', summary: 'auto', }, store: true, temperature: 1, text: { format: { type: 'text', }, }, tool_choice: 'auto', tools: [], top_p: 1, truncation: 'disabled', usage: { input_tokens: 34, input_tokens_details: { cached_tokens: 0, }, output_tokens: 538, output_tokens_details: { reasoning_tokens: 320, }, total_tokens: 572, }, user: null, metadata: {}, }, }; const result = await createModel('o3-mini').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { reasoningEffort: 'low', reasoningSummary: 'auto', include: ['reasoning.encrypted_content'], }, }, }); expect(result.content).toMatchInlineSnapshot(` [ { "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": "encrypted_reasoning_data_abc123", }, }, "text": "**Exploring burrito origins** The user is curious about the debate regarding Taqueria La Cumbre and El Farolito.", "type": "reasoning", }, { "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": "encrypted_reasoning_data_abc123", }, }, "text": "**Investigating burrito origins** There's a fascinating debate about who created the Mission burrito.", "type": "reasoning", }, { "providerMetadata": { "openai": { "itemId": "msg_67c97c02656c81908e080dfdf4a03cd1", }, }, "text": "answer text", "type": "text", }, ] `); expect(await server.calls[0].requestBodyJson).toMatchObject({ model: 'o3-mini', reasoning: { effort: 'low', summary: 'auto', }, include: ['reasoning.encrypted_content'], }); }); it('should handle encrypted content with empty summary', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'json-value', body: { id: 'resp_67c97c0203188190a025beb4a75242bc', object: 'response', created_at: 1741257730, status: 'completed', error: null, incomplete_details: null, input: [], instructions: null, max_output_tokens: null, model: 'o3-mini-2025-01-31', output: [ { id: 'rs_6808709f6fcc8191ad2e2fdd784017b3', type: 'reasoning', encrypted_content: 'encrypted_reasoning_data_abc123', summary: [], }, { id: 'msg_67c97c02656c81908e080dfdf4a03cd1', type: 'message', status: 'completed', role: 'assistant', content: [ { type: 'output_text', text: 'answer text', annotations: [], }, ], }, ], parallel_tool_calls: true, previous_response_id: null, reasoning: { effort: 'low', summary: 'auto', }, store: true, temperature: 1, text: { format: { type: 'text', }, }, tool_choice: 'auto', tools: [], top_p: 1, truncation: 'disabled', usage: { input_tokens: 34, input_tokens_details: { cached_tokens: 0, }, output_tokens: 538, output_tokens_details: { reasoning_tokens: 320, }, total_tokens: 572, }, user: null, metadata: {}, }, }; const result = await createModel('o3-mini').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { reasoningEffort: 'low', reasoningSummary: null, include: ['reasoning.encrypted_content'], }, }, }); expect(result.content).toMatchInlineSnapshot(` [ { "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": "encrypted_reasoning_data_abc123", }, }, "text": "", "type": "reasoning", }, { "providerMetadata": { "openai": { "itemId": "msg_67c97c02656c81908e080dfdf4a03cd1", }, }, "text": "answer text", "type": "text", }, ] `); expect(await server.calls[0].requestBodyJson).toMatchObject({ model: 'o3-mini', reasoning: { effort: 'low', }, include: ['reasoning.encrypted_content'], }); }); it('should handle multiple reasoning blocks', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'json-value', body: { id: 'resp_67c97c0203188190a025beb4a75242bc', object: 'response', created_at: 1741257730, status: 'completed', error: null, incomplete_details: null, input: [], instructions: null, max_output_tokens: null, model: 'o3-mini-2025-01-31', output: [ { id: 'rs_first_6808709f6fcc8191ad2e2fdd784017b3', type: 'reasoning', summary: [ { type: 'summary_text', text: '**Initial analysis**\n\nFirst reasoning block: analyzing the problem structure.', }, { type: 'summary_text', text: '**Deeper consideration**\n\nLet me think about the various approaches available.', }, ], }, { id: 'msg_67c97c02656c81908e080dfdf4a03cd1', type: 'message', status: 'completed', role: 'assistant', content: [ { type: 'output_text', text: 'Let me think about this step by step.', annotations: [], }, ], }, { id: 'rs_second_7908809g7gcc9291be3e3fee895028c4', type: 'reasoning', summary: [ { type: 'summary_text', text: 'Second reasoning block: considering alternative approaches.', }, ], }, { id: 'msg_final_78d08d03767d92908f25523f5ge51e77', type: 'message', status: 'completed', role: 'assistant', content: [ { type: 'output_text', text: 'Based on my analysis, here is the solution.', annotations: [], }, ], }, ], parallel_tool_calls: true, previous_response_id: null, reasoning: { effort: 'medium', summary: 'auto', }, store: true, temperature: null, text: { format: { type: 'text', }, }, tool_choice: 'auto', tools: [], top_p: null, truncation: 'disabled', usage: { input_tokens: 45, input_tokens_details: { cached_tokens: 0, }, output_tokens: 628, output_tokens_details: { reasoning_tokens: 420, }, total_tokens: 673, }, user: null, metadata: {}, }, }; const result = await createModel('o3-mini').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { reasoningEffort: 'medium', reasoningSummary: 'auto', }, }, }); expect(result.content).toMatchInlineSnapshot(` [ { "providerMetadata": { "openai": { "itemId": "rs_first_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": null, }, }, "text": "**Initial analysis** First reasoning block: analyzing the problem structure.", "type": "reasoning", }, { "providerMetadata": { "openai": { "itemId": "rs_first_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": null, }, }, "text": "**Deeper consideration** Let me think about the various approaches available.", "type": "reasoning", }, { "providerMetadata": { "openai": { "itemId": "msg_67c97c02656c81908e080dfdf4a03cd1", }, }, "text": "Let me think about this step by step.", "type": "text", }, { "providerMetadata": { "openai": { "itemId": "rs_second_7908809g7gcc9291be3e3fee895028c4", "reasoningEncryptedContent": null, }, }, "text": "Second reasoning block: considering alternative approaches.", "type": "reasoning", }, { "providerMetadata": { "openai": { "itemId": "msg_final_78d08d03767d92908f25523f5ge51e77", }, }, "text": "Based on my analysis, here is the solution.", "type": "text", }, ] `); expect(await server.calls[0].requestBodyJson).toMatchObject({ model: 'o3-mini', reasoning: { effort: 'medium', summary: 'auto', }, }); }); }); describe('tool calls', () => { beforeEach(() => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'json-value', body: { id: 'resp_67c97c0203188190a025beb4a75242bc', object: 'response', created_at: 1741257730, status: 'completed', error: null, incomplete_details: null, input: [], instructions: null, max_output_tokens: null, model: 'gpt-4o-2024-07-18', output: [ { type: 'function_call', id: 'fc_67caf7f4c1ec8190b27edfb5580cfd31', call_id: 'call_0NdsJqOS8N3J9l2p0p4WpYU9', name: 'weather', arguments: '{"location":"San Francisco"}', status: 'completed', }, { type: 'function_call', id: 'fc_67caf7f5071c81908209c2909c77af05', call_id: 'call_gexo0HtjUfmAIW4gjNOgyrcr', name: 'cityAttractions', arguments: '{"city":"San Francisco"}', status: 'completed', }, ], parallel_tool_calls: true, previous_response_id: null, reasoning: { effort: null, summary: null, }, store: true, temperature: 1, text: { format: { type: 'text', }, }, tool_choice: 'auto', tools: [ { type: 'function', description: 'Get the weather in a location', name: 'weather', parameters: { type: 'object', properties: { location: { type: 'string', description: 'The location to get the weather for', }, }, required: ['location'], additionalProperties: false, }, strict: true, }, { type: 'function', description: null, name: 'cityAttractions', parameters: { type: 'object', properties: { city: { type: 'string', }, }, required: ['city'], additionalProperties: false, }, strict: true, }, ], top_p: 1, truncation: 'disabled', usage: { input_tokens: 34, output_tokens: 538, output_tokens_details: { reasoning_tokens: 0, }, total_tokens: 572, }, user: null, metadata: {}, }, }; }); it('should generate tool calls', async () => { const result = await createModel('gpt-4o').doGenerate({ prompt: TEST_PROMPT, tools: TEST_TOOLS, }); expect(result.content).toMatchInlineSnapshot(` [ { "input": "{"location":"San Francisco"}", "providerMetadata": { "openai": { "itemId": "fc_67caf7f4c1ec8190b27edfb5580cfd31", }, }, "toolCallId": "call_0NdsJqOS8N3J9l2p0p4WpYU9", "toolName": "weather", "type": "tool-call", }, { "input": "{"city":"San Francisco"}", "providerMetadata": { "openai": { "itemId": "fc_67caf7f5071c81908209c2909c77af05", }, }, "toolCallId": "call_gexo0HtjUfmAIW4gjNOgyrcr", "toolName": "cityAttractions", "type": "tool-call", }, ] `); }); it('should have tool-calls finish reason', async () => { const result = await createModel('gpt-4o').doGenerate({ prompt: TEST_PROMPT, tools: TEST_TOOLS, }); expect(result.finishReason).toStrictEqual('tool-calls'); }); }); describe('web search', () => { const outputText = `Last week in San Francisco, several notable events and developments took place:\n\n**Bruce Lee Statue in Chinatown**\n\nThe Chinese Historical Society of America Museum announced plans to install a Bruce Lee statue in Chinatown. This initiative, supported by the Rose Pak Community Fund, the Bruce Lee Foundation, and Stand With Asians, aims to honor Lee's contributions to film and martial arts. Artist Arnie Kim has been commissioned for the project, with a fundraising goal of $150,000. ([axios.com](https://www.axios.com/local/san-francisco/2025/03/07/bruce-lee-statue-sf-chinatown?utm_source=chatgpt.com))\n\n**Office Leasing Revival**\n\nThe Bay Area experienced a resurgence in office leasing, securing 11 of the largest U.S. office leases in 2024. This trend, driven by the tech industry's growth and advancements in generative AI, suggests a potential boost to downtown recovery through increased foot traffic. ([axios.com](https://www.axios.com/local/san-francisco/2025/03/03/bay-area-office-leasing-activity?utm_source=chatgpt.com))\n\n**Spring Blooms in the Bay Area**\n\nWith the arrival of spring, several locations in the Bay Area are showcasing vibrant blooms. Notable spots include the Conservatory of Flowers, Japanese Tea Garden, Queen Wilhelmina Tulip Garden, and the San Francisco Botanical Garden, each offering unique floral displays. ([axios.com](https://www.axios.com/local/san-francisco/2025/03/03/where-to-see-spring-blooms-bay-area?utm_source=chatgpt.com))\n\n**Oceanfront Great Highway Park**\n\nSan Francisco's long-awaited Oceanfront Great Highway park is set to open on April 12. This 43-acre, car-free park will span a two-mile stretch of the Great Highway from Lincoln Way to Sloat Boulevard, marking the largest pedestrianization project in California's history. The park follows voter approval of Proposition K, which permanently bans cars on part of the highway. ([axios.com](https://www.axios.com/local/san-francisco/2025/03/03/great-highway-park-opening-april-recall-campaign?utm_source=chatgpt.com))\n\n**Warmer Spring Seasons**\n\nAn analysis by Climate Central revealed that San Francisco, along with most U.S. cities, is experiencing increasingly warmer spring seasons. Over a 55-year period from 1970 to 2024, the national average temperature during March through May rose by 2.4°F. This warming trend poses various risks, including early snowmelt and increased wildfire threats. ([axios.com](https://www.axios.com/local/san-francisco/2025/03/03/climate-weather-spring-temperatures-warmer-sf?utm_source=chatgpt.com))\n\n\n# Key San Francisco Developments Last Week:\n- [Bruce Lee statue to be installed in SF Chinatown](https://www.axios.com/local/san-francisco/2025/03/07/bruce-lee-statue-sf-chinatown?utm_source=chatgpt.com)\n- [The Bay Area is set to make an office leasing comeback](https://www.axios.com/local/san-francisco/2025/03/03/bay-area-office-leasing-activity?utm_source=chatgpt.com)\n- [Oceanfront Great Highway park set to open in April](https://www.axios.com/local/san-francisco/2025/03/03/great-highway-park-opening-april-recall-campaign?utm_source=chatgpt.com)`; beforeEach(() => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'json-value', body: { id: 'resp_67cf2b2f6bd081909be2c8054ddef0eb', object: 'response', created_at: 1741630255, status: 'completed', error: null, incomplete_details: null, instructions: null, max_output_tokens: null, model: 'gpt-4o-2024-07-18', output: [ { type: 'web_search_call', id: 'ws_67cf2b3051e88190b006770db6fdb13d', status: 'completed', }, { type: 'message', id: 'msg_67cf2b35467481908f24412e4fd40d66', status: 'completed', role: 'assistant', content: [ { type: 'output_text', text: outputText, annotations: [ { type: 'url_citation', start_index: 486, end_index: 606, url: 'https://www.axios.com/local/san-francisco/2025/03/07/bruce-lee-statue-sf-chinatown?utm_source=chatgpt.com', title: 'Bruce Lee statue to be installed in SF Chinatown', }, { type: 'url_citation', start_index: 912, end_index: 1035, url: 'https://www.axios.com/local/san-francisco/2025/03/03/bay-area-office-leasing-activity?utm_source=chatgpt.com', title: 'The Bay Area is set to make an office leasing comeback', }, { type: 'url_citation', start_index: 1346, end_index: 1472, url: 'https://www.axios.com/local/san-francisco/2025/03/03/where-to-see-spring-blooms-bay-area?utm_source=chatgpt.com', title: 'Where to see spring blooms in the Bay Area', }, { type: 'url_citation', start_index: 1884, end_index: 2023, url: 'https://www.axios.com/local/san-francisco/2025/03/03/great-highway-park-opening-april-recall-campaign?utm_source=chatgpt.com', title: 'Oceanfront Great Highway park set to open in April', }, { type: 'url_citation', start_index: 2404, end_index: 2540, url: 'https://www.axios.com/local/san-francisco/2025/03/03/climate-weather-spring-temperatures-warmer-sf?utm_source=chatgpt.com', title: "San Francisco's spring seasons are getting warmer", }, ], }, ], }, ], parallel_tool_calls: true, previous_response_id: null, reasoning: { effort: null, summary: null, }, store: true, temperature: 0, text: { format: { type: 'text', }, }, tool_choice: 'auto', tools: [ { type: 'web_search_preview', search_context_size: 'medium', user_location: { type: 'approximate', city: null, country: 'US', region: null, timezone: null, }, }, ], top_p: 1, truncation: 'disabled', usage: { input_tokens: 327, input_tokens_details: { cached_tokens: 0, }, output_tokens: 770, output_tokens_details: { reasoning_tokens: 0, }, total_tokens: 1097, }, user: null, metadata: {}, }, }; }); it('should generate text and sources', async () => { const result = await createModel('gpt-4o').doGenerate({ prompt: TEST_PROMPT, }); expect(result.content).toMatchInlineSnapshot(` [ { "input": "", "providerExecuted": true, "toolCallId": "ws_67cf2b3051e88190b006770db6fdb13d", "toolName": "web_search_preview", "type": "tool-call", }, { "providerExecuted": true, "result": { "status": "completed", }, "toolCallId": "ws_67cf2b3051e88190b006770db6fdb13d", "toolName": "web_search_preview", "type": "tool-result", }, { "providerMetadata": { "openai": { "itemId": "msg_67cf2b35467481908f24412e4fd40d66", }, }, "text": "Last week in San Francisco, several notable events and developments took place: **Bruce Lee Statue in Chinatown** The Chinese Historical Society of America Museum announced plans to install a Bruce Lee statue in Chinatown. This initiative, supported by the Rose Pak Community Fund, the Bruce Lee Foundation, and Stand With Asians, aims to honor Lee's contributions to film and martial arts. Artist Arnie Kim has been commissioned for the project, with a fundraising goal of $150,000. ([axios.com](https://www.axios.com/local/san-francisco/2025/03/07/bruce-lee-statue-sf-chinatown?utm_source=chatgpt.com)) **Office Leasing Revival** The Bay Area experienced a resurgence in office leasing, securing 11 of the largest U.S. office leases in 2024. This trend, driven by the tech industry's growth and advancements in generative AI, suggests a potential boost to downtown recovery through increased foot traffic. ([axios.com](https://www.axios.com/local/san-francisco/2025/03/03/bay-area-office-leasing-activity?utm_source=chatgpt.com)) **Spring Blooms in the Bay Area** With the arrival of spring, several locations in the Bay Area are showcasing vibrant blooms. Notable spots include the Conservatory of Flowers, Japanese Tea Garden, Queen Wilhelmina Tulip Garden, and the San Francisco Botanical Garden, each offering unique floral displays. ([axios.com](https://www.axios.com/local/san-francisco/2025/03/03/where-to-see-spring-blooms-bay-area?utm_source=chatgpt.com)) **Oceanfront Great Highway Park** San Francisco's long-awaited Oceanfront Great Highway park is set to open on April 12. This 43-acre, car-free park will span a two-mile stretch of the Great Highway from Lincoln Way to Sloat Boulevard, marking the largest pedestrianization project in California's history. The park follows voter approval of Proposition K, which permanently bans cars on part of the highway. ([axios.com](https://www.axios.com/local/san-francisco/2025/03/03/great-highway-park-opening-april-recall-campaign?utm_source=chatgpt.com)) **Warmer Spring Seasons** An analysis by Climate Central revealed that San Francisco, along with most U.S. cities, is experiencing increasingly warmer spring seasons. Over a 55-year period from 1970 to 2024, the national average temperature during March through May rose by 2.4°F. This warming trend poses various risks, including early snowmelt and increased wildfire threats. ([axios.com](https://www.axios.com/local/san-francisco/2025/03/03/climate-weather-spring-temperatures-warmer-sf?utm_source=chatgpt.com)) # Key San Francisco Developments Last Week: - [Bruce Lee statue to be installed in SF Chinatown](https://www.axios.com/local/san-francisco/2025/03/07/bruce-lee-statue-sf-chinatown?utm_source=chatgpt.com) - [The Bay Area is set to make an office leasing comeback](https://www.axios.com/local/san-francisco/2025/03/03/bay-area-office-leasing-activity?utm_source=chatgpt.com) - [Oceanfront Great Highway park set to open in April](https://www.axios.com/local/san-francisco/2025/03/03/great-highway-park-opening-april-recall-campaign?utm_source=chatgpt.com)", "type": "text", }, { "id": "id-0", "sourceType": "url", "title": "Bruce Lee statue to be installed in SF Chinatown", "type": "source", "url": "https://www.axios.com/local/san-francisco/2025/03/07/bruce-lee-statue-sf-chinatown?utm_source=chatgpt.com", }, { "id": "id-1", "sourceType": "url", "title": "The Bay Area is set to make an office leasing comeback", "type": "source", "url": "https://www.axios.com/local/san-francisco/2025/03/03/bay-area-office-leasing-activity?utm_source=chatgpt.com", }, { "id": "id-2", "sourceType": "url", "title": "Where to see spring blooms in the Bay Area", "type": "source", "url": "https://www.axios.com/local/san-francisco/2025/03/03/where-to-see-spring-blooms-bay-area?utm_source=chatgpt.com", }, { "id": "id-3", "sourceType": "url", "title": "Oceanfront Great Highway park set to open in April", "type": "source", "url": "https://www.axios.com/local/san-francisco/2025/03/03/great-highway-park-opening-april-recall-campaign?utm_source=chatgpt.com", }, { "id": "id-4", "sourceType": "url", "title": "San Francisco's spring seasons are getting warmer", "type": "source", "url": "https://www.axios.com/local/san-francisco/2025/03/03/climate-weather-spring-temperatures-warmer-sf?utm_source=chatgpt.com", }, ] `); }); }); describe('errors', () => { it('should throw an API call error when the response contains an error part', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'json-value', body: { id: 'resp_67c97c0203188190a025beb4a75242bc', object: 'response', created_at: 1741257730, status: 'completed', error: { code: 'ERR_SOMETHING', message: 'Something went wrong', }, incomplete_details: null, input: [], instructions: null, max_output_tokens: null, model: 'gpt-4o-2024-07-18', output: [], parallel_tool_calls: true, previous_response_id: null, reasoning: { effort: null, summary: null, }, store: true, temperature: 1, text: { format: { type: 'text', }, }, tool_choice: 'auto', tools: [], top_p: 1, truncation: 'disabled', usage: { input_tokens: 345, input_tokens_details: { cached_tokens: 234, }, output_tokens: 538, output_tokens_details: { reasoning_tokens: 123, }, total_tokens: 572, }, user: null, metadata: {}, }, }; expect( createModel('gpt-4o').doGenerate({ prompt: TEST_PROMPT, }), ).rejects.toThrow('Something went wrong'); }); }); }); describe('doStream', () => { it('should stream text deltas', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'stream-chunks', chunks: [ `data:{"type":"response.created","response":{"id":"resp_67c9a81b6a048190a9ee441c5755a4e8","object":"response","created_at":1741269019,"status":"in_progress","error":null,"incomplete_details":null,"input":[],"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":0.3,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}\n\n`, `data:{"type":"response.in_progress","response":{"id":"resp_67c9a81b6a048190a9ee441c5755a4e8","object":"response","created_at":1741269019,"status":"in_progress","error":null,"incomplete_details":null,"input":[],"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":0.3,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}\n\n`, `data:{"type":"response.output_item.added","output_index":0,"item":{"id":"msg_67c9a81dea8c8190b79651a2b3adf91e","type":"message","status":"in_progress","role":"assistant","content":[]}}\n\n`, `data:{"type":"response.content_part.added","item_id":"msg_67c9a81dea8c8190b79651a2b3adf91e","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67c9a81dea8c8190b79651a2b3adf91e","output_index":0,"content_index":0,"delta":"Hello,"}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67c9a81dea8c8190b79651a2b3adf91e","output_index":0,"content_index":0,"delta":" World!"}\n\n`, `data:{"type":"response.output_text.done","item_id":"msg_67c9a8787f4c8190b49c858d4c1cf20c","output_index":0,"content_index":0,"text":"Hello, World!"}\n\n`, `data:{"type":"response.content_part.done","item_id":"msg_67c9a8787f4c8190b49c858d4c1cf20c","output_index":0,"content_index":0,"part":{"type":"output_text","text":"Hello, World!","annotations":[]}}\n\n`, `data:{"type":"response.output_item.done","output_index":0,"item":{"id":"msg_67c9a8787f4c8190b49c858d4c1cf20c","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Hello, World!","annotations":[]}]}}\n\n`, `data:{"type":"response.completed","response":{"id":"resp_67c9a878139c8190aa2e3105411b408b","object":"response","created_at":1741269112,"status":"completed","error":null,"incomplete_details":null,"input":[],"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-07-18","output":[{"id":"msg_67c9a8787f4c8190b49c858d4c1cf20c","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Hello, World!","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":0.3,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1,"truncation":"disabled","usage":{"input_tokens":543,"input_tokens_details":{"cached_tokens":234},"output_tokens":478,"output_tokens_details":{"reasoning_tokens":123},"total_tokens":512},"user":null,"metadata":{}}}\n\n`, ], }; const { stream } = await createModel('gpt-4o').doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "resp_67c9a81b6a048190a9ee441c5755a4e8", "modelId": "gpt-4o-2024-07-18", "timestamp": 2025-03-06T13:50:19.000Z, "type": "response-metadata", }, { "id": "msg_67c9a81dea8c8190b79651a2b3adf91e", "providerMetadata": { "openai": { "itemId": "msg_67c9a81dea8c8190b79651a2b3adf91e", }, }, "type": "text-start", }, { "delta": "Hello,", "id": "msg_67c9a81dea8c8190b79651a2b3adf91e", "type": "text-delta", }, { "delta": " World!", "id": "msg_67c9a81dea8c8190b79651a2b3adf91e", "type": "text-delta", }, { "id": "msg_67c9a8787f4c8190b49c858d4c1cf20c", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "openai": { "responseId": "resp_67c9a81b6a048190a9ee441c5755a4e8", }, }, "type": "finish", "usage": { "cachedInputTokens": 234, "inputTokens": 543, "outputTokens": 478, "reasoningTokens": 123, "totalTokens": 1021, }, }, ] `); }); it('should send finish reason for incomplete response', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'stream-chunks', chunks: [ `data:{"type":"response.created","response":{"id":"resp_67c9a81b6a048190a9ee441c5755a4e8","object":"response","created_at":1741269019,"status":"in_progress","error":null,"incomplete_details":null,"input":[],"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":0.3,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}\n\n`, `data:{"type":"response.in_progress","response":{"id":"resp_67c9a81b6a048190a9ee441c5755a4e8","object":"response","created_at":1741269019,"status":"in_progress","error":null,"incomplete_details":null,"input":[],"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":0.3,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}\n\n`, `data:{"type":"response.output_item.added","output_index":0,"item":{"id":"msg_67c9a81dea8c8190b79651a2b3adf91e","type":"message","status":"in_progress","role":"assistant","content":[]}}\n\n`, `data:{"type":"response.content_part.added","item_id":"msg_67c9a81dea8c8190b79651a2b3adf91e","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67c9a81dea8c8190b79651a2b3adf91e","output_index":0,"content_index":0,"delta":"Hello,"}\n\n`, `data:{"type":"response.output_text.done","item_id":"msg_67c9a8787f4c8190b49c858d4c1cf20c","output_index":0,"content_index":0,"text":"Hello,!"}\n\n`, `data:{"type":"response.content_part.done","item_id":"msg_67c9a8787f4c8190b49c858d4c1cf20c","output_index":0,"content_index":0,"part":{"type":"output_text","text":"Hello,","annotations":[]}}\n\n`, `data:{"type":"response.output_item.done","output_index":0,"item":{"id":"msg_67c9a8787f4c8190b49c858d4c1cf20c","type":"message","status":"incomplete","role":"assistant","content":[{"type":"output_text","text":"Hello,","annotations":[]}]}}\n\n`, `data:{"type":"response.incomplete","response":{"id":"resp_67cadb40a0708190ac2763c0b6960f6f","object":"response","created_at":1741347648,"status":"incomplete","error":null,"incomplete_details":{"reason":"max_output_tokens"},"instructions":null,"max_output_tokens":100,"model":"gpt-4o-2024-07-18","output":[{"type":"message","id":"msg_67cadb410ccc81909fe1d8f427b9cf02","status":"incomplete","role":"assistant","content":[{"type":"output_text","text":"Hello,","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1,"truncation":"disabled","usage":{"input_tokens":0,"input_tokens_details":{"cached_tokens":0},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":0},"user":null,"metadata":{}}}\n\n`, ], }; const { stream } = await createModel('gpt-4o').doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "resp_67c9a81b6a048190a9ee441c5755a4e8", "modelId": "gpt-4o-2024-07-18", "timestamp": 2025-03-06T13:50:19.000Z, "type": "response-metadata", }, { "id": "msg_67c9a81dea8c8190b79651a2b3adf91e", "providerMetadata": { "openai": { "itemId": "msg_67c9a81dea8c8190b79651a2b3adf91e", }, }, "type": "text-start", }, { "delta": "Hello,", "id": "msg_67c9a81dea8c8190b79651a2b3adf91e", "type": "text-delta", }, { "id": "msg_67c9a8787f4c8190b49c858d4c1cf20c", "type": "text-end", }, { "finishReason": "length", "providerMetadata": { "openai": { "responseId": "resp_67c9a81b6a048190a9ee441c5755a4e8", }, }, "type": "finish", "usage": { "cachedInputTokens": 0, "inputTokens": 0, "outputTokens": 0, "reasoningTokens": 0, "totalTokens": 0, }, }, ] `); }); it('should send streaming tool calls', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'stream-chunks', chunks: [ `data:{"type":"response.created","response":{"id":"resp_67cb13a755c08190acbe3839a49632fc","object":"response","created_at":1741362087,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"function","description":"Get the current location.","name":"currentLocation","parameters":{"type":"object","properties":{},"additionalProperties":false},"strict":true},{"type":"function","description":"Get the weather in a location","name":"weather","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The location to get the weather for"}},"required":["location"],"additionalProperties":false},"strict":true}],"top_p":1,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}\n\n`, `data:{"type":"response.in_progress","response":{"id":"resp_67cb13a755c08190acbe3839a49632fc","object":"response","created_at":1741362087,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"function","description":"Get the current location.","name":"currentLocation","parameters":{"type":"object","properties":{},"additionalProperties":false},"strict":true},{"type":"function","description":"Get the weather in a location","name":"weather","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The location to get the weather for"}},"required":["location"],"additionalProperties":false},"strict":true}],"top_p":1,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}\n\n`, `data:{"type":"response.output_item.added","output_index":0,"item":{"type":"function_call","id":"fc_67cb13a838088190be08eb3927c87501","call_id":"call_6KxSghkb4MVnunFH2TxPErLP","name":"currentLocation","arguments":"","status":"completed"}}\n\n`, `data:{"type":"response.function_call_arguments.delta","item_id":"fc_67cb13a838088190be08eb3927c87501","output_index":0,"delta":"{}"}\n\n`, `data:{"type":"response.function_call_arguments.done","item_id":"fc_67cb13a838088190be08eb3927c87501","output_index":0,"arguments":"{}"}\n\n`, `data:{"type":"response.output_item.done","output_index":0,"item":{"type":"function_call","id":"fc_67cb13a838088190be08eb3927c87501","call_id":"call_pgjcAI4ZegMkP6bsAV7sfrJA","name":"currentLocation","arguments":"{}","status":"completed"}}\n\n`, `data:{"type":"response.output_item.added","output_index":1,"item":{"type":"function_call","id":"fc_67cb13a858f081908a600343fa040f47","call_id":"call_Dg6WUmFHNeR5JxX1s53s1G4b","name":"weather","arguments":"","status":"in_progress"}}\n\n`, `data:{"type":"response.function_call_arguments.delta","item_id":"fc_67cb13a858f081908a600343fa040f47","output_index":1,"delta":"{"}\n\n`, `data:{"type":"response.function_call_arguments.delta","item_id":"fc_67cb13a858f081908a600343fa040f47","output_index":1,"delta":"\\"location"}\n\n`, `data:{"type":"response.function_call_arguments.delta","item_id":"fc_67cb13a858f081908a600343fa040f47","output_index":1,"delta":"\\":"}\n\n`, `data:{"type":"response.function_call_arguments.delta","item_id":"fc_67cb13a858f081908a600343fa040f47","output_index":1,"delta":"\\"Rome"}\n\n`, `data:{"type":"response.function_call_arguments.delta","item_id":"fc_67cb13a858f081908a600343fa040f47","output_index":1,"delta":"\\"}"}\n\n`, `data:{"type":"response.function_call_arguments.done","item_id":"fc_67cb13a858f081908a600343fa040f47","output_index":1,"arguments":"{\\"location\\":\\"Rome\\"}"}\n\n`, `data:{"type":"response.output_item.done","output_index":1,"item":{"type":"function_call","id":"fc_67cb13a858f081908a600343fa040f47","call_id":"call_X2PAkDJInno9VVnNkDrfhboW","name":"weather","arguments":"{\\"location\\":\\"Rome\\"}","status":"completed"}}\n\n`, `data:{"type":"response.completed","response":{"id":"resp_67cb13a755c08190acbe3839a49632fc","object":"response","created_at":1741362087,"status":"completed","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-07-18","output":[{"type":"function_call","id":"fc_67cb13a838088190be08eb3927c87501","call_id":"call_KsVqaVAf3alAtCCkQe4itE7W","name":"currentLocation","arguments":"{}","status":"completed"},{"type":"function_call","id":"fc_67cb13a858f081908a600343fa040f47","call_id":"call_X2PAkDJInno9VVnNkDrfhboW","name":"weather","arguments":"{\\"location\\":\\"Rome\\"}","status":"completed"}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"function","description":"Get the current location.","name":"currentLocation","parameters":{"type":"object","properties":{},"additionalProperties":false},"strict":true},{"type":"function","description":"Get the weather in a location","name":"weather","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The location to get the weather for"}},"required":["location"],"additionalProperties":false},"strict":true}],"top_p":1,"truncation":"disabled","usage":{"input_tokens":0,"input_tokens_details":{"cached_tokens":0},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":0},"user":null,"metadata":{}}}\n\n`, ], }; const { stream } = await createModel('gpt-4o').doStream({ tools: TEST_TOOLS, prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "resp_67cb13a755c08190acbe3839a49632fc", "modelId": "gpt-4o-2024-07-18", "timestamp": 2025-03-07T15:41:27.000Z, "type": "response-metadata", }, { "id": "call_6KxSghkb4MVnunFH2TxPErLP", "toolName": "currentLocation", "type": "tool-input-start", }, { "delta": "{}", "id": "call_6KxSghkb4MVnunFH2TxPErLP", "type": "tool-input-delta", }, { "id": "call_pgjcAI4ZegMkP6bsAV7sfrJA", "type": "tool-input-end", }, { "input": "{}", "providerMetadata": { "openai": { "itemId": "fc_67cb13a838088190be08eb3927c87501", }, }, "toolCallId": "call_pgjcAI4ZegMkP6bsAV7sfrJA", "toolName": "currentLocation", "type": "tool-call", }, { "id": "call_Dg6WUmFHNeR5JxX1s53s1G4b", "toolName": "weather", "type": "tool-input-start", }, { "delta": "{", "id": "call_Dg6WUmFHNeR5JxX1s53s1G4b", "type": "tool-input-delta", }, { "delta": ""location", "id": "call_Dg6WUmFHNeR5JxX1s53s1G4b", "type": "tool-input-delta", }, { "delta": "":", "id": "call_Dg6WUmFHNeR5JxX1s53s1G4b", "type": "tool-input-delta", }, { "delta": ""Rome", "id": "call_Dg6WUmFHNeR5JxX1s53s1G4b", "type": "tool-input-delta", }, { "delta": ""}", "id": "call_Dg6WUmFHNeR5JxX1s53s1G4b", "type": "tool-input-delta", }, { "id": "call_X2PAkDJInno9VVnNkDrfhboW", "type": "tool-input-end", }, { "input": "{"location":"Rome"}", "providerMetadata": { "openai": { "itemId": "fc_67cb13a858f081908a600343fa040f47", }, }, "toolCallId": "call_X2PAkDJInno9VVnNkDrfhboW", "toolName": "weather", "type": "tool-call", }, { "finishReason": "tool-calls", "providerMetadata": { "openai": { "responseId": "resp_67cb13a755c08190acbe3839a49632fc", }, }, "type": "finish", "usage": { "cachedInputTokens": 0, "inputTokens": 0, "outputTokens": 0, "reasoningTokens": 0, "totalTokens": 0, }, }, ] `); }); it('should stream sources', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'stream-chunks', chunks: [ `data:{"type":"response.created","response":{"id":"resp_67cf3390786881908b27489d7e8cfb6b","object":"response","created_at":1741632400,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"web_search_preview","search_context_size":"medium","user_location":{"type":"approximate","city":null,"country":"US","region":null,"timezone":null}}],"top_p":1,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}\n\n`, `data:{"type":"response.in_progress","response":{"id":"resp_67cf3390786881908b27489d7e8cfb6b","object":"response","created_at":1741632400,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"web_search_preview","search_context_size":"medium","user_location":{"type":"approximate","city":null,"country":"US","region":null,"timezone":null}}],"top_p":1,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}\n\n`, `data:{"type":"response.output_item.added","output_index":0,"item":{"type":"web_search_call","id":"ws_67cf3390e9608190869b5d45698a7067","status":"in_progress"}}\n\n`, `data:{"type":"response.web_search_call.in_progress","output_index":0,"item_id":"ws_67cf3390e9608190869b5d45698a7067"}\n\n`, `data:{"type":"response.web_search_call.searching","output_index":0,"item_id":"ws_67cf3390e9608190869b5d45698a7067"}\n\n`, `data:{"type":"response.web_search_call.completed","output_index":0,"item_id":"ws_67cf3390e9608190869b5d45698a7067"}\n\n`, `data:{"type":"response.output_item.done","output_index":0,"item":{"type":"web_search_call","id":"ws_67cf3390e9608190869b5d45698a7067","status":"completed"}}\n\n`, `data:{"type":"response.output_item.added","output_index":1,"item":{"type":"message","id":"msg_67cf33924ea88190b8c12bf68c1f6416","status":"in_progress","role":"assistant","content":[]}}\n\n`, `data:{"type":"response.content_part.added","item_id":"msg_67cf33924ea88190b8c12bf68c1f6416","output_index":1,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67cf33924ea88190b8c12bf68c1f6416","output_index":1,"content_index":0,"delta":"Last week"}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67cf33924ea88190b8c12bf68c1f6416","output_index":1,"content_index":0,"delta":" in San Francisco"}\n\n`, `data:{"type":"response.output_text.annotation.added","item_id":"msg_67cf33924ea88190b8c12bf68c1f6416","output_index":1,"content_index":0,"annotation_index":0,"annotation":{"type":"url_citation","start_index":383,"end_index":493,"url":"https://www.sftourismtips.com/san-francisco-events-in-march.html?utm_source=chatgpt.com","title":"San Francisco Events in March 2025: Festivals, Theater & Easter"}}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67cf33924ea88190b8c12bf68c1f6416","output_index":1,"content_index":0,"delta":" a themed party"}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67cf33924ea88190b8c12bf68c1f6416","output_index":1,"content_index":0,"delta":"([axios.com](https://www.axios.com/local/san-francisco/2025/03/06/sf-events-march-what-to-do-giants-fanfest?utm_source=chatgpt.com))"}\n\n`, `data:{"type":"response.output_text.annotation.added","item_id":"msg_67cf33924ea88190b8c12bf68c1f6416","output_index":1,"content_index":0,"annotation_index":1,"annotation":{"type":"url_citation","start_index":630,"end_index":762,"url":"https://www.axios.com/local/san-francisco/2025/03/06/sf-events-march-what-to-do-giants-fanfest?utm_source=chatgpt.com","title":"SF weekend events: Giants FanFest, crab crawl and more"}}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67cf33924ea88190b8c12bf68c1f6416","output_index":1,"content_index":0,"delta":"."}\n\n`, `data:{"type":"response.output_text.done","item_id":"msg_67cf33924ea88190b8c12bf68c1f6416","output_index":1,"content_index":0,"text":"Last week in San Francisco a themed..."}\n\n`, `data:{"type":"response.content_part.done","item_id":"msg_67cf33924ea88190b8c12bf68c1f6416","output_index":1,"content_index":0,"part":{"type":"output_text","text":"Last week in San Francisco a themed party...","annotations":[{"type":"url_citation","start_index":383,"end_index":493,"url":"https://www.sftourismtips.com/san-francisco-events-in-march.html?utm_source=chatgpt.com","title":"San Francisco Events in March 2025: Festivals, Theater & Easter"},{"type":"url_citation","start_index":630,"end_index":762,"url":"https://www.axios.com/local/san-francisco/2025/03/06/sf-events-march-what-to-do-giants-fanfest?utm_source=chatgpt.com","title":"SF weekend events: Giants FanFest, crab crawl and more"}]}}\n\n`, `data:{"type":"response.output_item.done","output_index":1,"item":{"type":"message","id":"msg_67cf33924ea88190b8c12bf68c1f6416","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Last week in San Francisco a themed party...","annotations":[{"type":"url_citation","start_index":383,"end_index":493,"url":"https://www.sftourismtips.com/san-francisco-events-in-march.html?utm_source=chatgpt.com","title":"San Francisco Events in March 2025: Festivals, Theater & Easter"},{"type":"url_citation","start_index":630,"end_index":762,"url":"https://www.axios.com/local/san-francisco/2025/03/06/sf-events-march-what-to-do-giants-fanfest?utm_source=chatgpt.com","title":"SF weekend events: Giants FanFest, crab crawl and more"}]}]}}\n\n`, `data:{"type":"response.completed","response":{"id":"resp_67cf3390786881908b27489d7e8cfb6b","object":"response","created_at":1741632400,"status":"completed","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-mini-2024-07-18","output":[{"type":"web_search_call","id":"ws_67cf3390e9608190869b5d45698a7067","status":"completed"},{"type":"message","id":"msg_67cf33924ea88190b8c12bf68c1f6416","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Last week in San Francisco a themed party...","annotations":[{"type":"url_citation","start_index":383,"end_index":493,"url":"https://www.sftourismtips.com/san-francisco-events-in-march.html?utm_source=chatgpt.com","title":"San Francisco Events in March 2025: Festivals, Theater & Easter"},{"type":"url_citation","start_index":630,"end_index":762,"url":"https://www.axios.com/local/san-francisco/2025/03/06/sf-events-march-what-to-do-giants-fanfest?utm_source=chatgpt.com","title":"SF weekend events: Giants FanFest, crab crawl and more"}]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"web_search_preview","search_context_size":"medium","user_location":{"type":"approximate","city":null,"country":"US","region":null,"timezone":null}}],"top_p":1,"truncation":"disabled","usage":{"input_tokens":327,"input_tokens_details":{"cached_tokens":0},"output_tokens":834,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":1161},"user":null,"metadata":{}}}\n\n`, ], }; const { stream } = await createModel('gpt-4o-mini').doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "resp_67cf3390786881908b27489d7e8cfb6b", "modelId": "gpt-4o-mini-2024-07-18", "timestamp": 2025-03-10T18:46:40.000Z, "type": "response-metadata", }, { "id": "ws_67cf3390e9608190869b5d45698a7067", "toolName": "web_search_preview", "type": "tool-input-start", }, { "id": "ws_67cf3390e9608190869b5d45698a7067", "type": "tool-input-end", }, { "input": "", "providerExecuted": true, "toolCallId": "ws_67cf3390e9608190869b5d45698a7067", "toolName": "web_search_preview", "type": "tool-call", }, { "providerExecuted": true, "result": { "status": "completed", "type": "web_search_tool_result", }, "toolCallId": "ws_67cf3390e9608190869b5d45698a7067", "toolName": "web_search_preview", "type": "tool-result", }, { "id": "msg_67cf33924ea88190b8c12bf68c1f6416", "providerMetadata": { "openai": { "itemId": "msg_67cf33924ea88190b8c12bf68c1f6416", }, }, "type": "text-start", }, { "delta": "Last week", "id": "msg_67cf33924ea88190b8c12bf68c1f6416", "type": "text-delta", }, { "delta": " in San Francisco", "id": "msg_67cf33924ea88190b8c12bf68c1f6416", "type": "text-delta", }, { "id": "id-0", "sourceType": "url", "title": "San Francisco Events in March 2025: Festivals, Theater & Easter", "type": "source", "url": "https://www.sftourismtips.com/san-francisco-events-in-march.html?utm_source=chatgpt.com", }, { "delta": " a themed party", "id": "msg_67cf33924ea88190b8c12bf68c1f6416", "type": "text-delta", }, { "delta": "([axios.com](https://www.axios.com/local/san-francisco/2025/03/06/sf-events-march-what-to-do-giants-fanfest?utm_source=chatgpt.com))", "id": "msg_67cf33924ea88190b8c12bf68c1f6416", "type": "text-delta", }, { "id": "id-1", "sourceType": "url", "title": "SF weekend events: Giants FanFest, crab crawl and more", "type": "source", "url": "https://www.axios.com/local/san-francisco/2025/03/06/sf-events-march-what-to-do-giants-fanfest?utm_source=chatgpt.com", }, { "delta": ".", "id": "msg_67cf33924ea88190b8c12bf68c1f6416", "type": "text-delta", }, { "id": "msg_67cf33924ea88190b8c12bf68c1f6416", "type": "text-end", }, { "finishReason": "tool-calls", "providerMetadata": { "openai": { "responseId": "resp_67cf3390786881908b27489d7e8cfb6b", }, }, "type": "finish", "usage": { "cachedInputTokens": 0, "inputTokens": 327, "outputTokens": 834, "reasoningTokens": 0, "totalTokens": 1161, }, }, ] `); }); describe('errors', () => { it('should stream error parts', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'stream-chunks', chunks: [ `data:{"type":"response.created","response":{"id":"resp_67cf3390786881908b27489d7e8cfb6b","object":"response","created_at":1741632400,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"web_search_preview","search_context_size":"medium","user_location":{"type":"approximate","city":null,"country":"US","region":null,"timezone":null}}],"top_p":1,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}\n\n`, `data:{"type":"error","code":"ERR_SOMETHING","message":"Something went wrong","param":null,"sequence_number":1}\n\n`, ], }; const { stream } = await createModel('gpt-4o-mini').doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)) .toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "resp_67cf3390786881908b27489d7e8cfb6b", "modelId": "gpt-4o-mini-2024-07-18", "timestamp": 2025-03-10T18:46:40.000Z, "type": "response-metadata", }, { "error": { "code": "ERR_SOMETHING", "message": "Something went wrong", "param": null, "sequence_number": 1, "type": "error", }, "type": "error", }, { "finishReason": "unknown", "providerMetadata": { "openai": { "responseId": "resp_67cf3390786881908b27489d7e8cfb6b", }, }, "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it('should handle file_search tool calls', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'stream-chunks', chunks: [ `data:{"type":"response.created","response":{"id":"resp_67cf3390786881908b27489d7e8cfb6b","object":"response","created_at":1741632400,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"file_search"}],"top_p":1,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}\n\n`, `data:{"type":"response.output_item.added","output_index":0,"item":{"type":"file_search_call","id":"fs_67cf3390e9608190869b5d45698a7067","status":"in_progress"}}\n\n`, `data:{"type":"response.output_item.done","output_index":0,"item":{"type":"file_search_call","id":"fs_67cf3390e9608190869b5d45698a7067","status":"completed"}}\n\n`, `data:{"type":"response.output_item.added","output_index":1,"item":{"type":"message","id":"msg_67cf33924ea88190b8c12bf68c1f6416","status":"in_progress","role":"assistant","content":[]}}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67cf33924ea88190b8c12bf68c1f6416","output_index":1,"content_index":0,"delta":"Based on the search results, here is the information you requested."}\n\n`, `data:{"type":"response.output_item.done","output_index":1,"item":{"type":"message","id":"msg_67cf33924ea88190b8c12bf68c1f6416","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Based on the search results, here is the information you requested.","annotations":[]}]}}\n\n`, `data:{"type":"response.completed","response":{"id":"resp_67cf3390786881908b27489d7e8cfb6b","object":"response","created_at":1741632400,"status":"completed","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-mini-2024-07-18","output":[{"type":"file_search_call","id":"fs_67cf3390e9608190869b5d45698a7067","status":"completed"},{"type":"message","id":"msg_67cf33924ea88190b8c12bf68c1f6416","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Based on the search results, here is the information you requested.","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"file_search"}],"top_p":1,"truncation":"disabled","usage":{"input_tokens":327,"input_tokens_details":{"cached_tokens":0},"output_tokens":834,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":1161},"user":null,"metadata":{}}}\n\n`, ], }; const { stream } = await createModel('gpt-4o-mini').doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)) .toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "resp_67cf3390786881908b27489d7e8cfb6b", "modelId": "gpt-4o-mini-2024-07-18", "timestamp": 2025-03-10T18:46:40.000Z, "type": "response-metadata", }, { "id": "msg_67cf33924ea88190b8c12bf68c1f6416", "providerMetadata": { "openai": { "itemId": "msg_67cf33924ea88190b8c12bf68c1f6416", }, }, "type": "text-start", }, { "delta": "Based on the search results, here is the information you requested.", "id": "msg_67cf33924ea88190b8c12bf68c1f6416", "type": "text-delta", }, { "id": "msg_67cf33924ea88190b8c12bf68c1f6416", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "openai": { "responseId": "resp_67cf3390786881908b27489d7e8cfb6b", }, }, "type": "finish", "usage": { "cachedInputTokens": 0, "inputTokens": 327, "outputTokens": 834, "reasoningTokens": 0, "totalTokens": 1161, }, }, ] `); }); }); describe('reasoning', () => { it('should handle reasoning with summary', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'stream-chunks', chunks: [ `data:{"type":"response.created","response":{"id":"resp_67c9a81b6a048190a9ee441c5755a4e8","object":"response","created_at":1741269019,"status":"in_progress","error":null,"incomplete_details":null,"input":[],"instructions":null,"max_output_tokens":null,"model":"o3-mini-2025-01-31","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":"low","summary":"auto"},"store":true,"temperature":null,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":null,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}\n\n`, `data:{"type":"response.output_item.added","output_index":0,"item":{"id":"rs_6808709f6fcc8191ad2e2fdd784017b3","type":"reasoning"}}\n\n`, `data:{"type":"response.reasoning_summary_part.added","item_id":"rs_6808709f6fcc8191ad2e2fdd784017b3","summary_index":0}\n\n`, `data:{"type":"response.reasoning_summary_text.delta","item_id":"rs_6808709f6fcc8191ad2e2fdd784017b3","summary_index":0,"delta":"**Exploring burrito origins**\\n\\nThe user is"}\n\n`, `data:{"type":"response.reasoning_summary_text.delta","item_id":"rs_6808709f6fcc8191ad2e2fdd784017b3","summary_index":0,"delta":" curious about the debate regarding Taqueria La Cumbre and El Farolito."}\n\n`, `data:{"type":"response.reasoning_summary_part.done","item_id":"rs_6808709f6fcc8191ad2e2fdd784017b3","summary_index":0}\n\n`, `data:{"type":"response.reasoning_summary_part.added","item_id":"rs_6808709f6fcc8191ad2e2fdd784017b3","summary_index":1}\n\n`, `data:{"type":"response.reasoning_summary_text.delta","item_id":"rs_6808709f6fcc8191ad2e2fdd784017b3","summary_index":1,"delta":"**Investigating burrito origins**\\n\\nThere's a fascinating debate"}\n\n`, `data:{"type":"response.reasoning_summary_text.delta","item_id":"rs_6808709f6fcc8191ad2e2fdd784017b3","summary_index":1,"delta":" about who created the Mission burrito."}\n\n`, `data:{"type":"response.reasoning_summary_part.done","item_id":"rs_6808709f6fcc8191ad2e2fdd784017b3","summary_index":1}\n\n`, `data:{"type":"response.output_item.done","output_index":0,"item":{"id":"rs_6808709f6fcc8191ad2e2fdd784017b3","type":"reasoning"}}\n\n`, `data:{"type":"response.output_item.added","output_index":1,"item":{"id":"msg_67c97c02656c81908e080dfdf4a03cd1","type":"message"}}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67c97c02656c81908e080dfdf4a03cd1","delta":"answer"}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67c97c02656c81908e080dfdf4a03cd1","delta":" text"}\n\n`, `data:{"type":"response.output_item.done","output_index":1,"item":{"id":"msg_67c97c02656c81908e080dfdf4a03cd1","type":"message"}}\n\n`, `data:{"type":"response.completed","response":{"id":"resp_67c9a81b6a048190a9ee441c5755a4e8","object":"response","created_at":1741269019,"status":"completed","error":null,"incomplete_details":null,"input":[],"instructions":null,"max_output_tokens":null,"model":"o3-mini-2025-01-31","output":[{"id":"rs_6808709f6fcc8191ad2e2fdd784017b3","type":"reasoning","summary":[{"type":"summary_text","text":"**Exploring burrito origins**\\n\\nThe user is curious about the debate regarding Taqueria La Cumbre and El Farolito."},{"type":"summary_text","text":"**Investigating burrito origins**\\n\\nThere's a fascinating debate about who created the Mission burrito."}]},{"id":"msg_67c97c02656c81908e080dfdf4a03cd1","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"answer text","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":"low","summary":"auto"},"store":true,"temperature":null,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":null,"truncation":"disabled","usage":{"input_tokens":34,"input_tokens_details":{"cached_tokens":0},"output_tokens":538,"output_tokens_details":{"reasoning_tokens":320},"total_tokens":572},"user":null,"metadata":{}}}\n\n`, ], }; const { stream } = await createModel('o3-mini').doStream({ prompt: TEST_PROMPT, providerOptions: { openai: { reasoningEffort: 'low', reasoningSummary: 'auto', }, }, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)) .toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "resp_67c9a81b6a048190a9ee441c5755a4e8", "modelId": "o3-mini-2025-01-31", "timestamp": 2025-03-06T13:50:19.000Z, "type": "response-metadata", }, { "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:0", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": null, }, }, "type": "reasoning-start", }, { "delta": "**Exploring burrito origins** The user is", "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:0", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", }, }, "type": "reasoning-delta", }, { "delta": " curious about the debate regarding Taqueria La Cumbre and El Farolito.", "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:0", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", }, }, "type": "reasoning-delta", }, { "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:1", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": null, }, }, "type": "reasoning-start", }, { "delta": "**Investigating burrito origins** There's a fascinating debate", "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:1", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", }, }, "type": "reasoning-delta", }, { "delta": " about who created the Mission burrito.", "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:1", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", }, }, "type": "reasoning-delta", }, { "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:0", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": null, }, }, "type": "reasoning-end", }, { "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:1", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": null, }, }, "type": "reasoning-end", }, { "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "providerMetadata": { "openai": { "itemId": "msg_67c97c02656c81908e080dfdf4a03cd1", }, }, "type": "text-start", }, { "delta": "answer", "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "type": "text-delta", }, { "delta": " text", "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "type": "text-delta", }, { "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "openai": { "responseId": "resp_67c9a81b6a048190a9ee441c5755a4e8", }, }, "type": "finish", "usage": { "cachedInputTokens": 0, "inputTokens": 34, "outputTokens": 538, "reasoningTokens": 320, "totalTokens": 572, }, }, ] `); expect(await server.calls[0].requestBodyJson).toMatchObject({ model: 'o3-mini', reasoning: { effort: 'low', summary: 'auto', }, stream: true, }); }); it('should handle reasoning with empty summary', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'stream-chunks', chunks: [ `data:{"type":"response.created","response":{"id":"resp_67c9a81b6a048190a9ee441c5755a4e8","object":"response","created_at":1741269019,"status":"in_progress","error":null,"incomplete_details":null,"input":[],"instructions":null,"max_output_tokens":null,"model":"o3-mini-2025-01-31","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":"low","summary":"auto"},"store":true,"temperature":null,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":null,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}\n\n`, `data:{"type":"response.output_item.added","output_index":0,"item":{"id":"rs_6808709f6fcc8191ad2e2fdd784017b3","type":"reasoning"}}\n\n`, `data:{"type":"response.output_item.done","output_index":0,"item":{"id":"rs_6808709f6fcc8191ad2e2fdd784017b3","type":"reasoning"}}\n\n`, `data:{"type":"response.output_item.added","output_index":1,"item":{"id":"msg_67c97c02656c81908e080dfdf4a03cd1","type":"message"}}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67c97c02656c81908e080dfdf4a03cd1","delta":"answer"}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67c97c02656c81908e080dfdf4a03cd1","delta":" text"}\n\n`, `data:{"type":"response.output_item.done","output_index":1,"item":{"id":"msg_67c97c02656c81908e080dfdf4a03cd1","type":"message"}}\n\n`, `data:{"type":"response.completed","response":{"id":"resp_67c9a81b6a048190a9ee441c5755a4e8","object":"response","created_at":1741269019,"status":"completed","error":null,"incomplete_details":null,"input":[],"instructions":null,"max_output_tokens":null,"model":"o3-mini-2025-01-31","output":[{"id":"rs_6808709f6fcc8191ad2e2fdd784017b3","type":"reasoning","summary":[]},{"id":"msg_67c97c02656c81908e080dfdf4a03cd1","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"answer text","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":"low","summary":"auto"},"store":true,"temperature":null,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":null,"truncation":"disabled","usage":{"input_tokens":34,"input_tokens_details":{"cached_tokens":0},"output_tokens":538,"output_tokens_details":{"reasoning_tokens":320},"total_tokens":572},"user":null,"metadata":{}}}\n\n`, ], }; const { stream } = await createModel('o3-mini').doStream({ prompt: TEST_PROMPT, providerOptions: { openai: { reasoningEffort: 'low', reasoningSummary: null, }, }, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)) .toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "resp_67c9a81b6a048190a9ee441c5755a4e8", "modelId": "o3-mini-2025-01-31", "timestamp": 2025-03-06T13:50:19.000Z, "type": "response-metadata", }, { "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:0", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": null, }, }, "type": "reasoning-start", }, { "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:0", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": null, }, }, "type": "reasoning-end", }, { "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "providerMetadata": { "openai": { "itemId": "msg_67c97c02656c81908e080dfdf4a03cd1", }, }, "type": "text-start", }, { "delta": "answer", "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "type": "text-delta", }, { "delta": " text", "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "type": "text-delta", }, { "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "openai": { "responseId": "resp_67c9a81b6a048190a9ee441c5755a4e8", }, }, "type": "finish", "usage": { "cachedInputTokens": 0, "inputTokens": 34, "outputTokens": 538, "reasoningTokens": 320, "totalTokens": 572, }, }, ] `); expect(await server.calls[0].requestBodyJson).toMatchObject({ model: 'o3-mini', reasoning: { effort: 'low', }, stream: true, }); }); it('should handle encrypted content with summary', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'stream-chunks', chunks: [ `data:{"type":"response.created","response":{"id":"resp_67c9a81b6a048190a9ee441c5755a4e8","object":"response","created_at":1741269019,"status":"in_progress","error":null,"incomplete_details":null,"input":[],"instructions":null,"max_output_tokens":null,"model":"o3-mini-2025-01-31","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":"low","summary":"auto"},"store":true,"temperature":null,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":null,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}\n\n`, `data:{"type":"response.output_item.added","output_index":0,"item":{"id":"rs_6808709f6fcc8191ad2e2fdd784017b3","type":"reasoning","encrypted_content":"encrypted_reasoning_data_abc123"}}\n\n`, `data:{"type":"response.reasoning_summary_part.added","item_id":"rs_6808709f6fcc8191ad2e2fdd784017b3","summary_index":0}\n\n`, `data:{"type":"response.reasoning_summary_text.delta","item_id":"rs_6808709f6fcc8191ad2e2fdd784017b3","summary_index":0,"delta":"**Exploring burrito origins**\\n\\nThe user is"}\n\n`, `data:{"type":"response.reasoning_summary_text.delta","item_id":"rs_6808709f6fcc8191ad2e2fdd784017b3","summary_index":0,"delta":" curious about the debate regarding Taqueria La Cumbre and El Farolito."}\n\n`, `data:{"type":"response.reasoning_summary_part.done","item_id":"rs_6808709f6fcc8191ad2e2fdd784017b3","summary_index":0}\n\n`, `data:{"type":"response.reasoning_summary_part.added","item_id":"rs_6808709f6fcc8191ad2e2fdd784017b3","summary_index":1}\n\n`, `data:{"type":"response.reasoning_summary_text.delta","item_id":"rs_6808709f6fcc8191ad2e2fdd784017b3","summary_index":1,"delta":"**Investigating burrito origins**\\n\\nThere's a fascinating debate"}\n\n`, `data:{"type":"response.reasoning_summary_text.delta","item_id":"rs_6808709f6fcc8191ad2e2fdd784017b3","summary_index":1,"delta":" about who created the Mission burrito."}\n\n`, `data:{"type":"response.reasoning_summary_part.done","item_id":"rs_6808709f6fcc8191ad2e2fdd784017b3","summary_index":1}\n\n`, `data:{"type":"response.output_item.done","output_index":0,"item":{"id":"rs_6808709f6fcc8191ad2e2fdd784017b3","type":"reasoning","encrypted_content":"encrypted_reasoning_data_final_def456"}}\n\n`, `data:{"type":"response.output_item.added","output_index":1,"item":{"id":"msg_67c97c02656c81908e080dfdf4a03cd1","type":"message"}}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67c97c02656c81908e080dfdf4a03cd1","delta":"answer"}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67c97c02656c81908e080dfdf4a03cd1","delta":" text"}\n\n`, `data:{"type":"response.output_item.done","output_index":1,"item":{"id":"msg_67c97c02656c81908e080dfdf4a03cd1","type":"message"}}\n\n`, `data:{"type":"response.completed","response":{"id":"resp_67c9a81b6a048190a9ee441c5755a4e8","object":"response","created_at":1741269019,"status":"completed","error":null,"incomplete_details":null,"input":[],"instructions":null,"max_output_tokens":null,"model":"o3-mini-2025-01-31","output":[{"id":"rs_6808709f6fcc8191ad2e2fdd784017b3","type":"reasoning","encrypted_content":"encrypted_reasoning_data_final_def456","summary":[{"type":"summary_text","text":"**Exploring burrito origins**\\n\\nThe user is curious about the debate regarding Taqueria La Cumbre and El Farolito."},{"type":"summary_text","text":"**Investigating burrito origins**\\n\\nThere's a fascinating debate about who created the Mission burrito."}]},{"id":"msg_67c97c02656c81908e080dfdf4a03cd1","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"answer text","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":"low","summary":"auto"},"store":true,"temperature":null,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":null,"truncation":"disabled","usage":{"input_tokens":34,"input_tokens_details":{"cached_tokens":0},"output_tokens":538,"output_tokens_details":{"reasoning_tokens":320},"total_tokens":572},"user":null,"metadata":{}}}\n\n`, ], }; const { stream } = await createModel('o3-mini').doStream({ prompt: TEST_PROMPT, providerOptions: { openai: { reasoningEffort: 'low', reasoningSummary: 'auto', include: ['reasoning.encrypted_content'], }, }, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)) .toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "resp_67c9a81b6a048190a9ee441c5755a4e8", "modelId": "o3-mini-2025-01-31", "timestamp": 2025-03-06T13:50:19.000Z, "type": "response-metadata", }, { "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:0", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": "encrypted_reasoning_data_abc123", }, }, "type": "reasoning-start", }, { "delta": "**Exploring burrito origins** The user is", "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:0", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", }, }, "type": "reasoning-delta", }, { "delta": " curious about the debate regarding Taqueria La Cumbre and El Farolito.", "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:0", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", }, }, "type": "reasoning-delta", }, { "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:1", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": "encrypted_reasoning_data_abc123", }, }, "type": "reasoning-start", }, { "delta": "**Investigating burrito origins** There's a fascinating debate", "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:1", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", }, }, "type": "reasoning-delta", }, { "delta": " about who created the Mission burrito.", "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:1", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", }, }, "type": "reasoning-delta", }, { "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:0", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": "encrypted_reasoning_data_final_def456", }, }, "type": "reasoning-end", }, { "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:1", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": "encrypted_reasoning_data_final_def456", }, }, "type": "reasoning-end", }, { "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "providerMetadata": { "openai": { "itemId": "msg_67c97c02656c81908e080dfdf4a03cd1", }, }, "type": "text-start", }, { "delta": "answer", "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "type": "text-delta", }, { "delta": " text", "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "type": "text-delta", }, { "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "openai": { "responseId": "resp_67c9a81b6a048190a9ee441c5755a4e8", }, }, "type": "finish", "usage": { "cachedInputTokens": 0, "inputTokens": 34, "outputTokens": 538, "reasoningTokens": 320, "totalTokens": 572, }, }, ] `); expect(await server.calls[0].requestBodyJson).toMatchObject({ model: 'o3-mini', reasoning: { effort: 'low', summary: 'auto', }, include: ['reasoning.encrypted_content'], stream: true, }); }); it('should handle encrypted content with empty summary', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'stream-chunks', chunks: [ `data:{"type":"response.created","response":{"id":"resp_67c9a81b6a048190a9ee441c5755a4e8","object":"response","created_at":1741269019,"status":"in_progress","error":null,"incomplete_details":null,"input":[],"instructions":null,"max_output_tokens":null,"model":"o3-mini-2025-01-31","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":"low","summary":"auto"},"store":true,"temperature":null,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":null,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}\n\n`, `data:{"type":"response.output_item.added","output_index":0,"item":{"id":"rs_6808709f6fcc8191ad2e2fdd784017b3","type":"reasoning","encrypted_content":"encrypted_reasoning_data_abc123"}}\n\n`, `data:{"type":"response.output_item.done","output_index":0,"item":{"id":"rs_6808709f6fcc8191ad2e2fdd784017b3","type":"reasoning","encrypted_content":"encrypted_reasoning_data_final_def456"}}\n\n`, `data:{"type":"response.output_item.added","output_index":1,"item":{"id":"msg_67c97c02656c81908e080dfdf4a03cd1","type":"message"}}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67c97c02656c81908e080dfdf4a03cd1","delta":"answer"}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67c97c02656c81908e080dfdf4a03cd1","delta":" text"}\n\n`, `data:{"type":"response.output_item.done","output_index":1,"item":{"id":"msg_67c97c02656c81908e080dfdf4a03cd1","type":"message"}}\n\n`, `data:{"type":"response.completed","response":{"id":"resp_67c9a81b6a048190a9ee441c5755a4e8","object":"response","created_at":1741269019,"status":"completed","error":null,"incomplete_details":null,"input":[],"instructions":null,"max_output_tokens":null,"model":"o3-mini-2025-01-31","output":[{"id":"rs_6808709f6fcc8191ad2e2fdd784017b3","type":"reasoning","encrypted_content":"encrypted_reasoning_data_final_def456","summary":[]},{"id":"msg_67c97c02656c81908e080dfdf4a03cd1","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"answer text","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":"low","summary":"auto"},"store":true,"temperature":null,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":null,"truncation":"disabled","usage":{"input_tokens":34,"input_tokens_details":{"cached_tokens":0},"output_tokens":538,"output_tokens_details":{"reasoning_tokens":320},"total_tokens":572},"user":null,"metadata":{}}}\n\n`, ], }; const { stream } = await createModel('o3-mini').doStream({ prompt: TEST_PROMPT, providerOptions: { openai: { reasoningEffort: 'low', reasoningSummary: null, include: ['reasoning.encrypted_content'], }, }, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)) .toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "resp_67c9a81b6a048190a9ee441c5755a4e8", "modelId": "o3-mini-2025-01-31", "timestamp": 2025-03-06T13:50:19.000Z, "type": "response-metadata", }, { "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:0", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": "encrypted_reasoning_data_abc123", }, }, "type": "reasoning-start", }, { "id": "rs_6808709f6fcc8191ad2e2fdd784017b3:0", "providerMetadata": { "openai": { "itemId": "rs_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": "encrypted_reasoning_data_final_def456", }, }, "type": "reasoning-end", }, { "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "providerMetadata": { "openai": { "itemId": "msg_67c97c02656c81908e080dfdf4a03cd1", }, }, "type": "text-start", }, { "delta": "answer", "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "type": "text-delta", }, { "delta": " text", "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "type": "text-delta", }, { "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "openai": { "responseId": "resp_67c9a81b6a048190a9ee441c5755a4e8", }, }, "type": "finish", "usage": { "cachedInputTokens": 0, "inputTokens": 34, "outputTokens": 538, "reasoningTokens": 320, "totalTokens": 572, }, }, ] `); expect(await server.calls[0].requestBodyJson).toMatchObject({ model: 'o3-mini', reasoning: { effort: 'low', }, include: ['reasoning.encrypted_content'], stream: true, }); }); it('should handle multiple reasoning blocks', async () => { server.urls['https://api.openai.com/v1/responses'].response = { type: 'stream-chunks', chunks: [ `data:{"type":"response.created","response":{"id":"resp_67c9a81b6a048190a9ee441c5755a4e8","object":"response","created_at":1741269019,"status":"in_progress","error":null,"incomplete_details":null,"input":[],"instructions":null,"max_output_tokens":null,"model":"o3-mini-2025-01-31","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":"medium","summary":"auto"},"store":true,"temperature":null,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":null,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}\n\n`, // First reasoning block (with multiple summary parts) `data:{"type":"response.output_item.added","output_index":0,"item":{"id":"rs_first_6808709f6fcc8191ad2e2fdd784017b3","type":"reasoning"}}\n\n`, `data:{"type":"response.reasoning_summary_part.added","item_id":"rs_first_6808709f6fcc8191ad2e2fdd784017b3","summary_index":0}\n\n`, `data:{"type":"response.reasoning_summary_text.delta","item_id":"rs_first_6808709f6fcc8191ad2e2fdd784017b3","summary_index":0,"delta":"**Initial analysis**\\n\\nFirst reasoning block:"}\n\n`, `data:{"type":"response.reasoning_summary_text.delta","item_id":"rs_first_6808709f6fcc8191ad2e2fdd784017b3","summary_index":0,"delta":" analyzing the problem structure."}\n\n`, `data:{"type":"response.reasoning_summary_part.done","item_id":"rs_first_6808709f6fcc8191ad2e2fdd784017b3","summary_index":0}\n\n`, `data:{"type":"response.reasoning_summary_part.added","item_id":"rs_first_6808709f6fcc8191ad2e2fdd784017b3","summary_index":1}\n\n`, `data:{"type":"response.reasoning_summary_text.delta","item_id":"rs_first_6808709f6fcc8191ad2e2fdd784017b3","summary_index":1,"delta":"**Deeper consideration**\\n\\nLet me think about"}\n\n`, `data:{"type":"response.reasoning_summary_text.delta","item_id":"rs_first_6808709f6fcc8191ad2e2fdd784017b3","summary_index":1,"delta":" the various approaches available."}\n\n`, `data:{"type":"response.reasoning_summary_part.done","item_id":"rs_first_6808709f6fcc8191ad2e2fdd784017b3","summary_index":1}\n\n`, `data:{"type":"response.output_item.done","output_index":0,"item":{"id":"rs_first_6808709f6fcc8191ad2e2fdd784017b3","type":"reasoning"}}\n\n`, // First message `data:{"type":"response.output_item.added","output_index":1,"item":{"id":"msg_67c97c02656c81908e080dfdf4a03cd1","type":"message"}}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67c97c02656c81908e080dfdf4a03cd1","delta":"Let me think about"}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_67c97c02656c81908e080dfdf4a03cd1","delta":" this step by step."}\n\n`, `data:{"type":"response.output_item.done","output_index":1,"item":{"id":"msg_67c97c02656c81908e080dfdf4a03cd1","type":"message"}}\n\n`, // Second reasoning block `data:{"type":"response.output_item.added","output_index":2,"item":{"id":"rs_second_7908809g7gcc9291be3e3fee895028c4","type":"reasoning"}}\n\n`, `data:{"type":"response.reasoning_summary_part.added","item_id":"rs_second_7908809g7gcc9291be3e3fee895028c4","summary_index":0}\n\n`, `data:{"type":"response.reasoning_summary_text.delta","item_id":"rs_second_7908809g7gcc9291be3e3fee895028c4","summary_index":0,"delta":"Second reasoning block:"}\n\n`, `data:{"type":"response.reasoning_summary_text.delta","item_id":"rs_second_7908809g7gcc9291be3e3fee895028c4","summary_index":0,"delta":" considering alternative approaches."}\n\n`, `data:{"type":"response.reasoning_summary_part.done","item_id":"rs_second_7908809g7gcc9291be3e3fee895028c4","summary_index":0}\n\n`, `data:{"type":"response.output_item.done","output_index":2,"item":{"id":"rs_second_7908809g7gcc9291be3e3fee895028c4","type":"reasoning"}}\n\n`, // Final message `data:{"type":"response.output_item.added","output_index":3,"item":{"id":"msg_final_78d08d03767d92908f25523f5ge51e77","type":"message"}}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_final_78d08d03767d92908f25523f5ge51e77","delta":"Based on my analysis,"}\n\n`, `data:{"type":"response.output_text.delta","item_id":"msg_final_78d08d03767d92908f25523f5ge51e77","delta":" here is the solution."}\n\n`, `data:{"type":"response.output_item.done","output_index":3,"item":{"id":"msg_final_78d08d03767d92908f25523f5ge51e77","type":"message"}}\n\n`, `data:{"type":"response.completed","response":{"id":"resp_67c9a81b6a048190a9ee441c5755a4e8","object":"response","created_at":1741269019,"status":"completed","error":null,"incomplete_details":null,"input":[],"instructions":null,"max_output_tokens":null,"model":"o3-mini-2025-01-31","output":[{"id":"rs_first_6808709f6fcc8191ad2e2fdd784017b3","type":"reasoning","summary":[{"type":"summary_text","text":"**Initial analysis**\\n\\nFirst reasoning block: analyzing the problem structure."},{"type":"summary_text","text":"**Deeper consideration**\\n\\nLet me think about the various approaches available."}]},{"id":"msg_67c97c02656c81908e080dfdf4a03cd1","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Let me think about this step by step.","annotations":[]}]},{"id":"rs_second_7908809g7gcc9291be3e3fee895028c4","type":"reasoning","summary":[{"type":"summary_text","text":"Second reasoning block: considering alternative approaches."}]},{"id":"msg_final_78d08d03767d92908f25523f5ge51e77","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Based on my analysis, here is the solution.","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":"medium","summary":"auto"},"store":true,"temperature":null,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":null,"truncation":"disabled","usage":{"input_tokens":45,"input_tokens_details":{"cached_tokens":0},"output_tokens":628,"output_tokens_details":{"reasoning_tokens":420},"total_tokens":673},"user":null,"metadata":{}}}\n\n`, ], }; const { stream } = await createModel('o3-mini').doStream({ prompt: TEST_PROMPT, providerOptions: { openai: { reasoningEffort: 'medium', reasoningSummary: 'auto', }, }, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)) .toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "resp_67c9a81b6a048190a9ee441c5755a4e8", "modelId": "o3-mini-2025-01-31", "timestamp": 2025-03-06T13:50:19.000Z, "type": "response-metadata", }, { "id": "rs_first_6808709f6fcc8191ad2e2fdd784017b3:0", "providerMetadata": { "openai": { "itemId": "rs_first_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": null, }, }, "type": "reasoning-start", }, { "delta": "**Initial analysis** First reasoning block:", "id": "rs_first_6808709f6fcc8191ad2e2fdd784017b3:0", "providerMetadata": { "openai": { "itemId": "rs_first_6808709f6fcc8191ad2e2fdd784017b3", }, }, "type": "reasoning-delta", }, { "delta": " analyzing the problem structure.", "id": "rs_first_6808709f6fcc8191ad2e2fdd784017b3:0", "providerMetadata": { "openai": { "itemId": "rs_first_6808709f6fcc8191ad2e2fdd784017b3", }, }, "type": "reasoning-delta", }, { "id": "rs_first_6808709f6fcc8191ad2e2fdd784017b3:1", "providerMetadata": { "openai": { "itemId": "rs_first_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": null, }, }, "type": "reasoning-start", }, { "delta": "**Deeper consideration** Let me think about", "id": "rs_first_6808709f6fcc8191ad2e2fdd784017b3:1", "providerMetadata": { "openai": { "itemId": "rs_first_6808709f6fcc8191ad2e2fdd784017b3", }, }, "type": "reasoning-delta", }, { "delta": " the various approaches available.", "id": "rs_first_6808709f6fcc8191ad2e2fdd784017b3:1", "providerMetadata": { "openai": { "itemId": "rs_first_6808709f6fcc8191ad2e2fdd784017b3", }, }, "type": "reasoning-delta", }, { "id": "rs_first_6808709f6fcc8191ad2e2fdd784017b3:0", "providerMetadata": { "openai": { "itemId": "rs_first_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": null, }, }, "type": "reasoning-end", }, { "id": "rs_first_6808709f6fcc8191ad2e2fdd784017b3:1", "providerMetadata": { "openai": { "itemId": "rs_first_6808709f6fcc8191ad2e2fdd784017b3", "reasoningEncryptedContent": null, }, }, "type": "reasoning-end", }, { "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "providerMetadata": { "openai": { "itemId": "msg_67c97c02656c81908e080dfdf4a03cd1", }, }, "type": "text-start", }, { "delta": "Let me think about", "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "type": "text-delta", }, { "delta": " this step by step.", "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "type": "text-delta", }, { "id": "msg_67c97c02656c81908e080dfdf4a03cd1", "type": "text-end", }, { "id": "rs_second_7908809g7gcc9291be3e3fee895028c4:0", "providerMetadata": { "openai": { "itemId": "rs_second_7908809g7gcc9291be3e3fee895028c4", "reasoningEncryptedContent": null, }, }, "type": "reasoning-start", }, { "delta": "Second reasoning block:", "id": "rs_second_7908809g7gcc9291be3e3fee895028c4:0", "providerMetadata": { "openai": { "itemId": "rs_second_7908809g7gcc9291be3e3fee895028c4", }, }, "type": "reasoning-delta", }, { "delta": " considering alternative approaches.", "id": "rs_second_7908809g7gcc9291be3e3fee895028c4:0", "providerMetadata": { "openai": { "itemId": "rs_second_7908809g7gcc9291be3e3fee895028c4", }, }, "type": "reasoning-delta", }, { "id": "rs_second_7908809g7gcc9291be3e3fee895028c4:0", "providerMetadata": { "openai": { "itemId": "rs_second_7908809g7gcc9291be3e3fee895028c4", "reasoningEncryptedContent": null, }, }, "type": "reasoning-end", }, { "id": "msg_final_78d08d03767d92908f25523f5ge51e77", "providerMetadata": { "openai": { "itemId": "msg_final_78d08d03767d92908f25523f5ge51e77", }, }, "type": "text-start", }, { "delta": "Based on my analysis,", "id": "msg_final_78d08d03767d92908f25523f5ge51e77", "type": "text-delta", }, { "delta": " here is the solution.", "id": "msg_final_78d08d03767d92908f25523f5ge51e77", "type": "text-delta", }, { "id": "msg_final_78d08d03767d92908f25523f5ge51e77", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "openai": { "responseId": "resp_67c9a81b6a048190a9ee441c5755a4e8", }, }, "type": "finish", "usage": { "cachedInputTokens": 0, "inputTokens": 45, "outputTokens": 628, "reasoningTokens": 420, "totalTokens": 673, }, }, ] `); expect(await server.calls[0].requestBodyJson).toMatchObject({ model: 'o3-mini', reasoning: { effort: 'medium', summary: 'auto', }, stream: true, }); }); }); describe('server-side tools', () => { const TEST_PROMPT = [ { role: 'user' as const, content: [ { type: 'text' as const, text: 'Search for recent news about San Francisco tech events, then check the status of our server-side tool implementation.', }, ], }, ]; function prepareJsonResponse(body: any) { server.urls['https://api.openai.com/v1/responses'].response = { type: 'json-value', body, }; } it('should enable server-side web search when using openai.tools.webSearchPreview', async () => { prepareJsonResponse({ id: 'resp_67cf2b2f6bd081909be2c8054ddef0eb', object: 'response', created_at: 1741630255, status: 'completed', error: null, incomplete_details: null, instructions: null, max_output_tokens: null, model: 'gpt-4o-mini', output: [ { type: 'web_search_call', id: 'ws_67cf2b3051e88190b006770db6fdb13d', status: 'completed', }, { type: 'message', id: 'msg_67cf2b35467481908f24412e4fd40d66', status: 'completed', role: 'assistant', content: [ { type: 'output_text', text: "As of June 23, 2025, here are some recent developments in San Francisco's tech scene:", annotations: [ { type: 'url_citation', start_index: 0, end_index: 50, url: 'https://www.eventbrite.sg/d/ca--san-francisco/tech-events/?utm_source=openai', title: 'Discover Tech Events & Activities in San Francisco, CA | Eventbrite', }, { type: 'url_citation', start_index: 51, end_index: 100, url: 'https://www.axios.com/2024/12/10/ai-sf-summit-2024-roundup?utm_source=openai', title: 'AI+ SF Summit: AI agents are the next big thing', }, ], }, ], }, ], usage: { input_tokens: 1359, output_tokens: 624 }, }); const result = await createModel('gpt-4o-mini').doGenerate({ prompt: TEST_PROMPT, tools: [ { type: 'provider-defined', id: 'openai.web_search_preview', name: 'web_search_preview', args: { searchContextSize: 'high', userLocation: { type: 'approximate', city: 'San Francisco', region: 'California', country: 'US', }, }, }, ], }); expect(result.content).toMatchInlineSnapshot(` [ { "input": "", "providerExecuted": true, "toolCallId": "ws_67cf2b3051e88190b006770db6fdb13d", "toolName": "web_search_preview", "type": "tool-call", }, { "providerExecuted": true, "result": { "status": "completed", }, "toolCallId": "ws_67cf2b3051e88190b006770db6fdb13d", "toolName": "web_search_preview", "type": "tool-result", }, { "providerMetadata": { "openai": { "itemId": "msg_67cf2b35467481908f24412e4fd40d66", }, }, "text": "As of June 23, 2025, here are some recent developments in San Francisco's tech scene:", "type": "text", }, { "id": "id-0", "sourceType": "url", "title": "Discover Tech Events & Activities in San Francisco, CA | Eventbrite", "type": "source", "url": "https://www.eventbrite.sg/d/ca--san-francisco/tech-events/?utm_source=openai", }, { "id": "id-1", "sourceType": "url", "title": "AI+ SF Summit: AI agents are the next big thing", "type": "source", "url": "https://www.axios.com/2024/12/10/ai-sf-summit-2024-roundup?utm_source=openai", }, ] `); }); it('should handle computer use tool calls', async () => { prepareJsonResponse({ id: 'resp_computer_test', object: 'response', created_at: 1741630255, status: 'completed', error: null, incomplete_details: null, instructions: null, max_output_tokens: null, model: 'gpt-4o-mini', output: [ { type: 'computer_call', id: 'computer_67cf2b3051e88190b006770db6fdb13d', status: 'completed', }, { type: 'message', id: 'msg_computer_test', status: 'completed', role: 'assistant', content: [ { type: 'output_text', text: "I've completed the computer task.", annotations: [], }, ], }, ], usage: { input_tokens: 100, output_tokens: 50 }, }); const result = await createModel('gpt-4o-mini').doGenerate({ prompt: [ { role: 'user' as const, content: [ { type: 'text' as const, text: 'Use the computer to complete a task.', }, ], }, ], tools: [ { type: 'provider-defined', id: 'openai.computer_use', name: 'computer_use', args: {}, }, ], }); expect(result.content).toMatchInlineSnapshot(` [ { "input": "", "providerExecuted": true, "toolCallId": "computer_67cf2b3051e88190b006770db6fdb13d", "toolName": "computer_use", "type": "tool-call", }, { "providerExecuted": true, "result": { "status": "completed", "type": "computer_use_tool_result", }, "toolCallId": "computer_67cf2b3051e88190b006770db6fdb13d", "toolName": "computer_use", "type": "tool-result", }, { "providerMetadata": { "openai": { "itemId": "msg_computer_test", }, }, "text": "I've completed the computer task.", "type": "text", }, ] `); }); }); }); }); --- File: /ai/packages/openai/src/responses/openai-responses-language-model.ts --- import { APICallError, LanguageModelV2, LanguageModelV2CallWarning, LanguageModelV2Content, LanguageModelV2FinishReason, LanguageModelV2StreamPart, LanguageModelV2Usage, } from '@ai-sdk/provider'; import { combineHeaders, createEventSourceResponseHandler, createJsonResponseHandler, generateId, parseProviderOptions, ParseResult, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { OpenAIConfig } from '../openai-config'; import { openaiFailedResponseHandler } from '../openai-error'; import { convertToOpenAIResponsesMessages } from './convert-to-openai-responses-messages'; import { mapOpenAIResponseFinishReason } from './map-openai-responses-finish-reason'; import { prepareResponsesTools } from './openai-responses-prepare-tools'; import { OpenAIResponsesModelId } from './openai-responses-settings'; export class OpenAIResponsesLanguageModel implements LanguageModelV2 { readonly specificationVersion = 'v2'; readonly modelId: OpenAIResponsesModelId; private readonly config: OpenAIConfig; constructor(modelId: OpenAIResponsesModelId, config: OpenAIConfig) { this.modelId = modelId; this.config = config; } readonly supportedUrls: Record<string, RegExp[]> = { 'image/*': [/^https?:\/\/.*$/], }; get provider(): string { return this.config.provider; } private async getArgs({ maxOutputTokens, temperature, stopSequences, topP, topK, presencePenalty, frequencyPenalty, seed, prompt, providerOptions, tools, toolChoice, responseFormat, }: Parameters<LanguageModelV2['doGenerate']>[0]) { const warnings: LanguageModelV2CallWarning[] = []; const modelConfig = getResponsesModelConfig(this.modelId); if (topK != null) { warnings.push({ type: 'unsupported-setting', setting: 'topK' }); } if (seed != null) { warnings.push({ type: 'unsupported-setting', setting: 'seed' }); } if (presencePenalty != null) { warnings.push({ type: 'unsupported-setting', setting: 'presencePenalty', }); } if (frequencyPenalty != null) { warnings.push({ type: 'unsupported-setting', setting: 'frequencyPenalty', }); } if (stopSequences != null) { warnings.push({ type: 'unsupported-setting', setting: 'stopSequences' }); } const { messages, warnings: messageWarnings } = await convertToOpenAIResponsesMessages({ prompt, systemMessageMode: modelConfig.systemMessageMode, }); warnings.push(...messageWarnings); const openaiOptions = await parseProviderOptions({ provider: 'openai', providerOptions, schema: openaiResponsesProviderOptionsSchema, }); const strictJsonSchema = openaiOptions?.strictJsonSchema ?? false; const baseArgs = { model: this.modelId, input: messages, temperature, top_p: topP, max_output_tokens: maxOutputTokens, ...(responseFormat?.type === 'json' && { text: { format: responseFormat.schema != null ? { type: 'json_schema', strict: strictJsonSchema, name: responseFormat.name ?? 'response', description: responseFormat.description, schema: responseFormat.schema, } : { type: 'json_object' }, }, }), // provider options: metadata: openaiOptions?.metadata, parallel_tool_calls: openaiOptions?.parallelToolCalls, previous_response_id: openaiOptions?.previousResponseId, store: openaiOptions?.store, user: openaiOptions?.user, instructions: openaiOptions?.instructions, service_tier: openaiOptions?.serviceTier, include: openaiOptions?.include, // model-specific settings: ...(modelConfig.isReasoningModel && (openaiOptions?.reasoningEffort != null || openaiOptions?.reasoningSummary != null) && { reasoning: { ...(openaiOptions?.reasoningEffort != null && { effort: openaiOptions.reasoningEffort, }), ...(openaiOptions?.reasoningSummary != null && { summary: openaiOptions.reasoningSummary, }), }, }), ...(modelConfig.requiredAutoTruncation && { truncation: 'auto', }), }; if (modelConfig.isReasoningModel) { // remove unsupported settings for reasoning models // see https://platform.openai.com/docs/guides/reasoning#limitations if (baseArgs.temperature != null) { baseArgs.temperature = undefined; warnings.push({ type: 'unsupported-setting', setting: 'temperature', details: 'temperature is not supported for reasoning models', }); } if (baseArgs.top_p != null) { baseArgs.top_p = undefined; warnings.push({ type: 'unsupported-setting', setting: 'topP', details: 'topP is not supported for reasoning models', }); } } else { if (openaiOptions?.reasoningEffort != null) { warnings.push({ type: 'unsupported-setting', setting: 'reasoningEffort', details: 'reasoningEffort is not supported for non-reasoning models', }); } if (openaiOptions?.reasoningSummary != null) { warnings.push({ type: 'unsupported-setting', setting: 'reasoningSummary', details: 'reasoningSummary is not supported for non-reasoning models', }); } } // Validate flex processing support if ( openaiOptions?.serviceTier === 'flex' && !supportsFlexProcessing(this.modelId) ) { warnings.push({ type: 'unsupported-setting', setting: 'serviceTier', details: 'flex processing is only available for o3 and o4-mini models', }); // Remove from args if not supported delete (baseArgs as any).service_tier; } // Validate priority processing support if ( openaiOptions?.serviceTier === 'priority' && !supportsPriorityProcessing(this.modelId) ) { warnings.push({ type: 'unsupported-setting', setting: 'serviceTier', details: 'priority processing is only available for supported models (GPT-4, o3, o4-mini) and requires Enterprise access', }); // Remove from args if not supported delete (baseArgs as any).service_tier; } const { tools: openaiTools, toolChoice: openaiToolChoice, toolWarnings, } = prepareResponsesTools({ tools, toolChoice, strictJsonSchema, }); return { args: { ...baseArgs, tools: openaiTools, tool_choice: openaiToolChoice, }, warnings: [...warnings, ...toolWarnings], }; } async doGenerate( options: Parameters<LanguageModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>> { const { args: body, warnings } = await this.getArgs(options); const url = this.config.url({ path: '/responses', modelId: this.modelId, }); const { responseHeaders, value: response, rawValue: rawResponse, } = await postJsonToApi({ url, headers: combineHeaders(this.config.headers(), options.headers), body, failedResponseHandler: openaiFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( z.object({ id: z.string(), created_at: z.number(), error: z .object({ code: z.string(), message: z.string(), }) .nullish(), model: z.string(), output: z.array( z.discriminatedUnion('type', [ z.object({ type: z.literal('message'), role: z.literal('assistant'), id: z.string(), content: z.array( z.object({ type: z.literal('output_text'), text: z.string(), annotations: z.array( z.object({ type: z.literal('url_citation'), start_index: z.number(), end_index: z.number(), url: z.string(), title: z.string(), }), ), }), ), }), z.object({ type: z.literal('function_call'), call_id: z.string(), name: z.string(), arguments: z.string(), id: z.string(), }), z.object({ type: z.literal('web_search_call'), id: z.string(), status: z.string().optional(), }), z.object({ type: z.literal('computer_call'), id: z.string(), status: z.string().optional(), }), z.object({ type: z.literal('file_search_call'), id: z.string(), status: z.string().optional(), }), z.object({ type: z.literal('reasoning'), id: z.string(), encrypted_content: z.string().nullish(), summary: z.array( z.object({ type: z.literal('summary_text'), text: z.string(), }), ), }), ]), ), incomplete_details: z.object({ reason: z.string() }).nullable(), usage: usageSchema, }), ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); if (response.error) { throw new APICallError({ message: response.error.message, url, requestBodyValues: body, statusCode: 400, responseHeaders, responseBody: rawResponse as string, isRetryable: false, }); } const content: Array<LanguageModelV2Content> = []; // map response content to content array for (const part of response.output) { switch (part.type) { case 'reasoning': { // when there are no summary parts, we need to add an empty reasoning part: if (part.summary.length === 0) { part.summary.push({ type: 'summary_text', text: '' }); } for (const summary of part.summary) { content.push({ type: 'reasoning' as const, text: summary.text, providerMetadata: { openai: { itemId: part.id, reasoningEncryptedContent: part.encrypted_content ?? null, }, }, }); } break; } case 'message': { for (const contentPart of part.content) { content.push({ type: 'text', text: contentPart.text, providerMetadata: { openai: { itemId: part.id, }, }, }); for (const annotation of contentPart.annotations) { content.push({ type: 'source', sourceType: 'url', id: this.config.generateId?.() ?? generateId(), url: annotation.url, title: annotation.title, }); } } break; } case 'function_call': { content.push({ type: 'tool-call', toolCallId: part.call_id, toolName: part.name, input: part.arguments, providerMetadata: { openai: { itemId: part.id, }, }, }); break; } case 'web_search_call': { content.push({ type: 'tool-call', toolCallId: part.id, toolName: 'web_search_preview', input: '', providerExecuted: true, }); content.push({ type: 'tool-result', toolCallId: part.id, toolName: 'web_search_preview', result: { status: part.status || 'completed' }, providerExecuted: true, }); break; } case 'computer_call': { content.push({ type: 'tool-call', toolCallId: part.id, toolName: 'computer_use', input: '', providerExecuted: true, }); content.push({ type: 'tool-result', toolCallId: part.id, toolName: 'computer_use', result: { type: 'computer_use_tool_result', status: part.status || 'completed', }, providerExecuted: true, }); break; } case 'file_search_call': { content.push({ type: 'tool-call', toolCallId: part.id, toolName: 'file_search', input: '', providerExecuted: true, }); content.push({ type: 'tool-result', toolCallId: part.id, toolName: 'file_search', result: { type: 'file_search_tool_result', status: part.status || 'completed', }, providerExecuted: true, }); break; } } } return { content, finishReason: mapOpenAIResponseFinishReason({ finishReason: response.incomplete_details?.reason, hasToolCalls: content.some(part => part.type === 'tool-call'), }), usage: { inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens, totalTokens: response.usage.input_tokens + response.usage.output_tokens, reasoningTokens: response.usage.output_tokens_details?.reasoning_tokens ?? undefined, cachedInputTokens: response.usage.input_tokens_details?.cached_tokens ?? undefined, }, request: { body }, response: { id: response.id, timestamp: new Date(response.created_at * 1000), modelId: response.model, headers: responseHeaders, body: rawResponse, }, providerMetadata: { openai: { responseId: response.id, }, }, warnings, }; } async doStream( options: Parameters<LanguageModelV2['doStream']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>> { const { args: body, warnings } = await this.getArgs(options); const { responseHeaders, value: response } = await postJsonToApi({ url: this.config.url({ path: '/responses', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body: { ...body, stream: true, }, failedResponseHandler: openaiFailedResponseHandler, successfulResponseHandler: createEventSourceResponseHandler( openaiResponsesChunkSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const self = this; let finishReason: LanguageModelV2FinishReason = 'unknown'; const usage: LanguageModelV2Usage = { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, }; let responseId: string | null = null; const ongoingToolCalls: Record< number, { toolName: string; toolCallId: string } | undefined > = {}; let hasToolCalls = false; const activeReasoning: Record< string, { encryptedContent?: string | null; summaryParts: number[]; } > = {}; return { stream: response.pipeThrough( new TransformStream< ParseResult<z.infer<typeof openaiResponsesChunkSchema>>, LanguageModelV2StreamPart >({ start(controller) { controller.enqueue({ type: 'stream-start', warnings }); }, transform(chunk, controller) { if (options.includeRawChunks) { controller.enqueue({ type: 'raw', rawValue: chunk.rawValue }); } // handle failed chunk parsing / validation: if (!chunk.success) { finishReason = 'error'; controller.enqueue({ type: 'error', error: chunk.error }); return; } const value = chunk.value; if (isResponseOutputItemAddedChunk(value)) { if (value.item.type === 'function_call') { ongoingToolCalls[value.output_index] = { toolName: value.item.name, toolCallId: value.item.call_id, }; controller.enqueue({ type: 'tool-input-start', id: value.item.call_id, toolName: value.item.name, }); } else if (value.item.type === 'web_search_call') { ongoingToolCalls[value.output_index] = { toolName: 'web_search_preview', toolCallId: value.item.id, }; controller.enqueue({ type: 'tool-input-start', id: value.item.id, toolName: 'web_search_preview', }); } else if (value.item.type === 'computer_call') { ongoingToolCalls[value.output_index] = { toolName: 'computer_use', toolCallId: value.item.id, }; controller.enqueue({ type: 'tool-input-start', id: value.item.id, toolName: 'computer_use', }); } else if (value.item.type === 'message') { controller.enqueue({ type: 'text-start', id: value.item.id, providerMetadata: { openai: { itemId: value.item.id, }, }, }); } else if (isResponseOutputItemAddedReasoningChunk(value)) { activeReasoning[value.item.id] = { encryptedContent: value.item.encrypted_content, summaryParts: [0], }; controller.enqueue({ type: 'reasoning-start', id: `${value.item.id}:0`, providerMetadata: { openai: { itemId: value.item.id, reasoningEncryptedContent: value.item.encrypted_content ?? null, }, }, }); } } else if (isResponseOutputItemDoneChunk(value)) { if (value.item.type === 'function_call') { ongoingToolCalls[value.output_index] = undefined; hasToolCalls = true; controller.enqueue({ type: 'tool-input-end', id: value.item.call_id, }); controller.enqueue({ type: 'tool-call', toolCallId: value.item.call_id, toolName: value.item.name, input: value.item.arguments, providerMetadata: { openai: { itemId: value.item.id, }, }, }); } else if (value.item.type === 'web_search_call') { ongoingToolCalls[value.output_index] = undefined; hasToolCalls = true; controller.enqueue({ type: 'tool-input-end', id: value.item.id, }); controller.enqueue({ type: 'tool-call', toolCallId: value.item.id, toolName: 'web_search_preview', input: '', providerExecuted: true, }); controller.enqueue({ type: 'tool-result', toolCallId: value.item.id, toolName: 'web_search_preview', result: { type: 'web_search_tool_result', status: value.item.status || 'completed', }, providerExecuted: true, }); } else if (value.item.type === 'computer_call') { ongoingToolCalls[value.output_index] = undefined; hasToolCalls = true; controller.enqueue({ type: 'tool-input-end', id: value.item.id, }); controller.enqueue({ type: 'tool-call', toolCallId: value.item.id, toolName: 'computer_use', input: '', providerExecuted: true, }); controller.enqueue({ type: 'tool-result', toolCallId: value.item.id, toolName: 'computer_use', result: { type: 'computer_use_tool_result', status: value.item.status || 'completed', }, providerExecuted: true, }); } else if (value.item.type === 'message') { controller.enqueue({ type: 'text-end', id: value.item.id, }); } else if (isResponseOutputItemDoneReasoningChunk(value)) { const activeReasoningPart = activeReasoning[value.item.id]; for (const summaryIndex of activeReasoningPart.summaryParts) { controller.enqueue({ type: 'reasoning-end', id: `${value.item.id}:${summaryIndex}`, providerMetadata: { openai: { itemId: value.item.id, reasoningEncryptedContent: value.item.encrypted_content ?? null, }, }, }); } delete activeReasoning[value.item.id]; } } else if (isResponseFunctionCallArgumentsDeltaChunk(value)) { const toolCall = ongoingToolCalls[value.output_index]; if (toolCall != null) { controller.enqueue({ type: 'tool-input-delta', id: toolCall.toolCallId, delta: value.delta, }); } } else if (isResponseCreatedChunk(value)) { responseId = value.response.id; controller.enqueue({ type: 'response-metadata', id: value.response.id, timestamp: new Date(value.response.created_at * 1000), modelId: value.response.model, }); } else if (isTextDeltaChunk(value)) { controller.enqueue({ type: 'text-delta', id: value.item_id, delta: value.delta, }); } else if (isResponseReasoningSummaryPartAddedChunk(value)) { // the first reasoning start is pushed in isResponseOutputItemAddedReasoningChunk. if (value.summary_index > 0) { activeReasoning[value.item_id]?.summaryParts.push( value.summary_index, ); controller.enqueue({ type: 'reasoning-start', id: `${value.item_id}:${value.summary_index}`, providerMetadata: { openai: { itemId: value.item_id, reasoningEncryptedContent: activeReasoning[value.item_id]?.encryptedContent ?? null, }, }, }); } } else if (isResponseReasoningSummaryTextDeltaChunk(value)) { controller.enqueue({ type: 'reasoning-delta', id: `${value.item_id}:${value.summary_index}`, delta: value.delta, providerMetadata: { openai: { itemId: value.item_id, }, }, }); } else if (isResponseFinishedChunk(value)) { finishReason = mapOpenAIResponseFinishReason({ finishReason: value.response.incomplete_details?.reason, hasToolCalls, }); usage.inputTokens = value.response.usage.input_tokens; usage.outputTokens = value.response.usage.output_tokens; usage.totalTokens = value.response.usage.input_tokens + value.response.usage.output_tokens; usage.reasoningTokens = value.response.usage.output_tokens_details?.reasoning_tokens ?? undefined; usage.cachedInputTokens = value.response.usage.input_tokens_details?.cached_tokens ?? undefined; } else if (isResponseAnnotationAddedChunk(value)) { controller.enqueue({ type: 'source', sourceType: 'url', id: self.config.generateId?.() ?? generateId(), url: value.annotation.url, title: value.annotation.title, }); } else if (isErrorChunk(value)) { controller.enqueue({ type: 'error', error: value }); } }, flush(controller) { controller.enqueue({ type: 'finish', finishReason, usage, providerMetadata: { openai: { responseId, }, }, }); }, }), ), request: { body }, response: { headers: responseHeaders }, }; } } const usageSchema = z.object({ input_tokens: z.number(), input_tokens_details: z .object({ cached_tokens: z.number().nullish() }) .nullish(), output_tokens: z.number(), output_tokens_details: z .object({ reasoning_tokens: z.number().nullish() }) .nullish(), }); const textDeltaChunkSchema = z.object({ type: z.literal('response.output_text.delta'), item_id: z.string(), delta: z.string(), }); const errorChunkSchema = z.object({ type: z.literal('error'), code: z.string(), message: z.string(), param: z.string().nullish(), sequence_number: z.number(), }); const responseFinishedChunkSchema = z.object({ type: z.enum(['response.completed', 'response.incomplete']), response: z.object({ incomplete_details: z.object({ reason: z.string() }).nullish(), usage: usageSchema, }), }); const responseCreatedChunkSchema = z.object({ type: z.literal('response.created'), response: z.object({ id: z.string(), created_at: z.number(), model: z.string(), }), }); const responseOutputItemAddedSchema = z.object({ type: z.literal('response.output_item.added'), output_index: z.number(), item: z.discriminatedUnion('type', [ z.object({ type: z.literal('message'), id: z.string(), }), z.object({ type: z.literal('reasoning'), id: z.string(), encrypted_content: z.string().nullish(), }), z.object({ type: z.literal('function_call'), id: z.string(), call_id: z.string(), name: z.string(), arguments: z.string(), }), z.object({ type: z.literal('web_search_call'), id: z.string(), status: z.string(), }), z.object({ type: z.literal('computer_call'), id: z.string(), status: z.string(), }), z.object({ type: z.literal('file_search_call'), id: z.string(), status: z.string(), }), ]), }); const responseOutputItemDoneSchema = z.object({ type: z.literal('response.output_item.done'), output_index: z.number(), item: z.discriminatedUnion('type', [ z.object({ type: z.literal('message'), id: z.string(), }), z.object({ type: z.literal('reasoning'), id: z.string(), encrypted_content: z.string().nullish(), }), z.object({ type: z.literal('function_call'), id: z.string(), call_id: z.string(), name: z.string(), arguments: z.string(), status: z.literal('completed'), }), z.object({ type: z.literal('web_search_call'), id: z.string(), status: z.literal('completed'), }), z.object({ type: z.literal('computer_call'), id: z.string(), status: z.literal('completed'), }), z.object({ type: z.literal('file_search_call'), id: z.string(), status: z.literal('completed'), }), ]), }); const responseFunctionCallArgumentsDeltaSchema = z.object({ type: z.literal('response.function_call_arguments.delta'), item_id: z.string(), output_index: z.number(), delta: z.string(), }); const responseAnnotationAddedSchema = z.object({ type: z.literal('response.output_text.annotation.added'), annotation: z.object({ type: z.literal('url_citation'), url: z.string(), title: z.string(), }), }); const responseReasoningSummaryPartAddedSchema = z.object({ type: z.literal('response.reasoning_summary_part.added'), item_id: z.string(), summary_index: z.number(), }); const responseReasoningSummaryTextDeltaSchema = z.object({ type: z.literal('response.reasoning_summary_text.delta'), item_id: z.string(), summary_index: z.number(), delta: z.string(), }); const openaiResponsesChunkSchema = z.union([ textDeltaChunkSchema, responseFinishedChunkSchema, responseCreatedChunkSchema, responseOutputItemAddedSchema, responseOutputItemDoneSchema, responseFunctionCallArgumentsDeltaSchema, responseAnnotationAddedSchema, responseReasoningSummaryPartAddedSchema, responseReasoningSummaryTextDeltaSchema, errorChunkSchema, z.object({ type: z.string() }).loose(), // fallback for unknown chunks ]); type ExtractByType< T, K extends T extends { type: infer U } ? U : never, > = T extends { type: K } ? T : never; function isTextDeltaChunk( chunk: z.infer<typeof openaiResponsesChunkSchema>, ): chunk is z.infer<typeof textDeltaChunkSchema> { return chunk.type === 'response.output_text.delta'; } function isResponseOutputItemDoneChunk( chunk: z.infer<typeof openaiResponsesChunkSchema>, ): chunk is z.infer<typeof responseOutputItemDoneSchema> { return chunk.type === 'response.output_item.done'; } function isResponseOutputItemDoneReasoningChunk( chunk: z.infer<typeof openaiResponsesChunkSchema>, ): chunk is z.infer<typeof responseOutputItemDoneSchema> & { item: ExtractByType< z.infer<typeof responseOutputItemDoneSchema>['item'], 'reasoning' >; } { return ( isResponseOutputItemDoneChunk(chunk) && chunk.item.type === 'reasoning' ); } function isResponseFinishedChunk( chunk: z.infer<typeof openaiResponsesChunkSchema>, ): chunk is z.infer<typeof responseFinishedChunkSchema> { return ( chunk.type === 'response.completed' || chunk.type === 'response.incomplete' ); } function isResponseCreatedChunk( chunk: z.infer<typeof openaiResponsesChunkSchema>, ): chunk is z.infer<typeof responseCreatedChunkSchema> { return chunk.type === 'response.created'; } function isResponseFunctionCallArgumentsDeltaChunk( chunk: z.infer<typeof openaiResponsesChunkSchema>, ): chunk is z.infer<typeof responseFunctionCallArgumentsDeltaSchema> { return chunk.type === 'response.function_call_arguments.delta'; } function isResponseOutputItemAddedChunk( chunk: z.infer<typeof openaiResponsesChunkSchema>, ): chunk is z.infer<typeof responseOutputItemAddedSchema> { return chunk.type === 'response.output_item.added'; } function isResponseOutputItemAddedReasoningChunk( chunk: z.infer<typeof openaiResponsesChunkSchema>, ): chunk is z.infer<typeof responseOutputItemAddedSchema> & { item: ExtractByType< z.infer<typeof responseOutputItemAddedSchema>['item'], 'reasoning' >; } { return ( isResponseOutputItemAddedChunk(chunk) && chunk.item.type === 'reasoning' ); } function isResponseAnnotationAddedChunk( chunk: z.infer<typeof openaiResponsesChunkSchema>, ): chunk is z.infer<typeof responseAnnotationAddedSchema> { return chunk.type === 'response.output_text.annotation.added'; } function isResponseReasoningSummaryPartAddedChunk( chunk: z.infer<typeof openaiResponsesChunkSchema>, ): chunk is z.infer<typeof responseReasoningSummaryPartAddedSchema> { return chunk.type === 'response.reasoning_summary_part.added'; } function isResponseReasoningSummaryTextDeltaChunk( chunk: z.infer<typeof openaiResponsesChunkSchema>, ): chunk is z.infer<typeof responseReasoningSummaryTextDeltaSchema> { return chunk.type === 'response.reasoning_summary_text.delta'; } function isErrorChunk( chunk: z.infer<typeof openaiResponsesChunkSchema>, ): chunk is z.infer<typeof errorChunkSchema> { return chunk.type === 'error'; } type ResponsesModelConfig = { isReasoningModel: boolean; systemMessageMode: 'remove' | 'system' | 'developer'; requiredAutoTruncation: boolean; }; function getResponsesModelConfig(modelId: string): ResponsesModelConfig { // o series reasoning models: if ( modelId.startsWith('o') || modelId.startsWith('codex-') || modelId.startsWith('computer-use') ) { if (modelId.startsWith('o1-mini') || modelId.startsWith('o1-preview')) { return { isReasoningModel: true, systemMessageMode: 'remove', requiredAutoTruncation: false, }; } return { isReasoningModel: true, systemMessageMode: 'developer', requiredAutoTruncation: false, }; } // gpt models: return { isReasoningModel: false, systemMessageMode: 'system', requiredAutoTruncation: false, }; } function supportsFlexProcessing(modelId: string): boolean { return modelId.startsWith('o3') || modelId.startsWith('o4-mini'); } function supportsPriorityProcessing(modelId: string): boolean { return ( modelId.startsWith('gpt-4') || modelId.startsWith('o3') || modelId.startsWith('o4-mini') ); } const openaiResponsesProviderOptionsSchema = z.object({ metadata: z.any().nullish(), parallelToolCalls: z.boolean().nullish(), previousResponseId: z.string().nullish(), store: z.boolean().nullish(), user: z.string().nullish(), reasoningEffort: z.string().nullish(), strictJsonSchema: z.boolean().nullish(), instructions: z.string().nullish(), reasoningSummary: z.string().nullish(), serviceTier: z.enum(['auto', 'flex', 'priority']).nullish(), include: z .array(z.enum(['reasoning.encrypted_content', 'file_search_call.results'])) .nullish(), }); export type OpenAIResponsesProviderOptions = z.infer< typeof openaiResponsesProviderOptionsSchema >; --- File: /ai/packages/openai/src/responses/openai-responses-prepare-tools.ts --- import { LanguageModelV2CallOptions, LanguageModelV2CallWarning, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { OpenAIResponsesTool } from './openai-responses-api-types'; import { fileSearchArgsSchema } from '../tool/file-search'; export function prepareResponsesTools({ tools, toolChoice, strictJsonSchema, }: { tools: LanguageModelV2CallOptions['tools']; toolChoice?: LanguageModelV2CallOptions['toolChoice']; strictJsonSchema: boolean; }): { tools?: Array<OpenAIResponsesTool>; toolChoice?: | 'auto' | 'none' | 'required' | { type: 'file_search' } | { type: 'web_search_preview' } | { type: 'function'; name: string }; toolWarnings: LanguageModelV2CallWarning[]; } { // when the tools array is empty, change it to undefined to prevent errors: tools = tools?.length ? tools : undefined; const toolWarnings: LanguageModelV2CallWarning[] = []; if (tools == null) { return { tools: undefined, toolChoice: undefined, toolWarnings }; } const openaiTools: Array<OpenAIResponsesTool> = []; for (const tool of tools) { switch (tool.type) { case 'function': openaiTools.push({ type: 'function', name: tool.name, description: tool.description, parameters: tool.inputSchema, strict: strictJsonSchema, }); break; case 'provider-defined': switch (tool.id) { case 'openai.file_search': { const args = fileSearchArgsSchema.parse(tool.args); openaiTools.push({ type: 'file_search', vector_store_ids: args.vectorStoreIds, max_num_results: args.maxNumResults, ranking_options: args.ranking ? { ranker: args.ranking.ranker } : undefined, filters: args.filters, }); break; } case 'openai.web_search_preview': openaiTools.push({ type: 'web_search_preview', search_context_size: tool.args.searchContextSize as | 'low' | 'medium' | 'high', user_location: tool.args.userLocation as { type: 'approximate'; city: string; region: string; }, }); break; default: toolWarnings.push({ type: 'unsupported-tool', tool }); break; } break; default: toolWarnings.push({ type: 'unsupported-tool', tool }); break; } } if (toolChoice == null) { return { tools: openaiTools, toolChoice: undefined, toolWarnings }; } const type = toolChoice.type; switch (type) { case 'auto': case 'none': case 'required': return { tools: openaiTools, toolChoice: type, toolWarnings }; case 'tool': return { tools: openaiTools, toolChoice: toolChoice.toolName === 'file_search' ? { type: 'file_search' } : toolChoice.toolName === 'web_search_preview' ? { type: 'web_search_preview' } : { type: 'function', name: toolChoice.toolName }, toolWarnings, }; default: { const _exhaustiveCheck: never = type; throw new UnsupportedFunctionalityError({ functionality: `tool choice type: ${_exhaustiveCheck}`, }); } } } --- File: /ai/packages/openai/src/responses/openai-responses-settings.ts --- export const openaiResponsesReasoningModelIds = [ 'o1', 'o1-2024-12-17', 'o1-mini', 'o1-mini-2024-09-12', 'o1-preview', 'o1-preview-2024-09-12', 'o3-mini', 'o3-mini-2025-01-31', 'o3', 'o3-2025-04-16', 'o4-mini', 'o4-mini-2025-04-16', 'codex-mini-latest', 'computer-use-preview', ] as const; export const openaiResponsesModelIds = [ 'gpt-4.1', 'gpt-4.1-2025-04-14', 'gpt-4.1-mini', 'gpt-4.1-mini-2025-04-14', 'gpt-4.1-nano', 'gpt-4.1-nano-2025-04-14', 'gpt-4o', 'gpt-4o-2024-05-13', 'gpt-4o-2024-08-06', 'gpt-4o-2024-11-20', 'gpt-4o-audio-preview', 'gpt-4o-audio-preview-2024-10-01', 'gpt-4o-audio-preview-2024-12-17', 'gpt-4o-search-preview', 'gpt-4o-search-preview-2025-03-11', 'gpt-4o-mini-search-preview', 'gpt-4o-mini-search-preview-2025-03-11', 'gpt-4o-mini', 'gpt-4o-mini-2024-07-18', 'gpt-4-turbo', 'gpt-4-turbo-2024-04-09', 'gpt-4-turbo-preview', 'gpt-4-0125-preview', 'gpt-4-1106-preview', 'gpt-4', 'gpt-4-0613', 'gpt-4.5-preview', 'gpt-4.5-preview-2025-02-27', 'gpt-3.5-turbo-0125', 'gpt-3.5-turbo', 'gpt-3.5-turbo-1106', 'chatgpt-4o-latest', ...openaiResponsesReasoningModelIds, ] as const; export type OpenAIResponsesModelId = | (typeof openaiResponsesModelIds)[number] | (string & {}); --- File: /ai/packages/openai/src/tool/file-search.ts --- import { createProviderDefinedToolFactory } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; // Filter schemas const comparisonFilterSchema = z.object({ key: z.string(), type: z.enum(['eq', 'ne', 'gt', 'gte', 'lt', 'lte']), value: z.union([z.string(), z.number(), z.boolean()]), }); const compoundFilterSchema: z.ZodType<any> = z.object({ type: z.enum(['and', 'or']), filters: z.array( z.union([comparisonFilterSchema, z.lazy(() => compoundFilterSchema)]), ), }); const filtersSchema = z.union([comparisonFilterSchema, compoundFilterSchema]); // Args validation schema export const fileSearchArgsSchema = z.object({ /** * List of vector store IDs to search through. If not provided, searches all available vector stores. */ vectorStoreIds: z.array(z.string()).optional(), /** * Maximum number of search results to return. Defaults to 10. */ maxNumResults: z.number().optional(), /** * Ranking options for the search. */ ranking: z .object({ ranker: z.enum(['auto', 'default-2024-08-21']).optional(), }) .optional(), /** * A filter to apply based on file attributes. */ filters: filtersSchema.optional(), }); export const fileSearch = createProviderDefinedToolFactory< { /** * The search query to execute. */ query: string; }, { /** * List of vector store IDs to search through. If not provided, searches all available vector stores. */ vectorStoreIds?: string[]; /** * Maximum number of search results to return. Defaults to 10. */ maxNumResults?: number; /** * Ranking options for the search. */ ranking?: { ranker?: 'auto' | 'default-2024-08-21'; }; /** * A filter to apply based on file attributes. */ filters?: | { key: string; type: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte'; value: string | number | boolean; } | { type: 'and' | 'or'; filters: any[]; }; } >({ id: 'openai.file_search', name: 'file_search', inputSchema: z.object({ query: z.string(), }), }); --- File: /ai/packages/openai/src/tool/web-search-preview.ts --- import { createProviderDefinedToolFactory } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; // Args validation schema export const webSearchPreviewArgsSchema = z.object({ /** * Search context size to use for the web search. * - high: Most comprehensive context, highest cost, slower response * - medium: Balanced context, cost, and latency (default) * - low: Least context, lowest cost, fastest response */ searchContextSize: z.enum(['low', 'medium', 'high']).optional(), /** * User location information to provide geographically relevant search results. */ userLocation: z .object({ /** * Type of location (always 'approximate') */ type: z.literal('approximate'), /** * Two-letter ISO country code (e.g., 'US', 'GB') */ country: z.string().optional(), /** * City name (free text, e.g., 'Minneapolis') */ city: z.string().optional(), /** * Region name (free text, e.g., 'Minnesota') */ region: z.string().optional(), /** * IANA timezone (e.g., 'America/Chicago') */ timezone: z.string().optional(), }) .optional(), }); export const webSearchPreview = createProviderDefinedToolFactory< { // Web search doesn't take input parameters - it's controlled by the prompt }, { /** * Search context size to use for the web search. * - high: Most comprehensive context, highest cost, slower response * - medium: Balanced context, cost, and latency (default) * - low: Least context, lowest cost, fastest response */ searchContextSize?: 'low' | 'medium' | 'high'; /** * User location information to provide geographically relevant search results. */ userLocation?: { /** * Type of location (always 'approximate') */ type: 'approximate'; /** * Two-letter ISO country code (e.g., 'US', 'GB') */ country?: string; /** * City name (free text, e.g., 'Minneapolis') */ city?: string; /** * Region name (free text, e.g., 'Minnesota') */ region?: string; /** * IANA timezone (e.g., 'America/Chicago') */ timezone?: string; }; } >({ id: 'openai.web_search_preview', name: 'web_search_preview', inputSchema: z.object({}), }); --- File: /ai/packages/openai/src/convert-to-openai-chat-messages.test.ts --- import { convertToOpenAIChatMessages } from './convert-to-openai-chat-messages'; describe('system messages', () => { it('should forward system messages', async () => { const result = convertToOpenAIChatMessages({ prompt: [{ role: 'system', content: 'You are a helpful assistant.' }], }); expect(result.messages).toEqual([ { role: 'system', content: 'You are a helpful assistant.' }, ]); }); it('should convert system messages to developer messages when requested', async () => { const result = convertToOpenAIChatMessages({ prompt: [{ role: 'system', content: 'You are a helpful assistant.' }], systemMessageMode: 'developer', }); expect(result.messages).toEqual([ { role: 'developer', content: 'You are a helpful assistant.' }, ]); }); it('should remove system messages when requested', async () => { const result = convertToOpenAIChatMessages({ prompt: [{ role: 'system', content: 'You are a helpful assistant.' }], systemMessageMode: 'remove', }); expect(result.messages).toEqual([]); }); }); describe('user messages', () => { it('should convert messages with only a text part to a string content', async () => { const result = convertToOpenAIChatMessages({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'Hello' }], }, ], }); expect(result.messages).toEqual([{ role: 'user', content: 'Hello' }]); }); it('should convert messages with image parts', async () => { const result = convertToOpenAIChatMessages({ prompt: [ { role: 'user', content: [ { type: 'text', text: 'Hello' }, { type: 'file', mediaType: 'image/png', data: Buffer.from([0, 1, 2, 3]).toString('base64'), }, ], }, ], }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'text', text: 'Hello' }, { type: 'image_url', image_url: { url: 'data:image/png;base64,AAECAw==' }, }, ], }, ]); }); it('should add image detail when specified through extension', async () => { const result = convertToOpenAIChatMessages({ prompt: [ { role: 'user', content: [ { type: 'file', mediaType: 'image/png', data: Buffer.from([0, 1, 2, 3]).toString('base64'), providerOptions: { openai: { imageDetail: 'low', }, }, }, ], }, ], }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'image_url', image_url: { url: 'data:image/png;base64,AAECAw==', detail: 'low', }, }, ], }, ]); }); describe('file parts', () => { it('should throw for unsupported mime types', () => { expect(() => convertToOpenAIChatMessages({ prompt: [ { role: 'user', content: [ { type: 'file', data: 'AAECAw==', mediaType: 'application/something', }, ], }, ], }), ).toThrow('file part media type application/something'); }); it('should throw for URL data', () => { expect(() => convertToOpenAIChatMessages({ prompt: [ { role: 'user', content: [ { type: 'file', data: new URL('https://example.com/foo.wav'), mediaType: 'audio/wav', }, ], }, ], }), ).toThrow('audio file parts with URLs'); }); it('should add audio content for audio/wav file parts', () => { const result = convertToOpenAIChatMessages({ prompt: [ { role: 'user', content: [ { type: 'file', data: 'AAECAw==', mediaType: 'audio/wav', }, ], }, ], }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'input_audio', input_audio: { data: 'AAECAw==', format: 'wav' }, }, ], }, ]); }); it('should add audio content for audio/mpeg file parts', () => { const result = convertToOpenAIChatMessages({ prompt: [ { role: 'user', content: [ { type: 'file', data: 'AAECAw==', mediaType: 'audio/mpeg', }, ], }, ], }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'input_audio', input_audio: { data: 'AAECAw==', format: 'mp3' }, }, ], }, ]); }); it('should add audio content for audio/mp3 file parts', () => { const result = convertToOpenAIChatMessages({ prompt: [ { role: 'user', content: [ { type: 'file', data: 'AAECAw==', mediaType: 'audio/mp3', // not official but sometimes used }, ], }, ], }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'input_audio', input_audio: { data: 'AAECAw==', format: 'mp3' }, }, ], }, ]); }); it('should convert messages with PDF file parts', async () => { const base64Data = 'AQIDBAU='; // Base64 encoding of pdfData const result = convertToOpenAIChatMessages({ prompt: [ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data: base64Data, filename: 'document.pdf', }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'file', file: { filename: 'document.pdf', file_data: 'data:application/pdf;base64,AQIDBAU=', }, }, ], }, ]); }); it('should convert messages with binary PDF file parts', async () => { const data = Uint8Array.from([1, 2, 3, 4, 5]); const result = convertToOpenAIChatMessages({ prompt: [ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data, filename: 'document.pdf', }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'file', file: { filename: 'document.pdf', file_data: 'data:application/pdf;base64,AQIDBAU=', }, }, ], }, ]); }); it('should convert messages with PDF file parts using file_id', async () => { const result = convertToOpenAIChatMessages({ prompt: [ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data: 'file-pdf-12345', }, ], }, ], }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'file', file: { file_id: 'file-pdf-12345', }, }, ], }, ]); }); it('should use default filename for PDF file parts when not provided', async () => { const base64Data = 'AQIDBAU='; const result = convertToOpenAIChatMessages({ prompt: [ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data: base64Data, }, ], }, ], systemMessageMode: 'system', }); expect(result.messages).toEqual([ { role: 'user', content: [ { type: 'file', file: { filename: 'part-0.pdf', file_data: 'data:application/pdf;base64,AQIDBAU=', }, }, ], }, ]); }); it('should throw error for unsupported file types', async () => { const base64Data = 'AQIDBAU='; expect(() => { convertToOpenAIChatMessages({ prompt: [ { role: 'user', content: [ { type: 'file', mediaType: 'text/plain', data: base64Data, }, ], }, ], systemMessageMode: 'system', }); }).toThrow('file part media type text/plain'); }); it('should throw error for file URLs', async () => { expect(() => { convertToOpenAIChatMessages({ prompt: [ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data: new URL('https://example.com/document.pdf'), }, ], }, ], systemMessageMode: 'system', }); }).toThrow('PDF file parts with URLs'); }); }); }); describe('tool calls', () => { it('should stringify arguments to tool calls', () => { const result = convertToOpenAIChatMessages({ prompt: [ { role: 'assistant', content: [ { type: 'tool-call', input: { foo: 'bar123' }, toolCallId: 'quux', toolName: 'thwomp', }, ], }, { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'quux', toolName: 'thwomp', output: { type: 'json', value: { oof: '321rab' } }, }, ], }, ], }); expect(result.messages).toEqual([ { role: 'assistant', content: '', tool_calls: [ { type: 'function', id: 'quux', function: { name: 'thwomp', arguments: JSON.stringify({ foo: 'bar123' }), }, }, ], }, { role: 'tool', content: JSON.stringify({ oof: '321rab' }), tool_call_id: 'quux', }, ]); }); it('should handle different tool output types', () => { const result = convertToOpenAIChatMessages({ prompt: [ { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'text-tool', toolName: 'text-tool', output: { type: 'text', value: 'Hello world' }, }, { type: 'tool-result', toolCallId: 'error-tool', toolName: 'error-tool', output: { type: 'error-text', value: 'Something went wrong' }, }, ], }, ], }); expect(result.messages).toMatchInlineSnapshot(` [ { "content": "Hello world", "role": "tool", "tool_call_id": "text-tool", }, { "content": "Something went wrong", "role": "tool", "tool_call_id": "error-tool", }, ] `); }); }); --- File: /ai/packages/openai/src/convert-to-openai-chat-messages.ts --- import { LanguageModelV2CallWarning, LanguageModelV2Prompt, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { OpenAIChatPrompt } from './openai-chat-prompt'; import { convertToBase64 } from '@ai-sdk/provider-utils'; export function convertToOpenAIChatMessages({ prompt, systemMessageMode = 'system', }: { prompt: LanguageModelV2Prompt; systemMessageMode?: 'system' | 'developer' | 'remove'; }): { messages: OpenAIChatPrompt; warnings: Array<LanguageModelV2CallWarning>; } { const messages: OpenAIChatPrompt = []; const warnings: Array<LanguageModelV2CallWarning> = []; for (const { role, content } of prompt) { switch (role) { case 'system': { switch (systemMessageMode) { case 'system': { messages.push({ role: 'system', content }); break; } case 'developer': { messages.push({ role: 'developer', content }); break; } case 'remove': { warnings.push({ type: 'other', message: 'system messages are removed for this model', }); break; } default: { const _exhaustiveCheck: never = systemMessageMode; throw new Error( `Unsupported system message mode: ${_exhaustiveCheck}`, ); } } break; } case 'user': { if (content.length === 1 && content[0].type === 'text') { messages.push({ role: 'user', content: content[0].text }); break; } messages.push({ role: 'user', content: content.map((part, index) => { switch (part.type) { case 'text': { return { type: 'text', text: part.text }; } case 'file': { if (part.mediaType.startsWith('image/')) { const mediaType = part.mediaType === 'image/*' ? 'image/jpeg' : part.mediaType; return { type: 'image_url', image_url: { url: part.data instanceof URL ? part.data.toString() : `data:${mediaType};base64,${convertToBase64(part.data)}`, // OpenAI specific extension: image detail detail: part.providerOptions?.openai?.imageDetail, }, }; } else if (part.mediaType.startsWith('audio/')) { if (part.data instanceof URL) { throw new UnsupportedFunctionalityError({ functionality: 'audio file parts with URLs', }); } switch (part.mediaType) { case 'audio/wav': { return { type: 'input_audio', input_audio: { data: convertToBase64(part.data), format: 'wav', }, }; } case 'audio/mp3': case 'audio/mpeg': { return { type: 'input_audio', input_audio: { data: convertToBase64(part.data), format: 'mp3', }, }; } default: { throw new UnsupportedFunctionalityError({ functionality: `audio content parts with media type ${part.mediaType}`, }); } } } else if (part.mediaType === 'application/pdf') { if (part.data instanceof URL) { throw new UnsupportedFunctionalityError({ functionality: 'PDF file parts with URLs', }); } return { type: 'file', file: typeof part.data === 'string' && part.data.startsWith('file-') ? { file_id: part.data } : { filename: part.filename ?? `part-${index}.pdf`, file_data: `data:application/pdf;base64,${convertToBase64(part.data)}`, }, }; } else { throw new UnsupportedFunctionalityError({ functionality: `file part media type ${part.mediaType}`, }); } } } }), }); break; } case 'assistant': { let text = ''; const toolCalls: Array<{ id: string; type: 'function'; function: { name: string; arguments: string }; }> = []; for (const part of content) { switch (part.type) { case 'text': { text += part.text; break; } case 'tool-call': { toolCalls.push({ id: part.toolCallId, type: 'function', function: { name: part.toolName, arguments: JSON.stringify(part.input), }, }); break; } } } messages.push({ role: 'assistant', content: text, tool_calls: toolCalls.length > 0 ? toolCalls : undefined, }); break; } case 'tool': { for (const toolResponse of content) { const output = toolResponse.output; let contentValue: string; switch (output.type) { case 'text': case 'error-text': contentValue = output.value; break; case 'content': case 'json': case 'error-json': contentValue = JSON.stringify(output.value); break; } messages.push({ role: 'tool', tool_call_id: toolResponse.toolCallId, content: contentValue, }); } break; } default: { const _exhaustiveCheck: never = role; throw new Error(`Unsupported role: ${_exhaustiveCheck}`); } } } return { messages, warnings }; } --- File: /ai/packages/openai/src/convert-to-openai-completion-prompt.ts --- import { InvalidPromptError, LanguageModelV2Prompt, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; export function convertToOpenAICompletionPrompt({ prompt, user = 'user', assistant = 'assistant', }: { prompt: LanguageModelV2Prompt; user?: string; assistant?: string; }): { prompt: string; stopSequences?: string[]; } { // transform to a chat message format: let text = ''; // if first message is a system message, add it to the text: if (prompt[0].role === 'system') { text += `${prompt[0].content}\n\n`; prompt = prompt.slice(1); } for (const { role, content } of prompt) { switch (role) { case 'system': { throw new InvalidPromptError({ message: 'Unexpected system message in prompt: ${content}', prompt, }); } case 'user': { const userMessage = content .map(part => { switch (part.type) { case 'text': { return part.text; } } }) .filter(Boolean) .join(''); text += `${user}:\n${userMessage}\n\n`; break; } case 'assistant': { const assistantMessage = content .map(part => { switch (part.type) { case 'text': { return part.text; } case 'tool-call': { throw new UnsupportedFunctionalityError({ functionality: 'tool-call messages', }); } } }) .join(''); text += `${assistant}:\n${assistantMessage}\n\n`; break; } case 'tool': { throw new UnsupportedFunctionalityError({ functionality: 'tool messages', }); } default: { const _exhaustiveCheck: never = role; throw new Error(`Unsupported role: ${_exhaustiveCheck}`); } } } // Assistant message prefix: text += `${assistant}:\n`; return { prompt: text, stopSequences: [`\n${user}:`], }; } --- File: /ai/packages/openai/src/get-response-metadata.ts --- export function getResponseMetadata({ id, model, created, }: { id?: string | undefined | null; created?: number | undefined | null; model?: string | undefined | null; }) { return { id: id ?? undefined, modelId: model ?? undefined, timestamp: created != null ? new Date(created * 1000) : undefined, }; } --- File: /ai/packages/openai/src/index.ts --- export { createOpenAI, openai } from './openai-provider'; export type { OpenAIProvider, OpenAIProviderSettings } from './openai-provider'; export type { OpenAIResponsesProviderOptions } from './responses/openai-responses-language-model'; --- File: /ai/packages/openai/src/map-openai-finish-reason.ts --- import { LanguageModelV2FinishReason } from '@ai-sdk/provider'; export function mapOpenAIFinishReason( finishReason: string | null | undefined, ): LanguageModelV2FinishReason { switch (finishReason) { case 'stop': return 'stop'; case 'length': return 'length'; case 'content_filter': return 'content-filter'; case 'function_call': case 'tool_calls': return 'tool-calls'; default: return 'unknown'; } } --- File: /ai/packages/openai/src/openai-api-types.ts --- export type OpenAISpeechAPITypes = { /** * The voice to use when generating the audio. * Supported voices are alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer, and verse. * @default 'alloy' */ voice?: | 'alloy' | 'ash' | 'ballad' | 'coral' | 'echo' | 'fable' | 'onyx' | 'nova' | 'sage' | 'shimmer' | 'verse'; /** * The speed of the generated audio. * Select a value from 0.25 to 4.0. * @default 1.0 */ speed?: number; /** * The format of the generated audio. * @default 'mp3' */ response_format?: 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm'; /** * Instructions for the speech generation e.g. "Speak in a slow and steady tone". * Does not work with tts-1 or tts-1-hd. */ instructions?: string; }; --- File: /ai/packages/openai/src/openai-chat-language-model.test.ts --- import { LanguageModelV2Prompt } from '@ai-sdk/provider'; import { convertReadableStreamToArray, createTestServer, isNodeVersion, } from '@ai-sdk/provider-utils/test'; import { createOpenAI } from './openai-provider'; const TEST_PROMPT: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; const TEST_LOGPROBS = { content: [ { token: 'Hello', logprob: -0.0009994634, top_logprobs: [ { token: 'Hello', logprob: -0.0009994634, }, ], }, { token: '!', logprob: -0.13410144, top_logprobs: [ { token: '!', logprob: -0.13410144, }, ], }, { token: ' How', logprob: -0.0009250381, top_logprobs: [ { token: ' How', logprob: -0.0009250381, }, ], }, { token: ' can', logprob: -0.047709424, top_logprobs: [ { token: ' can', logprob: -0.047709424, }, ], }, { token: ' I', logprob: -0.000009014684, top_logprobs: [ { token: ' I', logprob: -0.000009014684, }, ], }, { token: ' assist', logprob: -0.009125131, top_logprobs: [ { token: ' assist', logprob: -0.009125131, }, ], }, { token: ' you', logprob: -0.0000066306106, top_logprobs: [ { token: ' you', logprob: -0.0000066306106, }, ], }, { token: ' today', logprob: -0.00011093382, top_logprobs: [ { token: ' today', logprob: -0.00011093382, }, ], }, { token: '?', logprob: -0.00004596782, top_logprobs: [ { token: '?', logprob: -0.00004596782, }, ], }, ], }; const provider = createOpenAI({ apiKey: 'test-api-key', }); const model = provider.chat('gpt-3.5-turbo'); const server = createTestServer({ 'https://api.openai.com/v1/chat/completions': {}, }); describe('doGenerate', () => { function prepareJsonResponse({ content = '', tool_calls, function_call, annotations, usage = { prompt_tokens: 4, total_tokens: 34, completion_tokens: 30, }, finish_reason = 'stop', id = 'chatcmpl-95ZTZkhr0mHNKqerQfiwkuox3PHAd', created = 1711115037, model = 'gpt-3.5-turbo-0125', logprobs = null, headers, }: { content?: string; tool_calls?: Array<{ id: string; type: 'function'; function: { name: string; arguments: string; }; }>; function_call?: { name: string; arguments: string; }; annotations?: Array<{ type: 'url_citation'; start_index: number; end_index: number; url: string; title: string; }>; logprobs?: { content: | { token: string; logprob: number; top_logprobs: { token: string; logprob: number }[]; }[] | null; } | null; usage?: { prompt_tokens?: number; total_tokens?: number; completion_tokens?: number; completion_tokens_details?: { reasoning_tokens?: number; accepted_prediction_tokens?: number; rejected_prediction_tokens?: number; }; prompt_tokens_details?: { cached_tokens?: number; }; }; finish_reason?: string; created?: number; id?: string; model?: string; headers?: Record<string, string>; } = {}) { server.urls['https://api.openai.com/v1/chat/completions'].response = { type: 'json-value', headers, body: { id, object: 'chat.completion', created, model, choices: [ { index: 0, message: { role: 'assistant', content, tool_calls, function_call, annotations, }, ...(logprobs ? { logprobs } : {}), finish_reason, }, ], usage, system_fingerprint: 'fp_3bc1b5746c', }, }; } it('should extract text response', async () => { prepareJsonResponse({ content: 'Hello, World!' }); const result = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(result.content).toMatchInlineSnapshot(` [ { "text": "Hello, World!", "type": "text", }, ] `); }); it('should extract usage', async () => { prepareJsonResponse({ usage: { prompt_tokens: 20, total_tokens: 25, completion_tokens: 5 }, }); const { usage } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(usage).toMatchInlineSnapshot(` { "cachedInputTokens": undefined, "inputTokens": 20, "outputTokens": 5, "reasoningTokens": undefined, "totalTokens": 25, } `); }); it('should send request body', async () => { prepareJsonResponse({}); const { request } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(request).toMatchInlineSnapshot(` { "body": { "frequency_penalty": undefined, "logit_bias": undefined, "logprobs": undefined, "max_completion_tokens": undefined, "max_tokens": undefined, "messages": [ { "content": "Hello", "role": "user", }, ], "metadata": undefined, "model": "gpt-3.5-turbo", "parallel_tool_calls": undefined, "prediction": undefined, "presence_penalty": undefined, "reasoning_effort": undefined, "response_format": undefined, "seed": undefined, "service_tier": undefined, "stop": undefined, "store": undefined, "temperature": undefined, "tool_choice": undefined, "tools": undefined, "top_logprobs": undefined, "top_p": undefined, "user": undefined, }, } `); }); it('should send additional response information', async () => { prepareJsonResponse({ id: 'test-id', created: 123, model: 'test-model', }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response).toMatchInlineSnapshot(` { "body": { "choices": [ { "finish_reason": "stop", "index": 0, "message": { "content": "", "role": "assistant", }, }, ], "created": 123, "id": "test-id", "model": "test-model", "object": "chat.completion", "system_fingerprint": "fp_3bc1b5746c", "usage": { "completion_tokens": 30, "prompt_tokens": 4, "total_tokens": 34, }, }, "headers": { "content-length": "275", "content-type": "application/json", }, "id": "test-id", "modelId": "test-model", "timestamp": 1970-01-01T00:02:03.000Z, } `); }); it('should support partial usage', async () => { prepareJsonResponse({ usage: { prompt_tokens: 20, total_tokens: 20 }, }); const { usage } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(usage).toMatchInlineSnapshot(` { "cachedInputTokens": undefined, "inputTokens": 20, "outputTokens": undefined, "reasoningTokens": undefined, "totalTokens": 20, } `); }); it('should extract logprobs', async () => { prepareJsonResponse({ logprobs: TEST_LOGPROBS, }); const response = await provider.chat('gpt-3.5-turbo').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { logprobs: 1, }, }, }); expect(response.providerMetadata?.openai.logprobs).toStrictEqual( TEST_LOGPROBS.content, ); }); it('should extract finish reason', async () => { prepareJsonResponse({ finish_reason: 'stop', }); const response = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response.finishReason).toStrictEqual('stop'); }); it('should support unknown finish reason', async () => { prepareJsonResponse({ finish_reason: 'eos', }); const response = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response.finishReason).toStrictEqual('unknown'); }); it('should expose the raw response headers', async () => { prepareJsonResponse({ headers: { 'test-header': 'test-value' }, }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response?.headers).toMatchInlineSnapshot(` { "content-length": "321", "content-type": "application/json", "test-header": "test-value", } `); }); it('should pass the model and the messages', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: 'Hello' }], }); }); it('should pass settings', async () => { prepareJsonResponse(); await provider.chat('gpt-3.5-turbo').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { logitBias: { 50256: -100 }, parallelToolCalls: false, user: 'test-user-id', }, }, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "logit_bias": { "50256": -100, }, "messages": [ { "content": "Hello", "role": "user", }, ], "model": "gpt-3.5-turbo", "parallel_tool_calls": false, "user": "test-user-id", } `); }); it('should pass reasoningEffort setting from provider metadata', async () => { prepareJsonResponse({ content: '' }); const model = provider.chat('o1-mini'); await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { reasoningEffort: 'low' }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'o1-mini', messages: [{ role: 'user', content: 'Hello' }], reasoning_effort: 'low', }); }); it('should pass reasoningEffort setting from settings', async () => { prepareJsonResponse({ content: '' }); const model = provider.chat('o1-mini'); await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { reasoningEffort: 'high' }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'o1-mini', messages: [{ role: 'user', content: 'Hello' }], reasoning_effort: 'high', }); }); it('should pass tools and toolChoice', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], toolChoice: { type: 'tool', toolName: 'test-tool', }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "messages": [ { "content": "Hello", "role": "user", }, ], "model": "gpt-3.5-turbo", "tool_choice": { "function": { "name": "test-tool", }, "type": "function", }, "tools": [ { "function": { "name": "test-tool", "parameters": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "value": { "type": "string", }, }, "required": [ "value", ], "type": "object", }, "strict": false, }, "type": "function", }, ], } `); }); it('should pass headers', async () => { prepareJsonResponse({ content: '' }); const provider = createOpenAI({ apiKey: 'test-api-key', organization: 'test-organization', project: 'test-project', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.chat('gpt-3.5-turbo').doGenerate({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', 'openai-organization': 'test-organization', 'openai-project': 'test-project', }); }); it('should parse tool results', async () => { prepareJsonResponse({ tool_calls: [ { id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', type: 'function', function: { name: 'test-tool', arguments: '{"value":"Spark"}', }, }, ], }); const result = await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], toolChoice: { type: 'tool', toolName: 'test-tool', }, prompt: TEST_PROMPT, }); expect(result.content).toMatchInlineSnapshot(` [ { "input": "{"value":"Spark"}", "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, ] `); }); it('should parse annotations/citations', async () => { prepareJsonResponse({ content: 'Based on the search results [doc1], I found information.', annotations: [ { type: 'url_citation', start_index: 24, end_index: 29, url: 'https://example.com/doc1.pdf', title: 'Document 1', }, ], }); const result = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(result.content).toEqual([ { text: 'Based on the search results [doc1], I found information.', type: 'text', }, { id: expect.any(String), sourceType: 'url', title: 'Document 1', type: 'source', url: 'https://example.com/doc1.pdf', }, ]); }); describe('response format', () => { it('should not send a response_format when response format is text', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = provider.chat('gpt-4o-2024-08-06'); await model.doGenerate({ prompt: TEST_PROMPT, responseFormat: { type: 'text' }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o-2024-08-06', messages: [{ role: 'user', content: 'Hello' }], }); }); it('should forward json response format as "json_object" without schema', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = provider.chat('gpt-4o-2024-08-06'); await model.doGenerate({ prompt: TEST_PROMPT, responseFormat: { type: 'json' }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o-2024-08-06', messages: [{ role: 'user', content: 'Hello' }], response_format: { type: 'json_object' }, }); }); it('should forward json response format as "json_object" and omit schema when structuredOutputs are disabled', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = provider.chat('gpt-4o-2024-08-06'); const { warnings } = await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { structuredOutputs: false, }, }, responseFormat: { type: 'json', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "messages": [ { "content": "Hello", "role": "user", }, ], "model": "gpt-4o-2024-08-06", "response_format": { "type": "json_object", }, } `); expect(warnings).toEqual([ { details: 'JSON response format schema is only supported with structuredOutputs', setting: 'responseFormat', type: 'unsupported-setting', }, ]); }); it('should forward json response format as "json_object" and include schema when structuredOutputs are enabled', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = provider.chat('gpt-4o-2024-08-06'); const { warnings } = await model.doGenerate({ prompt: TEST_PROMPT, responseFormat: { type: 'json', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "messages": [ { "content": "Hello", "role": "user", }, ], "model": "gpt-4o-2024-08-06", "response_format": { "json_schema": { "name": "response", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "value": { "type": "string", }, }, "required": [ "value", ], "type": "object", }, "strict": false, }, "type": "json_schema", }, } `); expect(warnings).toEqual([]); }); it('should use json_schema & strict with responseFormat json when structuredOutputs are enabled', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = provider.chat('gpt-4o-2024-08-06'); await model.doGenerate({ responseFormat: { type: 'json', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "messages": [ { "content": "Hello", "role": "user", }, ], "model": "gpt-4o-2024-08-06", "response_format": { "json_schema": { "name": "response", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "value": { "type": "string", }, }, "required": [ "value", ], "type": "object", }, "strict": false, }, "type": "json_schema", }, } `); }); it('should set name & description with responseFormat json when structuredOutputs are enabled', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = provider.chat('gpt-4o-2024-08-06'); await model.doGenerate({ responseFormat: { type: 'json', name: 'test-name', description: 'test description', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "messages": [ { "content": "Hello", "role": "user", }, ], "model": "gpt-4o-2024-08-06", "response_format": { "json_schema": { "description": "test description", "name": "test-name", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "value": { "type": "string", }, }, "required": [ "value", ], "type": "object", }, "strict": false, }, "type": "json_schema", }, } `); }); it('should allow for undefined schema with responseFormat json when structuredOutputs are enabled', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = provider.chat('gpt-4o-2024-08-06'); await model.doGenerate({ responseFormat: { type: 'json', name: 'test-name', description: 'test description', }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o-2024-08-06', messages: [{ role: 'user', content: 'Hello' }], response_format: { type: 'json_object', }, }); }); it('should set strict with tool calls when structuredOutputs are enabled', async () => { prepareJsonResponse({ tool_calls: [ { id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', type: 'function', function: { name: 'test-tool', arguments: '{"value":"Spark"}', }, }, ], }); const model = provider.chat('gpt-4o-2024-08-06'); const result = await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool', description: 'test description', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], toolChoice: { type: 'required' }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "messages": [ { "content": "Hello", "role": "user", }, ], "model": "gpt-4o-2024-08-06", "tool_choice": "required", "tools": [ { "function": { "description": "test description", "name": "test-tool", "parameters": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "value": { "type": "string", }, }, "required": [ "value", ], "type": "object", }, "strict": false, }, "type": "function", }, ], } `); expect(result.content).toMatchInlineSnapshot(` [ { "input": "{"value":"Spark"}", "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, ] `); }); }); it('should set strict for tool usage when structuredOutputs are enabled', async () => { prepareJsonResponse({ tool_calls: [ { id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', type: 'function', function: { name: 'test-tool', arguments: '{"value":"Spark"}', }, }, ], }); const model = provider.chat('gpt-4o-2024-08-06'); const result = await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], toolChoice: { type: 'tool', toolName: 'test-tool', }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "messages": [ { "content": "Hello", "role": "user", }, ], "model": "gpt-4o-2024-08-06", "tool_choice": { "function": { "name": "test-tool", }, "type": "function", }, "tools": [ { "function": { "name": "test-tool", "parameters": { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "value": { "type": "string", }, }, "required": [ "value", ], "type": "object", }, "strict": false, }, "type": "function", }, ], } `); expect(result.content).toMatchInlineSnapshot(` [ { "input": "{"value":"Spark"}", "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, ] `); }); it('should return cached_tokens in prompt_details_tokens', async () => { prepareJsonResponse({ usage: { prompt_tokens: 15, completion_tokens: 20, total_tokens: 35, prompt_tokens_details: { cached_tokens: 1152, }, }, }); const model = provider.chat('gpt-4o-mini'); const result = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(result.usage).toMatchInlineSnapshot(` { "cachedInputTokens": 1152, "inputTokens": 15, "outputTokens": 20, "reasoningTokens": undefined, "totalTokens": 35, } `); }); it('should return accepted_prediction_tokens and rejected_prediction_tokens in completion_details_tokens', async () => { prepareJsonResponse({ usage: { prompt_tokens: 15, completion_tokens: 20, total_tokens: 35, completion_tokens_details: { accepted_prediction_tokens: 123, rejected_prediction_tokens: 456, }, }, }); const model = provider.chat('gpt-4o-mini'); const result = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(result.providerMetadata).toStrictEqual({ openai: { acceptedPredictionTokens: 123, rejectedPredictionTokens: 456, }, }); }); describe('reasoning models', () => { it('should clear out temperature, top_p, frequency_penalty, presence_penalty and return warnings', async () => { prepareJsonResponse(); const model = provider.chat('o1-preview'); const result = await model.doGenerate({ prompt: TEST_PROMPT, temperature: 0.5, topP: 0.7, frequencyPenalty: 0.2, presencePenalty: 0.3, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'o1-preview', messages: [{ role: 'user', content: 'Hello' }], }); expect(result.warnings).toStrictEqual([ { type: 'unsupported-setting', setting: 'temperature', details: 'temperature is not supported for reasoning models', }, { type: 'unsupported-setting', setting: 'topP', details: 'topP is not supported for reasoning models', }, { type: 'unsupported-setting', setting: 'frequencyPenalty', details: 'frequencyPenalty is not supported for reasoning models', }, { type: 'unsupported-setting', setting: 'presencePenalty', details: 'presencePenalty is not supported for reasoning models', }, ]); }); it('should convert maxOutputTokens to max_completion_tokens', async () => { prepareJsonResponse(); const model = provider.chat('o1-preview'); await model.doGenerate({ prompt: TEST_PROMPT, maxOutputTokens: 1000, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'o1-preview', messages: [{ role: 'user', content: 'Hello' }], max_completion_tokens: 1000, }); }); }); it('should remove system messages for o1-preview and add a warning', async () => { prepareJsonResponse(); const model = provider.chat('o1-preview'); const result = await model.doGenerate({ prompt: [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ], }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'o1-preview', messages: [{ role: 'user', content: 'Hello' }], }); expect(result.warnings).toStrictEqual([ { type: 'other', message: 'system messages are removed for this model', }, ]); }); it('should use developer messages for o1', async () => { prepareJsonResponse(); const model = provider.chat('o1'); const result = await model.doGenerate({ prompt: [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ], }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'o1', messages: [ { role: 'developer', content: 'You are a helpful assistant.' }, { role: 'user', content: 'Hello' }, ], }); expect(result.warnings).toStrictEqual([]); }); it('should return the reasoning tokens in the provider metadata', async () => { prepareJsonResponse({ usage: { prompt_tokens: 15, completion_tokens: 20, total_tokens: 35, completion_tokens_details: { reasoning_tokens: 10, }, }, }); const model = provider.chat('o1-preview'); const result = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(result.usage).toMatchInlineSnapshot(` { "cachedInputTokens": undefined, "inputTokens": 15, "outputTokens": 20, "reasoningTokens": 10, "totalTokens": 35, } `); }); it('should send max_completion_tokens extension setting', async () => { prepareJsonResponse({ model: 'o1-preview' }); const model = provider.chat('o1-preview'); await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { maxCompletionTokens: 255, }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'o1-preview', messages: [{ role: 'user', content: 'Hello' }], max_completion_tokens: 255, }); }); it('should send prediction extension setting', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { prediction: { type: 'content', content: 'Hello, World!', }, }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: 'Hello' }], prediction: { type: 'content', content: 'Hello, World!', }, }); }); it('should send store extension setting', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { store: true, }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: 'Hello' }], store: true, }); }); it('should send metadata extension values', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { metadata: { custom: 'value', }, }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: 'Hello' }], metadata: { custom: 'value', }, }); }); it('should remove temperature setting for gpt-4o-search-preview and add warning', async () => { prepareJsonResponse(); const model = provider.chat('gpt-4o-search-preview'); const result = await model.doGenerate({ prompt: TEST_PROMPT, temperature: 0.7, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.model).toBe('gpt-4o-search-preview'); expect(requestBody.temperature).toBeUndefined(); expect(result.warnings).toContainEqual({ type: 'unsupported-setting', setting: 'temperature', details: 'temperature is not supported for the search preview models and has been removed.', }); }); it('should remove temperature setting for gpt-4o-mini-search-preview and add warning', async () => { prepareJsonResponse(); const model = provider.chat('gpt-4o-mini-search-preview'); const result = await model.doGenerate({ prompt: TEST_PROMPT, temperature: 0.7, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.model).toBe('gpt-4o-mini-search-preview'); expect(requestBody.temperature).toBeUndefined(); expect(result.warnings).toContainEqual({ type: 'unsupported-setting', setting: 'temperature', details: 'temperature is not supported for the search preview models and has been removed.', }); }); it('should remove temperature setting for gpt-4o-mini-search-preview-2025-03-11 and add warning', async () => { prepareJsonResponse(); const model = provider.chat('gpt-4o-mini-search-preview-2025-03-11'); const result = await model.doGenerate({ prompt: TEST_PROMPT, temperature: 0.7, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.model).toBe('gpt-4o-mini-search-preview-2025-03-11'); expect(requestBody.temperature).toBeUndefined(); expect(result.warnings).toContainEqual({ type: 'unsupported-setting', setting: 'temperature', details: 'temperature is not supported for the search preview models and has been removed.', }); }); it('should send serviceTier flex processing setting', async () => { prepareJsonResponse({ content: '' }); const model = provider.chat('o3-mini'); await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { serviceTier: 'flex', }, }, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "messages": [ { "content": "Hello", "role": "user", }, ], "model": "o3-mini", "service_tier": "flex", } `); }); it('should show warning when using flex processing with unsupported model', async () => { prepareJsonResponse(); const model = provider.chat('gpt-4o-mini'); const result = await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { serviceTier: 'flex', }, }, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.service_tier).toBeUndefined(); expect(result.warnings).toContainEqual({ type: 'unsupported-setting', setting: 'serviceTier', details: 'flex processing is only available for o3 and o4-mini models', }); }); it('should allow flex processing with o4-mini model without warnings', async () => { prepareJsonResponse(); const model = provider.chat('o4-mini'); const result = await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { serviceTier: 'flex', }, }, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.service_tier).toBe('flex'); expect(result.warnings).toEqual([]); }); it('should send serviceTier priority processing setting', async () => { prepareJsonResponse(); const model = provider.chat('gpt-4o-mini'); await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { serviceTier: 'priority', }, }, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "messages": [ { "content": "Hello", "role": "user", }, ], "model": "gpt-4o-mini", "service_tier": "priority", } `); }); it('should show warning when using priority processing with unsupported model', async () => { prepareJsonResponse(); const model = provider.chat('gpt-3.5-turbo'); const result = await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { serviceTier: 'priority', }, }, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.service_tier).toBeUndefined(); expect(result.warnings).toContainEqual({ type: 'unsupported-setting', setting: 'serviceTier', details: 'priority processing is only available for supported models (GPT-4, o3, o4-mini) and requires Enterprise access', }); }); it('should allow priority processing with gpt-4o model without warnings', async () => { prepareJsonResponse(); const model = provider.chat('gpt-4o'); const result = await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { serviceTier: 'priority', }, }, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.service_tier).toBe('priority'); expect(result.warnings).toEqual([]); }); it('should allow priority processing with o3 model without warnings', async () => { prepareJsonResponse(); const model = provider.chat('o3-mini'); const result = await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { serviceTier: 'priority', }, }, }); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody.service_tier).toBe('priority'); expect(result.warnings).toEqual([]); }); }); describe('doStream', () => { function prepareStreamResponse({ content = [], usage = { prompt_tokens: 17, total_tokens: 244, completion_tokens: 227, }, logprobs = null, finish_reason = 'stop', model = 'gpt-3.5-turbo-0613', headers, }: { content?: string[]; usage?: { prompt_tokens: number; total_tokens: number; completion_tokens: number; prompt_tokens_details?: { cached_tokens?: number; }; completion_tokens_details?: { reasoning_tokens?: number; accepted_prediction_tokens?: number; rejected_prediction_tokens?: number; }; }; logprobs?: { content: | { token: string; logprob: number; top_logprobs: { token: string; logprob: number }[]; }[] | null; } | null; finish_reason?: string; model?: string; headers?: Record<string, string>; }) { server.urls['https://api.openai.com/v1/chat/completions'].response = { type: 'stream-chunks', headers, chunks: [ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"${model}",` + `"system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}\n\n`, ...content.map(text => { return ( `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"${model}",` + `"system_fingerprint":null,"choices":[{"index":1,"delta":{"content":"${text}"},"finish_reason":null}]}\n\n` ); }), `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"${model}",` + `"system_fingerprint":null,"choices":[{"index":0,"delta":{},"finish_reason":"${finish_reason}","logprobs":${JSON.stringify( logprobs, )}}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"${model}",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[],"usage":${JSON.stringify( usage, )}}\n\n`, 'data: [DONE]\n\n', ], }; } it('should stream text deltas', async () => { prepareStreamResponse({ content: ['Hello', ', ', 'World!'], finish_reason: 'stop', usage: { prompt_tokens: 17, total_tokens: 244, completion_tokens: 227, }, logprobs: TEST_LOGPROBS, }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP", "modelId": "gpt-3.5-turbo-0613", "timestamp": 2023-12-15T16:17:00.000Z, "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "", "id": "0", "type": "text-delta", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "delta": ", ", "id": "0", "type": "text-delta", }, { "delta": "World!", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "openai": { "logprobs": [ { "logprob": -0.0009994634, "token": "Hello", "top_logprobs": [ { "logprob": -0.0009994634, "token": "Hello", }, ], }, { "logprob": -0.13410144, "token": "!", "top_logprobs": [ { "logprob": -0.13410144, "token": "!", }, ], }, { "logprob": -0.0009250381, "token": " How", "top_logprobs": [ { "logprob": -0.0009250381, "token": " How", }, ], }, { "logprob": -0.047709424, "token": " can", "top_logprobs": [ { "logprob": -0.047709424, "token": " can", }, ], }, { "logprob": -0.000009014684, "token": " I", "top_logprobs": [ { "logprob": -0.000009014684, "token": " I", }, ], }, { "logprob": -0.009125131, "token": " assist", "top_logprobs": [ { "logprob": -0.009125131, "token": " assist", }, ], }, { "logprob": -0.0000066306106, "token": " you", "top_logprobs": [ { "logprob": -0.0000066306106, "token": " you", }, ], }, { "logprob": -0.00011093382, "token": " today", "top_logprobs": [ { "logprob": -0.00011093382, "token": " today", }, ], }, { "logprob": -0.00004596782, "token": "?", "top_logprobs": [ { "logprob": -0.00004596782, "token": "?", }, ], }, ], }, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 17, "outputTokens": 227, "reasoningTokens": undefined, "totalTokens": 244, }, }, ] `); }); it('should stream annotations/citations', async () => { server.urls['https://api.openai.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":null,"choices":[{"index":1,"delta":{"content":"Based on search results"},"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":null,"choices":[{"index":1,"delta":{"annotations":[{"type":"url_citation","start_index":24,"end_index":29,"url":"https://example.com/doc1.pdf","title":"Document 1"}]},"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":null,"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[],"usage":{"prompt_tokens":17,"completion_tokens":227,"total_tokens":244}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const streamResult = await convertReadableStreamToArray(stream); expect(streamResult).toEqual( expect.arrayContaining([ { type: 'stream-start', warnings: [] }, expect.objectContaining({ type: 'response-metadata', id: 'chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP', }), { type: 'text-start', id: '0' }, { type: 'text-delta', id: '0', delta: '' }, { type: 'text-delta', id: '0', delta: 'Based on search results' }, { type: 'source', sourceType: 'url', id: expect.any(String), url: 'https://example.com/doc1.pdf', title: 'Document 1', }, { type: 'text-end', id: '0' }, expect.objectContaining({ type: 'finish', finishReason: 'stop', }), ]), ); }); it('should stream tool deltas', async () => { server.urls['https://api.openai.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,` + `"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":""}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\""}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"value"}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\":\\""}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Spark"}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"le"}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" Day"}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"}"}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[],"usage":{"prompt_tokens":53,"completion_tokens":17,"total_tokens":70}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP", "modelId": "gpt-3.5-turbo-0125", "timestamp": 2024-03-25T09:06:38.000Z, "type": "response-metadata", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-input-start", }, { "delta": "{"", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "value", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "":"", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "Spark", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "le", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": " Day", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": ""}", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-end", }, { "input": "{"value":"Sparkle Day"}", "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "tool-calls", "providerMetadata": { "openai": {}, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 53, "outputTokens": 17, "reasoningTokens": undefined, "totalTokens": 70, }, }, ] `); }); it('should stream tool call deltas when tool call arguments are passed in the first chunk', async () => { server.urls['https://api.openai.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,` + `"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":"{\\""}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"va"}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"lue"}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\":\\""}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Spark"}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"le"}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" Day"}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"}"}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[],"usage":{"prompt_tokens":53,"completion_tokens":17,"total_tokens":70}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP", "modelId": "gpt-3.5-turbo-0125", "timestamp": 2024-03-25T09:06:38.000Z, "type": "response-metadata", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-input-start", }, { "delta": "{"", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "va", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "lue", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "":"", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "Spark", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "le", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": " Day", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": ""}", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-end", }, { "input": "{"value":"Sparkle Day"}", "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "tool-calls", "providerMetadata": { "openai": {}, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 53, "outputTokens": 17, "reasoningTokens": undefined, "totalTokens": 70, }, }, ] `); }); it('should not duplicate tool calls when there is an additional empty chunk after the tool call has been completed', async () => { server.urls['https://api.openai.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":226,"completion_tokens":0}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"id":"chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa",` + `"type":"function","index":0,"function":{"name":"searchGoogle"}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":233,"completion_tokens":7}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":"{\\"query\\": \\""}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":241,"completion_tokens":15}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":"latest"}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":242,"completion_tokens":16}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":" news"}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":243,"completion_tokens":17}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":" on"}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":244,"completion_tokens":18}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":" ai\\"}"}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":245,"completion_tokens":19}}\n\n`, // empty arguments chunk after the tool call has already been finished: `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":""}}]},"logprobs":null,"finish_reason":"tool_calls","stop_reason":128008}],` + `"usage":{"prompt_tokens":226,"total_tokens":246,"completion_tokens":20}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[],` + `"usage":{"prompt_tokens":226,"total_tokens":246,"completion_tokens":20}}\n\n`, `data: [DONE]\n\n`, ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'searchGoogle', inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chat-2267f7e2910a4254bac0650ba74cfc1c", "modelId": "meta/llama-3.1-8b-instruct:fp8", "timestamp": 2024-12-02T17:57:21.000Z, "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "", "id": "0", "type": "text-delta", }, { "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "toolName": "searchGoogle", "type": "tool-input-start", }, { "delta": "{"query": "", "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-delta", }, { "delta": "latest", "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-delta", }, { "delta": " news", "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-delta", }, { "delta": " on", "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-delta", }, { "delta": " ai"}", "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-delta", }, { "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-end", }, { "input": "{"query": "latest news on ai"}", "toolCallId": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "toolName": "searchGoogle", "type": "tool-call", }, { "id": "0", "type": "text-end", }, { "finishReason": "tool-calls", "providerMetadata": { "openai": {}, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 226, "outputTokens": 20, "reasoningTokens": undefined, "totalTokens": 246, }, }, ] `); }); it('should stream tool call that is sent in one chunk', async () => { server.urls['https://api.openai.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,` + `"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":"{\\"value\\":\\"Sparkle Day\\"}"}}]},` + `"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}]}\n\n`, `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[],"usage":{"prompt_tokens":53,"completion_tokens":17,"total_tokens":70}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP", "modelId": "gpt-3.5-turbo-0125", "timestamp": 2024-03-25T09:06:38.000Z, "type": "response-metadata", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-input-start", }, { "delta": "{"value":"Sparkle Day"}", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-end", }, { "input": "{"value":"Sparkle Day"}", "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "tool-calls", "providerMetadata": { "openai": {}, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 53, "outputTokens": 17, "reasoningTokens": undefined, "totalTokens": 70, }, }, ] `); }); it('should handle error stream parts', async () => { server.urls['https://api.openai.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"error":{"message": "The server had an error processing your request. Sorry about that! You can retry your request, or contact us through our ` + `help center at help.openai.com if you keep seeing this error.","type":"server_error","param":null,"code":null}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "error": { "code": null, "message": "The server had an error processing your request. Sorry about that! You can retry your request, or contact us through our help center at help.openai.com if you keep seeing this error.", "param": null, "type": "server_error", }, "type": "error", }, { "finishReason": "error", "providerMetadata": { "openai": {}, }, "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it.skipIf(isNodeVersion(20))( 'should handle unparsable stream parts', async () => { server.urls['https://api.openai.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [`data: {unparsable}\n\n`, 'data: [DONE]\n\n'], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "error": [AI_JSONParseError: JSON parsing failed: Text: {unparsable}. Error message: Expected property name or '}' in JSON at position 1 (line 1 column 2)], "type": "error", }, { "finishReason": "error", "providerMetadata": { "openai": {}, }, "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }, ); it('should send request body', async () => { prepareStreamResponse({ content: [] }); const { request } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(request).toMatchInlineSnapshot(` { "body": { "frequency_penalty": undefined, "logit_bias": undefined, "logprobs": undefined, "max_completion_tokens": undefined, "max_tokens": undefined, "messages": [ { "content": "Hello", "role": "user", }, ], "metadata": undefined, "model": "gpt-3.5-turbo", "parallel_tool_calls": undefined, "prediction": undefined, "presence_penalty": undefined, "reasoning_effort": undefined, "response_format": undefined, "seed": undefined, "service_tier": undefined, "stop": undefined, "store": undefined, "stream": true, "stream_options": { "include_usage": true, }, "temperature": undefined, "tool_choice": undefined, "tools": undefined, "top_logprobs": undefined, "top_p": undefined, "user": undefined, }, } `); }); it('should expose the raw response headers', async () => { prepareStreamResponse({ headers: { 'test-header': 'test-value' }, }); const { response } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', // custom header 'test-header': 'test-value', }); }); it('should pass the messages and the model', async () => { prepareStreamResponse({ content: [] }); await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ stream: true, stream_options: { include_usage: true }, model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: 'Hello' }], }); }); it('should pass headers', async () => { prepareStreamResponse({ content: [] }); const provider = createOpenAI({ apiKey: 'test-api-key', organization: 'test-organization', project: 'test-project', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.chat('gpt-3.5-turbo').doStream({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, includeRawChunks: false, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', 'openai-organization': 'test-organization', 'openai-project': 'test-project', }); }); it('should return cached tokens in providerMetadata', async () => { prepareStreamResponse({ content: [], usage: { prompt_tokens: 15, completion_tokens: 20, total_tokens: 35, prompt_tokens_details: { cached_tokens: 1152, }, }, }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ stream: true, stream_options: { include_usage: true }, model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: 'Hello' }], }); expect((await convertReadableStreamToArray(stream)).at(-1)) .toMatchInlineSnapshot(` { "finishReason": "stop", "providerMetadata": { "openai": {}, }, "type": "finish", "usage": { "cachedInputTokens": 1152, "inputTokens": 15, "outputTokens": 20, "reasoningTokens": undefined, "totalTokens": 35, }, } `); }); it('should return accepted_prediction_tokens and rejected_prediction_tokens in providerMetadata', async () => { prepareStreamResponse({ content: [], usage: { prompt_tokens: 15, completion_tokens: 20, total_tokens: 35, completion_tokens_details: { accepted_prediction_tokens: 123, rejected_prediction_tokens: 456, }, }, }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ stream: true, stream_options: { include_usage: true }, model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: 'Hello' }], }); expect((await convertReadableStreamToArray(stream)).at(-1)) .toMatchInlineSnapshot(` { "finishReason": "stop", "providerMetadata": { "openai": { "acceptedPredictionTokens": 123, "rejectedPredictionTokens": 456, }, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 15, "outputTokens": 20, "reasoningTokens": undefined, "totalTokens": 35, }, } `); }); it('should send store extension setting', async () => { prepareStreamResponse({ content: [] }); await model.doStream({ prompt: TEST_PROMPT, providerOptions: { openai: { store: true, }, }, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-3.5-turbo', stream: true, stream_options: { include_usage: true }, messages: [{ role: 'user', content: 'Hello' }], store: true, }); }); it('should send metadata extension values', async () => { prepareStreamResponse({ content: [] }); await model.doStream({ prompt: TEST_PROMPT, providerOptions: { openai: { metadata: { custom: 'value', }, }, }, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-3.5-turbo', stream: true, stream_options: { include_usage: true }, messages: [{ role: 'user', content: 'Hello' }], metadata: { custom: 'value', }, }); }); it('should send serviceTier flex processing setting in streaming', async () => { prepareStreamResponse({ content: [] }); const model = provider.chat('o3-mini'); await model.doStream({ prompt: TEST_PROMPT, providerOptions: { openai: { serviceTier: 'flex', }, }, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "messages": [ { "content": "Hello", "role": "user", }, ], "model": "o3-mini", "service_tier": "flex", "stream": true, "stream_options": { "include_usage": true, }, } `); }); it('should send serviceTier priority processing setting in streaming', async () => { prepareStreamResponse({ content: [] }); const model = provider.chat('gpt-4o-mini'); await model.doStream({ prompt: TEST_PROMPT, providerOptions: { openai: { serviceTier: 'priority', }, }, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "messages": [ { "content": "Hello", "role": "user", }, ], "model": "gpt-4o-mini", "service_tier": "priority", "stream": true, "stream_options": { "include_usage": true, }, } `); }); describe('reasoning models', () => { it('should stream text delta', async () => { prepareStreamResponse({ content: ['Hello, World!'], model: 'o1-preview', }); const model = provider.chat('o1-preview'); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP", "modelId": "o1-preview", "timestamp": 2023-12-15T16:17:00.000Z, "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "", "id": "0", "type": "text-delta", }, { "delta": "Hello, World!", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "openai": {}, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 17, "outputTokens": 227, "reasoningTokens": undefined, "totalTokens": 244, }, }, ] `); }); it('should send reasoning tokens', async () => { prepareStreamResponse({ content: ['Hello, World!'], model: 'o1-preview', usage: { prompt_tokens: 15, completion_tokens: 20, total_tokens: 35, completion_tokens_details: { reasoning_tokens: 10, }, }, }); const model = provider.chat('o1-preview'); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP", "modelId": "o1-preview", "timestamp": 2023-12-15T16:17:00.000Z, "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "", "id": "0", "type": "text-delta", }, { "delta": "Hello, World!", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "openai": {}, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 15, "outputTokens": 20, "reasoningTokens": 10, "totalTokens": 35, }, }, ] `); }); }); describe('raw chunks', () => { it('should include raw chunks when includeRawChunks is enabled', async () => { prepareStreamResponse({ content: ['Hello', ' World!'], }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: true, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks.filter(chunk => chunk.type === 'raw')) .toMatchInlineSnapshot(` [ { "rawValue": { "choices": [ { "delta": { "content": "", "role": "assistant", }, "finish_reason": null, "index": 0, }, ], "created": 1702657020, "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP", "model": "gpt-3.5-turbo-0613", "object": "chat.completion.chunk", "system_fingerprint": null, }, "type": "raw", }, { "rawValue": { "choices": [ { "delta": { "content": "Hello", }, "finish_reason": null, "index": 1, }, ], "created": 1702657020, "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP", "model": "gpt-3.5-turbo-0613", "object": "chat.completion.chunk", "system_fingerprint": null, }, "type": "raw", }, { "rawValue": { "choices": [ { "delta": { "content": " World!", }, "finish_reason": null, "index": 1, }, ], "created": 1702657020, "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP", "model": "gpt-3.5-turbo-0613", "object": "chat.completion.chunk", "system_fingerprint": null, }, "type": "raw", }, { "rawValue": { "choices": [ { "delta": {}, "finish_reason": "stop", "index": 0, "logprobs": null, }, ], "created": 1702657020, "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP", "model": "gpt-3.5-turbo-0613", "object": "chat.completion.chunk", "system_fingerprint": null, }, "type": "raw", }, { "rawValue": { "choices": [], "created": 1702657020, "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP", "model": "gpt-3.5-turbo-0613", "object": "chat.completion.chunk", "system_fingerprint": "fp_3bc1b5746c", "usage": { "completion_tokens": 227, "prompt_tokens": 17, "total_tokens": 244, }, }, "type": "raw", }, ] `); }); it('should not include raw chunks when includeRawChunks is false', async () => { prepareStreamResponse({ content: ['Hello', ' World!'], }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks.filter(chunk => chunk.type === 'raw')).toHaveLength(0); }); }); }); --- File: /ai/packages/openai/src/openai-chat-language-model.ts --- import { InvalidResponseDataError, LanguageModelV2, LanguageModelV2CallOptions, LanguageModelV2CallWarning, LanguageModelV2Content, LanguageModelV2FinishReason, LanguageModelV2StreamPart, LanguageModelV2Usage, SharedV2ProviderMetadata, } from '@ai-sdk/provider'; import { FetchFunction, ParseResult, combineHeaders, createEventSourceResponseHandler, createJsonResponseHandler, generateId, isParsableJson, parseProviderOptions, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { convertToOpenAIChatMessages } from './convert-to-openai-chat-messages'; import { getResponseMetadata } from './get-response-metadata'; import { mapOpenAIFinishReason } from './map-openai-finish-reason'; import { OpenAIChatModelId, openaiProviderOptions, } from './openai-chat-options'; import { openaiErrorDataSchema, openaiFailedResponseHandler, } from './openai-error'; import { prepareTools } from './openai-prepare-tools'; type OpenAIChatConfig = { provider: string; headers: () => Record<string, string | undefined>; url: (options: { modelId: string; path: string }) => string; fetch?: FetchFunction; }; export class OpenAIChatLanguageModel implements LanguageModelV2 { readonly specificationVersion = 'v2'; readonly modelId: OpenAIChatModelId; readonly supportedUrls = { 'image/*': [/^https?:\/\/.*$/], }; private readonly config: OpenAIChatConfig; constructor(modelId: OpenAIChatModelId, config: OpenAIChatConfig) { this.modelId = modelId; this.config = config; } get provider(): string { return this.config.provider; } private async getArgs({ prompt, maxOutputTokens, temperature, topP, topK, frequencyPenalty, presencePenalty, stopSequences, responseFormat, seed, tools, toolChoice, providerOptions, }: LanguageModelV2CallOptions) { const warnings: LanguageModelV2CallWarning[] = []; // Parse provider options const openaiOptions = (await parseProviderOptions({ provider: 'openai', providerOptions, schema: openaiProviderOptions, })) ?? {}; const structuredOutputs = openaiOptions.structuredOutputs ?? true; if (topK != null) { warnings.push({ type: 'unsupported-setting', setting: 'topK', }); } if ( responseFormat?.type === 'json' && responseFormat.schema != null && !structuredOutputs ) { warnings.push({ type: 'unsupported-setting', setting: 'responseFormat', details: 'JSON response format schema is only supported with structuredOutputs', }); } const { messages, warnings: messageWarnings } = convertToOpenAIChatMessages( { prompt, systemMessageMode: getSystemMessageMode(this.modelId), }, ); warnings.push(...messageWarnings); const strictJsonSchema = openaiOptions.strictJsonSchema ?? false; const baseArgs = { // model id: model: this.modelId, // model specific settings: logit_bias: openaiOptions.logitBias, logprobs: openaiOptions.logprobs === true || typeof openaiOptions.logprobs === 'number' ? true : undefined, top_logprobs: typeof openaiOptions.logprobs === 'number' ? openaiOptions.logprobs : typeof openaiOptions.logprobs === 'boolean' ? openaiOptions.logprobs ? 0 : undefined : undefined, user: openaiOptions.user, parallel_tool_calls: openaiOptions.parallelToolCalls, // standardized settings: max_tokens: maxOutputTokens, temperature, top_p: topP, frequency_penalty: frequencyPenalty, presence_penalty: presencePenalty, response_format: responseFormat?.type === 'json' ? structuredOutputs && responseFormat.schema != null ? { type: 'json_schema', json_schema: { schema: responseFormat.schema, strict: strictJsonSchema, name: responseFormat.name ?? 'response', description: responseFormat.description, }, } : { type: 'json_object' } : undefined, stop: stopSequences, seed, // openai specific settings: // TODO remove in next major version; we auto-map maxOutputTokens now max_completion_tokens: openaiOptions.maxCompletionTokens, store: openaiOptions.store, metadata: openaiOptions.metadata, prediction: openaiOptions.prediction, reasoning_effort: openaiOptions.reasoningEffort, service_tier: openaiOptions.serviceTier, // messages: messages, }; if (isReasoningModel(this.modelId)) { // remove unsupported settings for reasoning models // see https://platform.openai.com/docs/guides/reasoning#limitations if (baseArgs.temperature != null) { baseArgs.temperature = undefined; warnings.push({ type: 'unsupported-setting', setting: 'temperature', details: 'temperature is not supported for reasoning models', }); } if (baseArgs.top_p != null) { baseArgs.top_p = undefined; warnings.push({ type: 'unsupported-setting', setting: 'topP', details: 'topP is not supported for reasoning models', }); } if (baseArgs.frequency_penalty != null) { baseArgs.frequency_penalty = undefined; warnings.push({ type: 'unsupported-setting', setting: 'frequencyPenalty', details: 'frequencyPenalty is not supported for reasoning models', }); } if (baseArgs.presence_penalty != null) { baseArgs.presence_penalty = undefined; warnings.push({ type: 'unsupported-setting', setting: 'presencePenalty', details: 'presencePenalty is not supported for reasoning models', }); } if (baseArgs.logit_bias != null) { baseArgs.logit_bias = undefined; warnings.push({ type: 'other', message: 'logitBias is not supported for reasoning models', }); } if (baseArgs.logprobs != null) { baseArgs.logprobs = undefined; warnings.push({ type: 'other', message: 'logprobs is not supported for reasoning models', }); } if (baseArgs.top_logprobs != null) { baseArgs.top_logprobs = undefined; warnings.push({ type: 'other', message: 'topLogprobs is not supported for reasoning models', }); } // reasoning models use max_completion_tokens instead of max_tokens: if (baseArgs.max_tokens != null) { if (baseArgs.max_completion_tokens == null) { baseArgs.max_completion_tokens = baseArgs.max_tokens; } baseArgs.max_tokens = undefined; } } else if ( this.modelId.startsWith('gpt-4o-search-preview') || this.modelId.startsWith('gpt-4o-mini-search-preview') ) { if (baseArgs.temperature != null) { baseArgs.temperature = undefined; warnings.push({ type: 'unsupported-setting', setting: 'temperature', details: 'temperature is not supported for the search preview models and has been removed.', }); } } // Validate flex processing support if ( openaiOptions.serviceTier === 'flex' && !supportsFlexProcessing(this.modelId) ) { warnings.push({ type: 'unsupported-setting', setting: 'serviceTier', details: 'flex processing is only available for o3 and o4-mini models', }); baseArgs.service_tier = undefined; } // Validate priority processing support if ( openaiOptions.serviceTier === 'priority' && !supportsPriorityProcessing(this.modelId) ) { warnings.push({ type: 'unsupported-setting', setting: 'serviceTier', details: 'priority processing is only available for supported models (GPT-4, o3, o4-mini) and requires Enterprise access', }); baseArgs.service_tier = undefined; } const { tools: openaiTools, toolChoice: openaiToolChoice, toolWarnings, } = prepareTools({ tools, toolChoice, structuredOutputs, strictJsonSchema, }); return { args: { ...baseArgs, tools: openaiTools, tool_choice: openaiToolChoice, }, warnings: [...warnings, ...toolWarnings], }; } async doGenerate( options: Parameters<LanguageModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>> { const { args: body, warnings } = await this.getArgs(options); const { responseHeaders, value: response, rawValue: rawResponse, } = await postJsonToApi({ url: this.config.url({ path: '/chat/completions', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body, failedResponseHandler: openaiFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( openaiChatResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const choice = response.choices[0]; const content: Array<LanguageModelV2Content> = []; // text content: const text = choice.message.content; if (text != null && text.length > 0) { content.push({ type: 'text', text }); } // tool calls: for (const toolCall of choice.message.tool_calls ?? []) { content.push({ type: 'tool-call' as const, toolCallId: toolCall.id ?? generateId(), toolName: toolCall.function.name, input: toolCall.function.arguments!, }); } // annotations/citations: for (const annotation of choice.message.annotations ?? []) { content.push({ type: 'source', sourceType: 'url', id: generateId(), url: annotation.url, title: annotation.title, }); } // provider metadata: const completionTokenDetails = response.usage?.completion_tokens_details; const promptTokenDetails = response.usage?.prompt_tokens_details; const providerMetadata: SharedV2ProviderMetadata = { openai: {} }; if (completionTokenDetails?.accepted_prediction_tokens != null) { providerMetadata.openai.acceptedPredictionTokens = completionTokenDetails?.accepted_prediction_tokens; } if (completionTokenDetails?.rejected_prediction_tokens != null) { providerMetadata.openai.rejectedPredictionTokens = completionTokenDetails?.rejected_prediction_tokens; } if (choice.logprobs?.content != null) { providerMetadata.openai.logprobs = choice.logprobs.content; } return { content, finishReason: mapOpenAIFinishReason(choice.finish_reason), usage: { inputTokens: response.usage?.prompt_tokens ?? undefined, outputTokens: response.usage?.completion_tokens ?? undefined, totalTokens: response.usage?.total_tokens ?? undefined, reasoningTokens: completionTokenDetails?.reasoning_tokens ?? undefined, cachedInputTokens: promptTokenDetails?.cached_tokens ?? undefined, }, request: { body }, response: { ...getResponseMetadata(response), headers: responseHeaders, body: rawResponse, }, warnings, providerMetadata, }; } async doStream( options: Parameters<LanguageModelV2['doStream']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>> { const { args, warnings } = await this.getArgs(options); const body = { ...args, stream: true, stream_options: { include_usage: true, }, }; const { responseHeaders, value: response } = await postJsonToApi({ url: this.config.url({ path: '/chat/completions', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body, failedResponseHandler: openaiFailedResponseHandler, successfulResponseHandler: createEventSourceResponseHandler( openaiChatChunkSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const toolCalls: Array<{ id: string; type: 'function'; function: { name: string; arguments: string; }; hasFinished: boolean; }> = []; let finishReason: LanguageModelV2FinishReason = 'unknown'; const usage: LanguageModelV2Usage = { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, }; let isFirstChunk = true; let isActiveText = false; const providerMetadata: SharedV2ProviderMetadata = { openai: {} }; return { stream: response.pipeThrough( new TransformStream< ParseResult<z.infer<typeof openaiChatChunkSchema>>, LanguageModelV2StreamPart >({ start(controller) { controller.enqueue({ type: 'stream-start', warnings }); }, transform(chunk, controller) { if (options.includeRawChunks) { controller.enqueue({ type: 'raw', rawValue: chunk.rawValue }); } // handle failed chunk parsing / validation: if (!chunk.success) { finishReason = 'error'; controller.enqueue({ type: 'error', error: chunk.error }); return; } const value = chunk.value; // handle error chunks: if ('error' in value) { finishReason = 'error'; controller.enqueue({ type: 'error', error: value.error }); return; } if (isFirstChunk) { isFirstChunk = false; controller.enqueue({ type: 'response-metadata', ...getResponseMetadata(value), }); } if (value.usage != null) { usage.inputTokens = value.usage.prompt_tokens ?? undefined; usage.outputTokens = value.usage.completion_tokens ?? undefined; usage.totalTokens = value.usage.total_tokens ?? undefined; usage.reasoningTokens = value.usage.completion_tokens_details?.reasoning_tokens ?? undefined; usage.cachedInputTokens = value.usage.prompt_tokens_details?.cached_tokens ?? undefined; if ( value.usage.completion_tokens_details ?.accepted_prediction_tokens != null ) { providerMetadata.openai.acceptedPredictionTokens = value.usage.completion_tokens_details?.accepted_prediction_tokens; } if ( value.usage.completion_tokens_details ?.rejected_prediction_tokens != null ) { providerMetadata.openai.rejectedPredictionTokens = value.usage.completion_tokens_details?.rejected_prediction_tokens; } } const choice = value.choices[0]; if (choice?.finish_reason != null) { finishReason = mapOpenAIFinishReason(choice.finish_reason); } if (choice?.logprobs?.content != null) { providerMetadata.openai.logprobs = choice.logprobs.content; } if (choice?.delta == null) { return; } const delta = choice.delta; if (delta.content != null) { if (!isActiveText) { controller.enqueue({ type: 'text-start', id: '0' }); isActiveText = true; } controller.enqueue({ type: 'text-delta', id: '0', delta: delta.content, }); } if (delta.tool_calls != null) { for (const toolCallDelta of delta.tool_calls) { const index = toolCallDelta.index; // Tool call start. OpenAI returns all information except the arguments in the first chunk. if (toolCalls[index] == null) { if (toolCallDelta.type !== 'function') { throw new InvalidResponseDataError({ data: toolCallDelta, message: `Expected 'function' type.`, }); } if (toolCallDelta.id == null) { throw new InvalidResponseDataError({ data: toolCallDelta, message: `Expected 'id' to be a string.`, }); } if (toolCallDelta.function?.name == null) { throw new InvalidResponseDataError({ data: toolCallDelta, message: `Expected 'function.name' to be a string.`, }); } controller.enqueue({ type: 'tool-input-start', id: toolCallDelta.id, toolName: toolCallDelta.function.name, }); toolCalls[index] = { id: toolCallDelta.id, type: 'function', function: { name: toolCallDelta.function.name, arguments: toolCallDelta.function.arguments ?? '', }, hasFinished: false, }; const toolCall = toolCalls[index]; if ( toolCall.function?.name != null && toolCall.function?.arguments != null ) { // send delta if the argument text has already started: if (toolCall.function.arguments.length > 0) { controller.enqueue({ type: 'tool-input-delta', id: toolCall.id, delta: toolCall.function.arguments, }); } // check if tool call is complete // (some providers send the full tool call in one chunk): if (isParsableJson(toolCall.function.arguments)) { controller.enqueue({ type: 'tool-input-end', id: toolCall.id, }); controller.enqueue({ type: 'tool-call', toolCallId: toolCall.id ?? generateId(), toolName: toolCall.function.name, input: toolCall.function.arguments, }); toolCall.hasFinished = true; } } continue; } // existing tool call, merge if not finished const toolCall = toolCalls[index]; if (toolCall.hasFinished) { continue; } if (toolCallDelta.function?.arguments != null) { toolCall.function!.arguments += toolCallDelta.function?.arguments ?? ''; } // send delta controller.enqueue({ type: 'tool-input-delta', id: toolCall.id, delta: toolCallDelta.function.arguments ?? '', }); // check if tool call is complete if ( toolCall.function?.name != null && toolCall.function?.arguments != null && isParsableJson(toolCall.function.arguments) ) { controller.enqueue({ type: 'tool-input-end', id: toolCall.id, }); controller.enqueue({ type: 'tool-call', toolCallId: toolCall.id ?? generateId(), toolName: toolCall.function.name, input: toolCall.function.arguments, }); toolCall.hasFinished = true; } } } // annotations/citations: if (delta.annotations != null) { for (const annotation of delta.annotations) { controller.enqueue({ type: 'source', sourceType: 'url', id: generateId(), url: annotation.url, title: annotation.title, }); } } }, flush(controller) { if (isActiveText) { controller.enqueue({ type: 'text-end', id: '0' }); } controller.enqueue({ type: 'finish', finishReason, usage, ...(providerMetadata != null ? { providerMetadata } : {}), }); }, }), ), request: { body }, response: { headers: responseHeaders }, }; } } const openaiTokenUsageSchema = z .object({ prompt_tokens: z.number().nullish(), completion_tokens: z.number().nullish(), total_tokens: z.number().nullish(), prompt_tokens_details: z .object({ cached_tokens: z.number().nullish(), }) .nullish(), completion_tokens_details: z .object({ reasoning_tokens: z.number().nullish(), accepted_prediction_tokens: z.number().nullish(), rejected_prediction_tokens: z.number().nullish(), }) .nullish(), }) .nullish(); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const openaiChatResponseSchema = z.object({ id: z.string().nullish(), created: z.number().nullish(), model: z.string().nullish(), choices: z.array( z.object({ message: z.object({ role: z.literal('assistant').nullish(), content: z.string().nullish(), tool_calls: z .array( z.object({ id: z.string().nullish(), type: z.literal('function'), function: z.object({ name: z.string(), arguments: z.string(), }), }), ) .nullish(), annotations: z .array( z.object({ type: z.literal('url_citation'), start_index: z.number(), end_index: z.number(), url: z.string(), title: z.string(), }), ) .nullish(), }), index: z.number(), logprobs: z .object({ content: z .array( z.object({ token: z.string(), logprob: z.number(), top_logprobs: z.array( z.object({ token: z.string(), logprob: z.number(), }), ), }), ) .nullish(), }) .nullish(), finish_reason: z.string().nullish(), }), ), usage: openaiTokenUsageSchema, }); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const openaiChatChunkSchema = z.union([ z.object({ id: z.string().nullish(), created: z.number().nullish(), model: z.string().nullish(), choices: z.array( z.object({ delta: z .object({ role: z.enum(['assistant']).nullish(), content: z.string().nullish(), tool_calls: z .array( z.object({ index: z.number(), id: z.string().nullish(), type: z.literal('function').nullish(), function: z.object({ name: z.string().nullish(), arguments: z.string().nullish(), }), }), ) .nullish(), annotations: z .array( z.object({ type: z.literal('url_citation'), start_index: z.number(), end_index: z.number(), url: z.string(), title: z.string(), }), ) .nullish(), }) .nullish(), logprobs: z .object({ content: z .array( z.object({ token: z.string(), logprob: z.number(), top_logprobs: z.array( z.object({ token: z.string(), logprob: z.number(), }), ), }), ) .nullish(), }) .nullish(), finish_reason: z.string().nullish(), index: z.number(), }), ), usage: openaiTokenUsageSchema, }), openaiErrorDataSchema, ]); function isReasoningModel(modelId: string) { return modelId.startsWith('o'); } function supportsFlexProcessing(modelId: string) { return modelId.startsWith('o3') || modelId.startsWith('o4-mini'); } function supportsPriorityProcessing(modelId: string) { return ( modelId.startsWith('gpt-4') || modelId.startsWith('o3') || modelId.startsWith('o4-mini') ); } function getSystemMessageMode(modelId: string) { if (!isReasoningModel(modelId)) { return 'system'; } return ( reasoningModels[modelId as keyof typeof reasoningModels] ?.systemMessageMode ?? 'developer' ); } const reasoningModels = { 'o1-mini': { systemMessageMode: 'remove', }, 'o1-mini-2024-09-12': { systemMessageMode: 'remove', }, 'o1-preview': { systemMessageMode: 'remove', }, 'o1-preview-2024-09-12': { systemMessageMode: 'remove', }, o3: { systemMessageMode: 'developer', }, 'o3-2025-04-16': { systemMessageMode: 'developer', }, 'o3-mini': { systemMessageMode: 'developer', }, 'o3-mini-2025-01-31': { systemMessageMode: 'developer', }, 'o4-mini': { systemMessageMode: 'developer', }, 'o4-mini-2025-04-16': { systemMessageMode: 'developer', }, } as const; --- File: /ai/packages/openai/src/openai-chat-options.ts --- import { z } from 'zod/v4'; // https://platform.openai.com/docs/models export type OpenAIChatModelId = | 'o1' | 'o1-2024-12-17' | 'o1-mini' | 'o1-mini-2024-09-12' | 'o1-preview' | 'o1-preview-2024-09-12' | 'o3-mini' | 'o3-mini-2025-01-31' | 'o3' | 'o3-2025-04-16' | 'o4-mini' | 'o4-mini-2025-04-16' | 'gpt-4.1' | 'gpt-4.1-2025-04-14' | 'gpt-4.1-mini' | 'gpt-4.1-mini-2025-04-14' | 'gpt-4.1-nano' | 'gpt-4.1-nano-2025-04-14' | 'gpt-4o' | 'gpt-4o-2024-05-13' | 'gpt-4o-2024-08-06' | 'gpt-4o-2024-11-20' | 'gpt-4o-audio-preview' | 'gpt-4o-audio-preview-2024-10-01' | 'gpt-4o-audio-preview-2024-12-17' | 'gpt-4o-search-preview' | 'gpt-4o-search-preview-2025-03-11' | 'gpt-4o-mini-search-preview' | 'gpt-4o-mini-search-preview-2025-03-11' | 'gpt-4o-mini' | 'gpt-4o-mini-2024-07-18' | 'gpt-4-turbo' | 'gpt-4-turbo-2024-04-09' | 'gpt-4-turbo-preview' | 'gpt-4-0125-preview' | 'gpt-4-1106-preview' | 'gpt-4' | 'gpt-4-0613' | 'gpt-4.5-preview' | 'gpt-4.5-preview-2025-02-27' | 'gpt-3.5-turbo-0125' | 'gpt-3.5-turbo' | 'gpt-3.5-turbo-1106' | 'chatgpt-4o-latest' | (string & {}); export const openaiProviderOptions = z.object({ /** * Modify the likelihood of specified tokens appearing in the completion. * * Accepts a JSON object that maps tokens (specified by their token ID in * the GPT tokenizer) to an associated bias value from -100 to 100. */ logitBias: z.record(z.coerce.number<string>(), z.number()).optional(), /** * Return the log probabilities of the tokens. * * Setting to true will return the log probabilities of the tokens that * were generated. * * Setting to a number will return the log probabilities of the top n * tokens that were generated. */ logprobs: z.union([z.boolean(), z.number()]).optional(), /** * Whether to enable parallel function calling during tool use. Default to true. */ parallelToolCalls: z.boolean().optional(), /** * A unique identifier representing your end-user, which can help OpenAI to * monitor and detect abuse. */ user: z.string().optional(), /** * Reasoning effort for reasoning models. Defaults to `medium`. */ reasoningEffort: z.enum(['low', 'medium', 'high']).optional(), /** * Maximum number of completion tokens to generate. Useful for reasoning models. */ maxCompletionTokens: z.number().optional(), /** * Whether to enable persistence in responses API. */ store: z.boolean().optional(), /** * Metadata to associate with the request. */ metadata: z.record(z.string().max(64), z.string().max(512)).optional(), /** * Parameters for prediction mode. */ prediction: z.record(z.string(), z.any()).optional(), /** * Whether to use structured outputs. * * @default true */ structuredOutputs: z.boolean().optional(), /** * Service tier for the request. * - 'auto': Default service tier * - 'flex': 50% cheaper processing at the cost of increased latency. Only available for o3 and o4-mini models. * - 'priority': Higher-speed processing with predictably low latency at premium cost. Available for Enterprise customers. * * @default 'auto' */ serviceTier: z.enum(['auto', 'flex', 'priority']).optional(), /** * Whether to use strict JSON schema validation. * * @default false */ strictJsonSchema: z.boolean().optional(), }); export type OpenAIProviderOptions = z.infer<typeof openaiProviderOptions>; --- File: /ai/packages/openai/src/openai-chat-prompt.ts --- export type OpenAIChatPrompt = Array<ChatCompletionMessage>; export type ChatCompletionMessage = | ChatCompletionSystemMessage | ChatCompletionDeveloperMessage | ChatCompletionUserMessage | ChatCompletionAssistantMessage | ChatCompletionToolMessage; export interface ChatCompletionSystemMessage { role: 'system'; content: string; } export interface ChatCompletionDeveloperMessage { role: 'developer'; content: string; } export interface ChatCompletionUserMessage { role: 'user'; content: string | Array<ChatCompletionContentPart>; } export type ChatCompletionContentPart = | ChatCompletionContentPartText | ChatCompletionContentPartImage | ChatCompletionContentPartInputAudio | ChatCompletionContentPartFile; export interface ChatCompletionContentPartText { type: 'text'; text: string; } export interface ChatCompletionContentPartImage { type: 'image_url'; image_url: { url: string }; } export interface ChatCompletionContentPartInputAudio { type: 'input_audio'; input_audio: { data: string; format: 'wav' | 'mp3' }; } export interface ChatCompletionContentPartFile { type: 'file'; file: { filename: string; file_data: string } | { file_id: string }; } export interface ChatCompletionAssistantMessage { role: 'assistant'; content?: string | null; tool_calls?: Array<ChatCompletionMessageToolCall>; } export interface ChatCompletionMessageToolCall { type: 'function'; id: string; function: { arguments: string; name: string; }; } export interface ChatCompletionToolMessage { role: 'tool'; content: string; tool_call_id: string; } --- File: /ai/packages/openai/src/openai-completion-language-model.test.ts --- import { LanguageModelV2Prompt } from '@ai-sdk/provider'; import { convertReadableStreamToArray, createTestServer, isNodeVersion, } from '@ai-sdk/provider-utils/test'; import { createOpenAI } from './openai-provider'; const TEST_PROMPT: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; const TEST_LOGPROBS = { tokens: [' ever', ' after', '.\n\n', 'The', ' end', '.'], token_logprobs: [ -0.0664508, -0.014520033, -1.3820221, -0.7890417, -0.5323165, -0.10247037, ], top_logprobs: [ { ' ever': -0.0664508, }, { ' after': -0.014520033, }, { '.\n\n': -1.3820221, }, { The: -0.7890417, }, { ' end': -0.5323165, }, { '.': -0.10247037, }, ] as Record<string, number>[], }; const provider = createOpenAI({ apiKey: 'test-api-key', }); const model = provider.completion('gpt-3.5-turbo-instruct'); const server = createTestServer({ 'https://api.openai.com/v1/completions': {}, }); describe('doGenerate', () => { function prepareJsonResponse({ content = '', usage = { prompt_tokens: 4, total_tokens: 34, completion_tokens: 30, }, logprobs = null, finish_reason = 'stop', id = 'cmpl-96cAM1v77r4jXa4qb2NSmRREV5oWB', created = 1711363706, model = 'gpt-3.5-turbo-instruct', headers, }: { content?: string; usage?: { prompt_tokens: number; total_tokens: number; completion_tokens: number; }; logprobs?: { tokens: string[]; token_logprobs: number[]; top_logprobs: Record<string, number>[]; } | null; finish_reason?: string; id?: string; created?: number; model?: string; headers?: Record<string, string>; }) { server.urls['https://api.openai.com/v1/completions'].response = { type: 'json-value', headers, body: { id, object: 'text_completion', created, model, choices: [ { text: content, index: 0, ...(logprobs ? { logprobs } : {}), finish_reason, }, ], usage, }, }; } it('should extract text response', async () => { prepareJsonResponse({ content: 'Hello, World!' }); const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "text": "Hello, World!", "type": "text", }, ] `); }); it('should extract usage', async () => { prepareJsonResponse({ usage: { prompt_tokens: 20, total_tokens: 25, completion_tokens: 5 }, }); const { usage } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(usage).toMatchInlineSnapshot(` { "inputTokens": 20, "outputTokens": 5, "totalTokens": 25, } `); }); it('should send request body', async () => { prepareJsonResponse({}); const { request } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(request).toMatchInlineSnapshot(` { "body": { "echo": undefined, "frequency_penalty": undefined, "logit_bias": undefined, "logprobs": undefined, "max_tokens": undefined, "model": "gpt-3.5-turbo-instruct", "presence_penalty": undefined, "prompt": "user: Hello assistant: ", "seed": undefined, "stop": [ " user:", ], "suffix": undefined, "temperature": undefined, "top_p": undefined, "user": undefined, }, } `); }); it('should send additional response information', async () => { prepareJsonResponse({ id: 'test-id', created: 123, model: 'test-model', }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect({ id: response?.id, timestamp: response?.timestamp, modelId: response?.modelId, }).toStrictEqual({ id: 'test-id', timestamp: new Date(123 * 1000), modelId: 'test-model', }); }); it('should extract logprobs', async () => { prepareJsonResponse({ logprobs: TEST_LOGPROBS }); const provider = createOpenAI({ apiKey: 'test-api-key' }); const response = await provider.completion('gpt-3.5-turbo').doGenerate({ prompt: TEST_PROMPT, providerOptions: { openai: { logprobs: 1, }, }, }); expect(response.providerMetadata?.openai.logprobs).toStrictEqual( TEST_LOGPROBS, ); }); it('should extract finish reason', async () => { prepareJsonResponse({ finish_reason: 'stop', }); const { finishReason } = await provider .completion('gpt-3.5-turbo-instruct') .doGenerate({ prompt: TEST_PROMPT, }); expect(finishReason).toStrictEqual('stop'); }); it('should support unknown finish reason', async () => { prepareJsonResponse({ finish_reason: 'eos', }); const { finishReason } = await provider .completion('gpt-3.5-turbo-instruct') .doGenerate({ prompt: TEST_PROMPT, }); expect(finishReason).toStrictEqual('unknown'); }); it('should expose the raw response headers', async () => { prepareJsonResponse({ headers: { 'test-header': 'test-value', }, }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response?.headers).toMatchInlineSnapshot(` { "content-length": "250", "content-type": "application/json", "test-header": "test-value", } `); }); it('should pass the model and the prompt', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "model": "gpt-3.5-turbo-instruct", "prompt": "user: Hello assistant: ", "stop": [ " user:", ], } `); }); it('should pass headers', async () => { prepareJsonResponse({ content: '' }); const provider = createOpenAI({ apiKey: 'test-api-key', organization: 'test-organization', project: 'test-project', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.completion('gpt-3.5-turbo-instruct').doGenerate({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', 'openai-organization': 'test-organization', 'openai-project': 'test-project', }); }); }); describe('doStream', () => { function prepareStreamResponse({ content = [], finish_reason = 'stop', usage = { prompt_tokens: 10, total_tokens: 372, completion_tokens: 362, }, logprobs = null, headers, }: { content?: string[]; usage?: { prompt_tokens: number; total_tokens: number; completion_tokens: number; }; logprobs?: { tokens: string[]; token_logprobs: number[]; top_logprobs: Record<string, number>[]; } | null; finish_reason?: string; headers?: Record<string, string>; }) { server.urls['https://api.openai.com/v1/completions'].response = { type: 'stream-chunks', headers, chunks: [ ...content.map(text => { return ( `data: {"id":"cmpl-96c64EdfhOw8pjFFgVpLuT8k2MtdT","object":"text_completion","created":1711363440,` + `"choices":[{"text":"${text}","index":0,"logprobs":${JSON.stringify( logprobs, )},"finish_reason":null}],"model":"gpt-3.5-turbo-instruct"}\n\n` ); }), `data: {"id":"cmpl-96c3yLQE1TtZCd6n6OILVmzev8M8H","object":"text_completion","created":1711363310,` + `"choices":[{"text":"","index":0,"logprobs":${JSON.stringify(logprobs)},"finish_reason":"${finish_reason}"}],"model":"gpt-3.5-turbo-instruct"}\n\n`, `data: {"id":"cmpl-96c3yLQE1TtZCd6n6OILVmzev8M8H","object":"text_completion","created":1711363310,` + `"model":"gpt-3.5-turbo-instruct","usage":${JSON.stringify( usage, )},"choices":[]}\n\n`, 'data: [DONE]\n\n', ], }; } it('should stream text deltas', async () => { prepareStreamResponse({ content: ['Hello', ', ', 'World!'], finish_reason: 'stop', usage: { prompt_tokens: 10, total_tokens: 372, completion_tokens: 362, }, logprobs: TEST_LOGPROBS, }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "cmpl-96c64EdfhOw8pjFFgVpLuT8k2MtdT", "modelId": "gpt-3.5-turbo-instruct", "timestamp": 2024-03-25T10:44:00.000Z, "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "delta": ", ", "id": "0", "type": "text-delta", }, { "delta": "World!", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "openai": { "logprobs": { "token_logprobs": [ -0.0664508, -0.014520033, -1.3820221, -0.7890417, -0.5323165, -0.10247037, ], "tokens": [ " ever", " after", ". ", "The", " end", ".", ], "top_logprobs": [ { " ever": -0.0664508, }, { " after": -0.014520033, }, { ". ": -1.3820221, }, { "The": -0.7890417, }, { " end": -0.5323165, }, { ".": -0.10247037, }, ], }, }, }, "type": "finish", "usage": { "inputTokens": 10, "outputTokens": 362, "totalTokens": 372, }, }, ] `); }); it('should handle error stream parts', async () => { server.urls['https://api.openai.com/v1/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"error":{"message": "The server had an error processing your request. Sorry about that! You can retry your request, or contact us through our ` + `help center at help.openai.com if you keep seeing this error.","type":"server_error","param":null,"code":null}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "error": { "code": null, "message": "The server had an error processing your request. Sorry about that! You can retry your request, or contact us through our help center at help.openai.com if you keep seeing this error.", "param": null, "type": "server_error", }, "type": "error", }, { "finishReason": "error", "providerMetadata": { "openai": {}, }, "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it.skipIf(isNodeVersion(20))( 'should handle unparsable stream parts', async () => { server.urls['https://api.openai.com/v1/completions'].response = { type: 'stream-chunks', chunks: [`data: {unparsable}\n\n`, 'data: [DONE]\n\n'], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "error": [AI_JSONParseError: JSON parsing failed: Text: {unparsable}. Error message: Expected property name or '}' in JSON at position 1 (line 1 column 2)], "type": "error", }, { "finishReason": "error", "providerMetadata": { "openai": {}, }, "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }, ); it('should send request body', async () => { prepareStreamResponse({ content: [] }); const { request } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(request).toMatchInlineSnapshot(` { "body": { "echo": undefined, "frequency_penalty": undefined, "logit_bias": undefined, "logprobs": undefined, "max_tokens": undefined, "model": "gpt-3.5-turbo-instruct", "presence_penalty": undefined, "prompt": "user: Hello assistant: ", "seed": undefined, "stop": [ " user:", ], "stream": true, "stream_options": { "include_usage": true, }, "suffix": undefined, "temperature": undefined, "top_p": undefined, "user": undefined, }, } `); }); it('should expose the raw response headers', async () => { prepareStreamResponse({ headers: { 'test-header': 'test-value' }, }); const { response } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', // custom header 'test-header': 'test-value', }); }); it('should pass the model and the prompt', async () => { prepareStreamResponse({ content: [] }); await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "model": "gpt-3.5-turbo-instruct", "prompt": "user: Hello assistant: ", "stop": [ " user:", ], "stream": true, "stream_options": { "include_usage": true, }, } `); }); it('should pass headers', async () => { prepareStreamResponse({ content: [] }); const provider = createOpenAI({ apiKey: 'test-api-key', organization: 'test-organization', project: 'test-project', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.completion('gpt-3.5-turbo-instruct').doStream({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, includeRawChunks: false, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', 'openai-organization': 'test-organization', 'openai-project': 'test-project', }); }); }); --- File: /ai/packages/openai/src/openai-completion-language-model.ts --- import { LanguageModelV2, LanguageModelV2CallWarning, LanguageModelV2FinishReason, LanguageModelV2StreamPart, LanguageModelV2Usage, SharedV2ProviderMetadata, } from '@ai-sdk/provider'; import { FetchFunction, ParseResult, combineHeaders, createEventSourceResponseHandler, createJsonResponseHandler, parseProviderOptions, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { convertToOpenAICompletionPrompt } from './convert-to-openai-completion-prompt'; import { getResponseMetadata } from './get-response-metadata'; import { mapOpenAIFinishReason } from './map-openai-finish-reason'; import { OpenAICompletionModelId, openaiCompletionProviderOptions, } from './openai-completion-options'; import { openaiErrorDataSchema, openaiFailedResponseHandler, } from './openai-error'; type OpenAICompletionConfig = { provider: string; headers: () => Record<string, string | undefined>; url: (options: { modelId: string; path: string }) => string; fetch?: FetchFunction; }; export class OpenAICompletionLanguageModel implements LanguageModelV2 { readonly specificationVersion = 'v2'; readonly modelId: OpenAICompletionModelId; private readonly config: OpenAICompletionConfig; private get providerOptionsName(): string { return this.config.provider.split('.')[0].trim(); } constructor( modelId: OpenAICompletionModelId, config: OpenAICompletionConfig, ) { this.modelId = modelId; this.config = config; } get provider(): string { return this.config.provider; } readonly supportedUrls: Record<string, RegExp[]> = { // No URLs are supported for completion models. }; private async getArgs({ prompt, maxOutputTokens, temperature, topP, topK, frequencyPenalty, presencePenalty, stopSequences: userStopSequences, responseFormat, tools, toolChoice, seed, providerOptions, }: Parameters<LanguageModelV2['doGenerate']>[0]) { const warnings: LanguageModelV2CallWarning[] = []; // Parse provider options const openaiOptions = { ...(await parseProviderOptions({ provider: 'openai', providerOptions, schema: openaiCompletionProviderOptions, })), ...(await parseProviderOptions({ provider: this.providerOptionsName, providerOptions, schema: openaiCompletionProviderOptions, })), }; if (topK != null) { warnings.push({ type: 'unsupported-setting', setting: 'topK' }); } if (tools?.length) { warnings.push({ type: 'unsupported-setting', setting: 'tools' }); } if (toolChoice != null) { warnings.push({ type: 'unsupported-setting', setting: 'toolChoice' }); } if (responseFormat != null && responseFormat.type !== 'text') { warnings.push({ type: 'unsupported-setting', setting: 'responseFormat', details: 'JSON response format is not supported.', }); } const { prompt: completionPrompt, stopSequences } = convertToOpenAICompletionPrompt({ prompt }); const stop = [...(stopSequences ?? []), ...(userStopSequences ?? [])]; return { args: { // model id: model: this.modelId, // model specific settings: echo: openaiOptions.echo, logit_bias: openaiOptions.logitBias, logprobs: openaiOptions?.logprobs === true ? 0 : openaiOptions?.logprobs === false ? undefined : openaiOptions?.logprobs, suffix: openaiOptions.suffix, user: openaiOptions.user, // standardized settings: max_tokens: maxOutputTokens, temperature, top_p: topP, frequency_penalty: frequencyPenalty, presence_penalty: presencePenalty, seed, // prompt: prompt: completionPrompt, // stop sequences: stop: stop.length > 0 ? stop : undefined, }, warnings, }; } async doGenerate( options: Parameters<LanguageModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>> { const { args, warnings } = await this.getArgs(options); const { responseHeaders, value: response, rawValue: rawResponse, } = await postJsonToApi({ url: this.config.url({ path: '/completions', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body: args, failedResponseHandler: openaiFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( openaiCompletionResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const choice = response.choices[0]; const providerMetadata: SharedV2ProviderMetadata = { openai: {} }; if (choice.logprobs != null) { providerMetadata.openai.logprobs = choice.logprobs; } return { content: [{ type: 'text', text: choice.text }], usage: { inputTokens: response.usage?.prompt_tokens, outputTokens: response.usage?.completion_tokens, totalTokens: response.usage?.total_tokens, }, finishReason: mapOpenAIFinishReason(choice.finish_reason), request: { body: args }, response: { ...getResponseMetadata(response), headers: responseHeaders, body: rawResponse, }, providerMetadata, warnings, }; } async doStream( options: Parameters<LanguageModelV2['doStream']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>> { const { args, warnings } = await this.getArgs(options); const body = { ...args, stream: true, stream_options: { include_usage: true, }, }; const { responseHeaders, value: response } = await postJsonToApi({ url: this.config.url({ path: '/completions', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body, failedResponseHandler: openaiFailedResponseHandler, successfulResponseHandler: createEventSourceResponseHandler( openaiCompletionChunkSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); let finishReason: LanguageModelV2FinishReason = 'unknown'; const providerMetadata: SharedV2ProviderMetadata = { openai: {} }; const usage: LanguageModelV2Usage = { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, }; let isFirstChunk = true; return { stream: response.pipeThrough( new TransformStream< ParseResult<z.infer<typeof openaiCompletionChunkSchema>>, LanguageModelV2StreamPart >({ start(controller) { controller.enqueue({ type: 'stream-start', warnings }); }, transform(chunk, controller) { if (options.includeRawChunks) { controller.enqueue({ type: 'raw', rawValue: chunk.rawValue }); } // handle failed chunk parsing / validation: if (!chunk.success) { finishReason = 'error'; controller.enqueue({ type: 'error', error: chunk.error }); return; } const value = chunk.value; // handle error chunks: if ('error' in value) { finishReason = 'error'; controller.enqueue({ type: 'error', error: value.error }); return; } if (isFirstChunk) { isFirstChunk = false; controller.enqueue({ type: 'response-metadata', ...getResponseMetadata(value), }); controller.enqueue({ type: 'text-start', id: '0' }); } if (value.usage != null) { usage.inputTokens = value.usage.prompt_tokens; usage.outputTokens = value.usage.completion_tokens; usage.totalTokens = value.usage.total_tokens; } const choice = value.choices[0]; if (choice?.finish_reason != null) { finishReason = mapOpenAIFinishReason(choice.finish_reason); } if (choice?.logprobs != null) { providerMetadata.openai.logprobs = choice.logprobs; } if (choice?.text != null && choice.text.length > 0) { controller.enqueue({ type: 'text-delta', id: '0', delta: choice.text, }); } }, flush(controller) { if (!isFirstChunk) { controller.enqueue({ type: 'text-end', id: '0' }); } controller.enqueue({ type: 'finish', finishReason, providerMetadata, usage, }); }, }), ), request: { body }, response: { headers: responseHeaders }, }; } } const usageSchema = z.object({ prompt_tokens: z.number(), completion_tokens: z.number(), total_tokens: z.number(), }); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const openaiCompletionResponseSchema = z.object({ id: z.string().nullish(), created: z.number().nullish(), model: z.string().nullish(), choices: z.array( z.object({ text: z.string(), finish_reason: z.string(), logprobs: z .object({ tokens: z.array(z.string()), token_logprobs: z.array(z.number()), top_logprobs: z.array(z.record(z.string(), z.number())).nullish(), }) .nullish(), }), ), usage: usageSchema.nullish(), }); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const openaiCompletionChunkSchema = z.union([ z.object({ id: z.string().nullish(), created: z.number().nullish(), model: z.string().nullish(), choices: z.array( z.object({ text: z.string(), finish_reason: z.string().nullish(), index: z.number(), logprobs: z .object({ tokens: z.array(z.string()), token_logprobs: z.array(z.number()), top_logprobs: z.array(z.record(z.string(), z.number())).nullish(), }) .nullish(), }), ), usage: usageSchema.nullish(), }), openaiErrorDataSchema, ]); --- File: /ai/packages/openai/src/openai-completion-options.ts --- import { z } from 'zod/v4'; // https://platform.openai.com/docs/models export type OpenAICompletionModelId = 'gpt-3.5-turbo-instruct' | (string & {}); export const openaiCompletionProviderOptions = z.object({ /** Echo back the prompt in addition to the completion. */ echo: z.boolean().optional(), /** Modify the likelihood of specified tokens appearing in the completion. Accepts a JSON object that maps tokens (specified by their token ID in the GPT tokenizer) to an associated bias value from -100 to 100. You can use this tokenizer tool to convert text to token IDs. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token. As an example, you can pass {"50256": -100} to prevent the <|endoftext|> token from being generated. */ logitBias: z.record(z.string(), z.number()).optional(), /** The suffix that comes after a completion of inserted text. */ suffix: z.string().optional(), /** A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. Learn more. */ user: z.string().optional(), /** Return the log probabilities of the tokens. Including logprobs will increase the response size and can slow down response times. However, it can be useful to better understand how the model is behaving. Setting to true will return the log probabilities of the tokens that were generated. Setting to a number will return the log probabilities of the top n tokens that were generated. */ logprobs: z.union([z.boolean(), z.number()]).optional(), }); export type OpenAICompletionProviderOptions = z.infer< typeof openaiCompletionProviderOptions >; --- File: /ai/packages/openai/src/openai-config.ts --- import { FetchFunction } from '@ai-sdk/provider-utils'; export type OpenAIConfig = { provider: string; url: (options: { modelId: string; path: string }) => string; headers: () => Record<string, string | undefined>; fetch?: FetchFunction; generateId?: () => string; }; --- File: /ai/packages/openai/src/openai-embedding-model.test.ts --- import { EmbeddingModelV2Embedding } from '@ai-sdk/provider'; import { createTestServer } from '@ai-sdk/provider-utils/test'; import { createOpenAI } from './openai-provider'; const dummyEmbeddings = [ [0.1, 0.2, 0.3, 0.4, 0.5], [0.6, 0.7, 0.8, 0.9, 1.0], ]; const testValues = ['sunny day at the beach', 'rainy day in the city']; const provider = createOpenAI({ apiKey: 'test-api-key' }); const model = provider.embedding('text-embedding-3-large'); const server = createTestServer({ 'https://api.openai.com/v1/embeddings': {}, }); describe('doEmbed', () => { function prepareJsonResponse({ embeddings = dummyEmbeddings, usage = { prompt_tokens: 8, total_tokens: 8 }, headers, }: { embeddings?: EmbeddingModelV2Embedding[]; usage?: { prompt_tokens: number; total_tokens: number }; headers?: Record<string, string>; } = {}) { server.urls['https://api.openai.com/v1/embeddings'].response = { type: 'json-value', headers, body: { object: 'list', data: embeddings.map((embedding, i) => ({ object: 'embedding', index: i, embedding, })), model: 'text-embedding-3-large', usage, }, }; } it('should extract embedding', async () => { prepareJsonResponse(); const { embeddings } = await model.doEmbed({ values: testValues }); expect(embeddings).toStrictEqual(dummyEmbeddings); }); it('should expose the raw response', async () => { prepareJsonResponse({ headers: { 'test-header': 'test-value', }, }); const { response } = await model.doEmbed({ values: testValues }); expect(response?.headers).toStrictEqual({ // default headers: 'content-length': '236', 'content-type': 'application/json', // custom header 'test-header': 'test-value', }); expect(response).toMatchSnapshot(); }); it('should extract usage', async () => { prepareJsonResponse({ usage: { prompt_tokens: 20, total_tokens: 20 }, }); const { usage } = await model.doEmbed({ values: testValues }); expect(usage).toStrictEqual({ tokens: 20 }); }); it('should pass the model and the values', async () => { prepareJsonResponse(); await model.doEmbed({ values: testValues }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'text-embedding-3-large', input: testValues, encoding_format: 'float', }); }); it('should pass the dimensions setting', async () => { prepareJsonResponse(); await provider.embedding('text-embedding-3-large').doEmbed({ values: testValues, providerOptions: { openai: { dimensions: 64 } }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'text-embedding-3-large', input: testValues, encoding_format: 'float', dimensions: 64, }); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createOpenAI({ apiKey: 'test-api-key', organization: 'test-organization', project: 'test-project', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.embedding('text-embedding-3-large').doEmbed({ values: testValues, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', 'openai-organization': 'test-organization', 'openai-project': 'test-project', }); }); }); --- File: /ai/packages/openai/src/openai-embedding-model.ts --- import { EmbeddingModelV2, TooManyEmbeddingValuesForCallError, } from '@ai-sdk/provider'; import { combineHeaders, createJsonResponseHandler, parseProviderOptions, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { OpenAIConfig } from './openai-config'; import { OpenAIEmbeddingModelId, openaiEmbeddingProviderOptions, } from './openai-embedding-options'; import { openaiFailedResponseHandler } from './openai-error'; export class OpenAIEmbeddingModel implements EmbeddingModelV2<string> { readonly specificationVersion = 'v2'; readonly modelId: OpenAIEmbeddingModelId; readonly maxEmbeddingsPerCall = 2048; readonly supportsParallelCalls = true; private readonly config: OpenAIConfig; get provider(): string { return this.config.provider; } constructor(modelId: OpenAIEmbeddingModelId, config: OpenAIConfig) { this.modelId = modelId; this.config = config; } async doEmbed({ values, headers, abortSignal, providerOptions, }: Parameters<EmbeddingModelV2<string>['doEmbed']>[0]): Promise< Awaited<ReturnType<EmbeddingModelV2<string>['doEmbed']>> > { if (values.length > this.maxEmbeddingsPerCall) { throw new TooManyEmbeddingValuesForCallError({ provider: this.provider, modelId: this.modelId, maxEmbeddingsPerCall: this.maxEmbeddingsPerCall, values, }); } // Parse provider options const openaiOptions = (await parseProviderOptions({ provider: 'openai', providerOptions, schema: openaiEmbeddingProviderOptions, })) ?? {}; const { responseHeaders, value: response, rawValue, } = await postJsonToApi({ url: this.config.url({ path: '/embeddings', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), headers), body: { model: this.modelId, input: values, encoding_format: 'float', dimensions: openaiOptions.dimensions, user: openaiOptions.user, }, failedResponseHandler: openaiFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( openaiTextEmbeddingResponseSchema, ), abortSignal, fetch: this.config.fetch, }); return { embeddings: response.data.map(item => item.embedding), usage: response.usage ? { tokens: response.usage.prompt_tokens } : undefined, response: { headers: responseHeaders, body: rawValue }, }; } } // minimal version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const openaiTextEmbeddingResponseSchema = z.object({ data: z.array(z.object({ embedding: z.array(z.number()) })), usage: z.object({ prompt_tokens: z.number() }).nullish(), }); --- File: /ai/packages/openai/src/openai-embedding-options.ts --- import { z } from 'zod/v4'; export type OpenAIEmbeddingModelId = | 'text-embedding-3-small' | 'text-embedding-3-large' | 'text-embedding-ada-002' | (string & {}); export const openaiEmbeddingProviderOptions = z.object({ /** The number of dimensions the resulting output embeddings should have. Only supported in text-embedding-3 and later models. */ dimensions: z.number().optional(), /** A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. Learn more. */ user: z.string().optional(), }); export type OpenAIEmbeddingProviderOptions = z.infer< typeof openaiEmbeddingProviderOptions >; --- File: /ai/packages/openai/src/openai-error.test.ts --- import { safeParseJSON } from '@ai-sdk/provider-utils'; import { openaiErrorDataSchema } from './openai-error'; describe('openaiErrorDataSchema', () => { it('should parse OpenRouter resource exhausted error', async () => { const error = ` {"error":{"message":"{\\n \\"error\\": {\\n \\"code\\": 429,\\n \\"message\\": \\"Resource has been exhausted (e.g. check quota).\\",\\n \\"status\\": \\"RESOURCE_EXHAUSTED\\"\\n }\\n}\\n","code":429}} `; const result = await safeParseJSON({ text: error, schema: openaiErrorDataSchema, }); expect(result).toStrictEqual({ success: true, value: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, rawValue: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, }); }); }); --- File: /ai/packages/openai/src/openai-error.ts --- import { z } from 'zod/v4'; import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; export const openaiErrorDataSchema = z.object({ error: z.object({ message: z.string(), // The additional information below is handled loosely to support // OpenAI-compatible providers that have slightly different error // responses: type: z.string().nullish(), param: z.any().nullish(), code: z.union([z.string(), z.number()]).nullish(), }), }); export type OpenAIErrorData = z.infer<typeof openaiErrorDataSchema>; export const openaiFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: openaiErrorDataSchema, errorToMessage: data => data.error.message, }); --- File: /ai/packages/openai/src/openai-image-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { OpenAIImageModel } from './openai-image-model'; import { createOpenAI } from './openai-provider'; const prompt = 'A cute baby sea otter'; const provider = createOpenAI({ apiKey: 'test-api-key' }); const model = provider.image('dall-e-3'); const server = createTestServer({ 'https://api.openai.com/v1/images/generations': {}, }); describe('doGenerate', () => { function prepareJsonResponse({ headers, }: { headers?: Record<string, string>; } = {}) { server.urls['https://api.openai.com/v1/images/generations'].response = { type: 'json-value', headers, body: { created: 1733837122, data: [ { revised_prompt: 'A charming visual illustration of a baby sea otter swimming joyously.', b64_json: 'base64-image-1', }, { b64_json: 'base64-image-2', }, ], }, }; } it('should pass the model and the settings', async () => { prepareJsonResponse(); await model.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: undefined, providerOptions: { openai: { style: 'vivid' } }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'dall-e-3', prompt, n: 1, size: '1024x1024', style: 'vivid', response_format: 'b64_json', }); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createOpenAI({ apiKey: 'test-api-key', organization: 'test-organization', project: 'test-project', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.image('dall-e-3').doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: undefined, providerOptions: { openai: { style: 'vivid' } }, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', 'openai-organization': 'test-organization', 'openai-project': 'test-project', }); }); it('should extract the generated images', async () => { prepareJsonResponse(); const result = await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.images).toStrictEqual(['base64-image-1', 'base64-image-2']); }); it('should return warnings for unsupported settings', async () => { prepareJsonResponse(); const result = await model.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: '1:1', seed: 123, providerOptions: {}, }); expect(result.warnings).toStrictEqual([ { type: 'unsupported-setting', setting: 'aspectRatio', details: 'This model does not support aspect ratio. Use `size` instead.', }, { type: 'unsupported-setting', setting: 'seed', }, ]); }); it('should respect maxImagesPerCall setting', async () => { const defaultModel = provider.image('dall-e-2'); expect(defaultModel.maxImagesPerCall).toBe(10); // dall-e-2's default from settings const unknownModel = provider.image('unknown-model' as any); expect(unknownModel.maxImagesPerCall).toBe(1); // fallback for unknown models }); it('should include response data with timestamp, modelId and headers', async () => { prepareJsonResponse({ headers: { 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); const testDate = new Date('2024-03-15T12:00:00Z'); const customModel = new OpenAIImageModel('dall-e-3', { provider: 'test-provider', url: () => 'https://api.openai.com/v1/images/generations', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.response).toStrictEqual({ timestamp: testDate, modelId: 'dall-e-3', headers: { 'content-length': '180', 'content-type': 'application/json', 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); }); it('should use real date when no custom date provider is specified', async () => { prepareJsonResponse(); const beforeDate = new Date(); const result = await model.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: undefined, providerOptions: {}, }); const afterDate = new Date(); expect(result.response.timestamp.getTime()).toBeGreaterThanOrEqual( beforeDate.getTime(), ); expect(result.response.timestamp.getTime()).toBeLessThanOrEqual( afterDate.getTime(), ); expect(result.response.modelId).toBe('dall-e-3'); }); it('should not include response_format for gpt-image-1', async () => { prepareJsonResponse(); const gptImageModel = provider.image('gpt-image-1'); await gptImageModel.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: undefined, providerOptions: {}, }); const requestBody = await server.calls[server.calls.length - 1].requestBodyJson; expect(requestBody).toStrictEqual({ model: 'gpt-image-1', prompt, n: 1, size: '1024x1024', }); expect(requestBody).not.toHaveProperty('response_format'); }); it('should include response_format for dall-e-3', async () => { prepareJsonResponse(); await model.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: undefined, providerOptions: {}, }); const requestBody = await server.calls[server.calls.length - 1].requestBodyJson; expect(requestBody).toHaveProperty('response_format', 'b64_json'); }); it('should return image meta data', async () => { prepareJsonResponse(); const result = await model.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: undefined, seed: undefined, providerOptions: { openai: { style: 'vivid' } }, }); expect(result.providerMetadata).toStrictEqual({ openai: { images: [ { revisedPrompt: 'A charming visual illustration of a baby sea otter swimming joyously.', }, null, ], }, }); }); }); --- File: /ai/packages/openai/src/openai-image-model.ts --- import { ImageModelV2, ImageModelV2CallWarning } from '@ai-sdk/provider'; import { combineHeaders, createJsonResponseHandler, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { OpenAIConfig } from './openai-config'; import { openaiFailedResponseHandler } from './openai-error'; import { OpenAIImageModelId, modelMaxImagesPerCall, hasDefaultResponseFormat, } from './openai-image-settings'; interface OpenAIImageModelConfig extends OpenAIConfig { _internal?: { currentDate?: () => Date; }; } export class OpenAIImageModel implements ImageModelV2 { readonly specificationVersion = 'v2'; get maxImagesPerCall(): number { return modelMaxImagesPerCall[this.modelId] ?? 1; } get provider(): string { return this.config.provider; } constructor( readonly modelId: OpenAIImageModelId, private readonly config: OpenAIImageModelConfig, ) {} async doGenerate({ prompt, n, size, aspectRatio, seed, providerOptions, headers, abortSignal, }: Parameters<ImageModelV2['doGenerate']>[0]): Promise< Awaited<ReturnType<ImageModelV2['doGenerate']>> > { const warnings: Array<ImageModelV2CallWarning> = []; if (aspectRatio != null) { warnings.push({ type: 'unsupported-setting', setting: 'aspectRatio', details: 'This model does not support aspect ratio. Use `size` instead.', }); } if (seed != null) { warnings.push({ type: 'unsupported-setting', setting: 'seed' }); } const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { value: response, responseHeaders } = await postJsonToApi({ url: this.config.url({ path: '/images/generations', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), headers), body: { model: this.modelId, prompt, n, size, ...(providerOptions.openai ?? {}), ...(!hasDefaultResponseFormat.has(this.modelId) ? { response_format: 'b64_json' } : {}), }, failedResponseHandler: openaiFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( openaiImageResponseSchema, ), abortSignal, fetch: this.config.fetch, }); return { images: response.data.map(item => item.b64_json), warnings, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, }, providerMetadata: { openai: { images: response.data.map(item => item.revised_prompt ? { revisedPrompt: item.revised_prompt, } : null, ), }, }, }; } } // minimal version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const openaiImageResponseSchema = z.object({ data: z.array( z.object({ b64_json: z.string(), revised_prompt: z.string().optional() }), ), }); --- File: /ai/packages/openai/src/openai-image-settings.ts --- export type OpenAIImageModelId = | 'gpt-image-1' | 'dall-e-3' | 'dall-e-2' | (string & {}); // https://platform.openai.com/docs/guides/images export const modelMaxImagesPerCall: Record<OpenAIImageModelId, number> = { 'dall-e-3': 1, 'dall-e-2': 10, 'gpt-image-1': 10, }; export const hasDefaultResponseFormat = new Set(['gpt-image-1']); --- File: /ai/packages/openai/src/openai-prepare-tools.test.ts --- import { prepareTools } from './openai-prepare-tools'; it('should return undefined tools and toolChoice when tools are null', () => { const result = prepareTools({ tools: undefined, structuredOutputs: false, strictJsonSchema: false, }); expect(result).toEqual({ tools: undefined, toolChoice: undefined, toolWarnings: [], }); }); it('should return undefined tools and toolChoice when tools are empty', () => { const result = prepareTools({ tools: [], structuredOutputs: false, strictJsonSchema: false, }); expect(result).toEqual({ tools: undefined, toolChoice: undefined, toolWarnings: [], }); }); it('should correctly prepare function tools', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'A test function', inputSchema: { type: 'object', properties: {} }, }, ], structuredOutputs: false, strictJsonSchema: false, }); expect(result.tools).toEqual([ { type: 'function', function: { name: 'testFunction', description: 'A test function', parameters: { type: 'object', properties: {} }, strict: undefined, }, }, ]); expect(result.toolChoice).toBeUndefined(); expect(result.toolWarnings).toEqual([]); }); it('should correctly prepare provider-defined-server tools', () => { const result = prepareTools({ tools: [ { type: 'provider-defined', id: 'openai.file_search', name: 'file_search', args: { vectorStoreIds: ['vs_123'], maxNumResults: 10, ranking: { ranker: 'auto', }, }, }, { type: 'provider-defined', id: 'openai.web_search_preview', name: 'web_search_preview', args: { searchContextSize: 'high', userLocation: { type: 'approximate', city: 'San Francisco', region: 'CA', }, }, }, ], structuredOutputs: false, strictJsonSchema: false, }); expect(result.tools).toEqual([ { type: 'file_search', vector_store_ids: ['vs_123'], max_num_results: 10, ranking_options: { ranker: 'auto', }, }, { type: 'web_search_preview', search_context_size: 'high', user_location: { type: 'approximate', city: 'San Francisco', region: 'CA', }, }, ]); expect(result.toolChoice).toBeUndefined(); expect(result.toolWarnings).toEqual([]); }); it('should correctly prepare file_search with filters', () => { const result = prepareTools({ tools: [ { type: 'provider-defined', id: 'openai.file_search', name: 'file_search', args: { vectorStoreIds: ['vs_123'], maxNumResults: 5, filters: { type: 'and', filters: [ { key: 'author', type: 'eq', value: 'John Doe' }, { key: 'date', type: 'gte', value: '2023-01-01' }, ], }, }, }, ], structuredOutputs: false, strictJsonSchema: false, }); expect(result.tools).toEqual([ { type: 'file_search', vector_store_ids: ['vs_123'], max_num_results: 5, ranking_options: undefined, filters: { type: 'and', filters: [ { key: 'author', type: 'eq', value: 'John Doe' }, { key: 'date', type: 'gte', value: '2023-01-01' }, ], }, }, ]); }); it('should add warnings for unsupported tools', () => { const result = prepareTools({ tools: [ { type: 'provider-defined', id: 'openai.unsupported_tool', name: 'unsupported_tool', args: {}, }, ], structuredOutputs: false, strictJsonSchema: false, }); expect(result.tools).toEqual([]); expect(result.toolChoice).toBeUndefined(); expect(result.toolWarnings).toMatchInlineSnapshot(` [ { "tool": { "args": {}, "id": "openai.unsupported_tool", "name": "unsupported_tool", "type": "provider-defined", }, "type": "unsupported-tool", }, ] `); }); it('should add warnings for unsupported provider-defined tools', () => { const result = prepareTools({ tools: [ { type: 'provider-defined', id: 'some.client_tool', name: 'clientTool', args: {}, } as any, ], structuredOutputs: false, strictJsonSchema: false, }); expect(result.tools).toEqual([]); expect(result.toolChoice).toBeUndefined(); expect(result.toolWarnings).toEqual([ { type: 'unsupported-tool', tool: { type: 'provider-defined', id: 'some.client_tool', name: 'clientTool', args: {}, }, }, ]); }); it('should handle tool choice "auto"', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'Test', inputSchema: {}, }, ], toolChoice: { type: 'auto' }, structuredOutputs: false, strictJsonSchema: false, }); expect(result.toolChoice).toEqual('auto'); }); it('should handle tool choice "required"', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'Test', inputSchema: {}, }, ], toolChoice: { type: 'required' }, structuredOutputs: false, strictJsonSchema: false, }); expect(result.toolChoice).toEqual('required'); }); it('should handle tool choice "none"', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'Test', inputSchema: {}, }, ], toolChoice: { type: 'none' }, structuredOutputs: false, strictJsonSchema: false, }); expect(result.toolChoice).toEqual('none'); }); it('should handle tool choice "tool"', () => { const result = prepareTools({ tools: [ { type: 'function', name: 'testFunction', description: 'Test', inputSchema: {}, }, ], toolChoice: { type: 'tool', toolName: 'testFunction' }, structuredOutputs: false, strictJsonSchema: false, }); expect(result.toolChoice).toEqual({ type: 'function', function: { name: 'testFunction' }, }); }); --- File: /ai/packages/openai/src/openai-prepare-tools.ts --- import { LanguageModelV2CallOptions, LanguageModelV2CallWarning, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { OpenAITools, OpenAIToolChoice } from './openai-types'; import { fileSearchArgsSchema } from './tool/file-search'; import { webSearchPreviewArgsSchema } from './tool/web-search-preview'; export function prepareTools({ tools, toolChoice, structuredOutputs, strictJsonSchema, }: { tools: LanguageModelV2CallOptions['tools']; toolChoice?: LanguageModelV2CallOptions['toolChoice']; structuredOutputs: boolean; strictJsonSchema: boolean; }): { tools?: OpenAITools; toolChoice?: OpenAIToolChoice; toolWarnings: Array<LanguageModelV2CallWarning>; } { // when the tools array is empty, change it to undefined to prevent errors: tools = tools?.length ? tools : undefined; const toolWarnings: LanguageModelV2CallWarning[] = []; if (tools == null) { return { tools: undefined, toolChoice: undefined, toolWarnings }; } const openaiTools: OpenAITools = []; for (const tool of tools) { switch (tool.type) { case 'function': openaiTools.push({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.inputSchema, strict: structuredOutputs ? strictJsonSchema : undefined, }, }); break; case 'provider-defined': switch (tool.id) { case 'openai.file_search': { const args = fileSearchArgsSchema.parse(tool.args); openaiTools.push({ type: 'file_search', vector_store_ids: args.vectorStoreIds, max_num_results: args.maxNumResults, ranking_options: args.ranking ? { ranker: args.ranking.ranker } : undefined, filters: args.filters, }); break; } case 'openai.web_search_preview': { const args = webSearchPreviewArgsSchema.parse(tool.args); openaiTools.push({ type: 'web_search_preview', search_context_size: args.searchContextSize, user_location: args.userLocation, }); break; } default: toolWarnings.push({ type: 'unsupported-tool', tool }); break; } break; default: toolWarnings.push({ type: 'unsupported-tool', tool }); break; } } if (toolChoice == null) { return { tools: openaiTools, toolChoice: undefined, toolWarnings }; } const type = toolChoice.type; switch (type) { case 'auto': case 'none': case 'required': return { tools: openaiTools, toolChoice: type, toolWarnings }; case 'tool': return { tools: openaiTools, toolChoice: { type: 'function', function: { name: toolChoice.toolName, }, }, toolWarnings, }; default: { const _exhaustiveCheck: never = type; throw new UnsupportedFunctionalityError({ functionality: `tool choice type: ${_exhaustiveCheck}`, }); } } } --- File: /ai/packages/openai/src/openai-provider.ts --- import { EmbeddingModelV2, ImageModelV2, LanguageModelV2, ProviderV2, TranscriptionModelV2, SpeechModelV2, } from '@ai-sdk/provider'; import { FetchFunction, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { OpenAIChatLanguageModel } from './openai-chat-language-model'; import { OpenAIChatModelId } from './openai-chat-options'; import { OpenAICompletionLanguageModel } from './openai-completion-language-model'; import { OpenAICompletionModelId } from './openai-completion-options'; import { OpenAIEmbeddingModel } from './openai-embedding-model'; import { OpenAIEmbeddingModelId } from './openai-embedding-options'; import { OpenAIImageModel } from './openai-image-model'; import { OpenAIImageModelId } from './openai-image-settings'; import { openaiTools } from './openai-tools'; import { OpenAITranscriptionModel } from './openai-transcription-model'; import { OpenAITranscriptionModelId } from './openai-transcription-options'; import { OpenAIResponsesLanguageModel } from './responses/openai-responses-language-model'; import { OpenAIResponsesModelId } from './responses/openai-responses-settings'; import { OpenAISpeechModel } from './openai-speech-model'; import { OpenAISpeechModelId } from './openai-speech-options'; export interface OpenAIProvider extends ProviderV2 { (modelId: OpenAIResponsesModelId): LanguageModelV2; /** Creates an OpenAI model for text generation. */ languageModel(modelId: OpenAIResponsesModelId): OpenAIResponsesLanguageModel; /** Creates an OpenAI chat model for text generation. */ chat(modelId: OpenAIChatModelId): LanguageModelV2; /** Creates an OpenAI responses API model for text generation. */ responses(modelId: OpenAIResponsesModelId): LanguageModelV2; /** Creates an OpenAI completion model for text generation. */ completion(modelId: OpenAICompletionModelId): LanguageModelV2; /** Creates a model for text embeddings. */ embedding(modelId: OpenAIEmbeddingModelId): EmbeddingModelV2<string>; /** Creates a model for text embeddings. */ textEmbedding(modelId: OpenAIEmbeddingModelId): EmbeddingModelV2<string>; /** Creates a model for text embeddings. */ textEmbeddingModel(modelId: OpenAIEmbeddingModelId): EmbeddingModelV2<string>; /** Creates a model for image generation. */ image(modelId: OpenAIImageModelId): ImageModelV2; /** Creates a model for image generation. */ imageModel(modelId: OpenAIImageModelId): ImageModelV2; /** Creates a model for transcription. */ transcription(modelId: OpenAITranscriptionModelId): TranscriptionModelV2; /** Creates a model for speech generation. */ speech(modelId: OpenAISpeechModelId): SpeechModelV2; /** OpenAI-specific tools. */ tools: typeof openaiTools; } export interface OpenAIProviderSettings { /** Base URL for the OpenAI API calls. */ baseURL?: string; /** API key for authenticating requests. */ apiKey?: string; /** OpenAI Organization. */ organization?: string; /** OpenAI project. */ project?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Provider name. Overrides the `openai` default name for 3rd party providers. */ name?: string; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } /** Create an OpenAI provider instance. */ export function createOpenAI( options: OpenAIProviderSettings = {}, ): OpenAIProvider { const baseURL = withoutTrailingSlash(options.baseURL) ?? 'https://api.openai.com/v1'; const providerName = options.name ?? 'openai'; const getHeaders = () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'OPENAI_API_KEY', description: 'OpenAI', })}`, 'OpenAI-Organization': options.organization, 'OpenAI-Project': options.project, ...options.headers, }); const createChatModel = (modelId: OpenAIChatModelId) => new OpenAIChatLanguageModel(modelId, { provider: `${providerName}.chat`, url: ({ path }) => `${baseURL}${path}`, headers: getHeaders, fetch: options.fetch, }); const createCompletionModel = (modelId: OpenAICompletionModelId) => new OpenAICompletionLanguageModel(modelId, { provider: `${providerName}.completion`, url: ({ path }) => `${baseURL}${path}`, headers: getHeaders, fetch: options.fetch, }); const createEmbeddingModel = (modelId: OpenAIEmbeddingModelId) => new OpenAIEmbeddingModel(modelId, { provider: `${providerName}.embedding`, url: ({ path }) => `${baseURL}${path}`, headers: getHeaders, fetch: options.fetch, }); const createImageModel = (modelId: OpenAIImageModelId) => new OpenAIImageModel(modelId, { provider: `${providerName}.image`, url: ({ path }) => `${baseURL}${path}`, headers: getHeaders, fetch: options.fetch, }); const createTranscriptionModel = (modelId: OpenAITranscriptionModelId) => new OpenAITranscriptionModel(modelId, { provider: `${providerName}.transcription`, url: ({ path }) => `${baseURL}${path}`, headers: getHeaders, fetch: options.fetch, }); const createSpeechModel = (modelId: OpenAISpeechModelId) => new OpenAISpeechModel(modelId, { provider: `${providerName}.speech`, url: ({ path }) => `${baseURL}${path}`, headers: getHeaders, fetch: options.fetch, }); const createLanguageModel = (modelId: OpenAIResponsesModelId) => { if (new.target) { throw new Error( 'The OpenAI model function cannot be called with the new keyword.', ); } return createResponsesModel(modelId); }; const createResponsesModel = (modelId: OpenAIResponsesModelId) => { return new OpenAIResponsesLanguageModel(modelId, { provider: `${providerName}.responses`, url: ({ path }) => `${baseURL}${path}`, headers: getHeaders, fetch: options.fetch, }); }; const provider = function (modelId: OpenAIResponsesModelId) { return createLanguageModel(modelId); }; provider.languageModel = createLanguageModel; provider.chat = createChatModel; provider.completion = createCompletionModel; provider.responses = createResponsesModel; provider.embedding = createEmbeddingModel; provider.textEmbedding = createEmbeddingModel; provider.textEmbeddingModel = createEmbeddingModel; provider.image = createImageModel; provider.imageModel = createImageModel; provider.transcription = createTranscriptionModel; provider.transcriptionModel = createTranscriptionModel; provider.speech = createSpeechModel; provider.speechModel = createSpeechModel; provider.tools = openaiTools; return provider as OpenAIProvider; } /** Default OpenAI provider instance. */ export const openai = createOpenAI(); --- File: /ai/packages/openai/src/openai-speech-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { OpenAISpeechModel } from './openai-speech-model'; import { createOpenAI } from './openai-provider'; const provider = createOpenAI({ apiKey: 'test-api-key' }); const model = provider.speech('tts-1'); const server = createTestServer({ 'https://api.openai.com/v1/audio/speech': {}, }); describe('doGenerate', () => { function prepareAudioResponse({ headers, format = 'mp3', }: { headers?: Record<string, string>; format?: 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm'; } = {}) { const audioBuffer = new Uint8Array(100); // Mock audio data server.urls['https://api.openai.com/v1/audio/speech'].response = { type: 'binary', headers: { 'content-type': `audio/${format}`, ...headers, }, body: Buffer.from(audioBuffer), }; return audioBuffer; } it('should pass the model and text', async () => { prepareAudioResponse(); await model.doGenerate({ text: 'Hello from the AI SDK!', }); expect(await server.calls[0].requestBodyJson).toMatchObject({ model: 'tts-1', input: 'Hello from the AI SDK!', }); }); it('should pass headers', async () => { prepareAudioResponse(); const provider = createOpenAI({ apiKey: 'test-api-key', organization: 'test-organization', project: 'test-project', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.speech('tts-1').doGenerate({ text: 'Hello from the AI SDK!', headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toMatchObject({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', 'openai-organization': 'test-organization', 'openai-project': 'test-project', }); }); it('should pass options', async () => { prepareAudioResponse(); await model.doGenerate({ text: 'Hello from the AI SDK!', voice: 'nova', outputFormat: 'opus', speed: 1.5, }); expect(await server.calls[0].requestBodyJson).toMatchObject({ model: 'tts-1', input: 'Hello from the AI SDK!', voice: 'nova', speed: 1.5, response_format: 'opus', }); }); it('should return audio data with correct content type', async () => { const audio = new Uint8Array(100); // Mock audio data prepareAudioResponse({ format: 'opus', headers: { 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); const result = await model.doGenerate({ text: 'Hello from the AI SDK!', outputFormat: 'opus', }); expect(result.audio).toStrictEqual(audio); }); it('should include response data with timestamp, modelId and headers', async () => { prepareAudioResponse({ headers: { 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); const testDate = new Date(0); const customModel = new OpenAISpeechModel('tts-1', { provider: 'test-provider', url: () => 'https://api.openai.com/v1/audio/speech', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ text: 'Hello from the AI SDK!', }); expect(result.response).toMatchObject({ timestamp: testDate, modelId: 'tts-1', headers: { 'content-type': 'audio/mp3', 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); }); it('should use real date when no custom date provider is specified', async () => { prepareAudioResponse(); const testDate = new Date(0); const customModel = new OpenAISpeechModel('tts-1', { provider: 'test-provider', url: () => 'https://api.openai.com/v1/audio/speech', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ text: 'Hello from the AI SDK!', }); expect(result.response.timestamp.getTime()).toEqual(testDate.getTime()); expect(result.response.modelId).toBe('tts-1'); }); it('should handle different audio formats', async () => { const formats = ['mp3', 'opus', 'aac', 'flac', 'wav', 'pcm'] as const; for (const format of formats) { const audio = prepareAudioResponse({ format }); const result = await model.doGenerate({ text: 'Hello from the AI SDK!', providerOptions: { openai: { response_format: format, }, }, }); expect(result.audio).toStrictEqual(audio); } }); it('should include warnings if any are generated', async () => { prepareAudioResponse(); const result = await model.doGenerate({ text: 'Hello from the AI SDK!', }); expect(result.warnings).toEqual([]); }); }); --- File: /ai/packages/openai/src/openai-speech-model.ts --- import { SpeechModelV2, SpeechModelV2CallWarning } from '@ai-sdk/provider'; import { combineHeaders, createBinaryResponseHandler, parseProviderOptions, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { OpenAIConfig } from './openai-config'; import { openaiFailedResponseHandler } from './openai-error'; import { OpenAISpeechModelId } from './openai-speech-options'; import { OpenAISpeechAPITypes } from './openai-api-types'; // https://platform.openai.com/docs/api-reference/audio/createSpeech const OpenAIProviderOptionsSchema = z.object({ instructions: z.string().nullish(), speed: z.number().min(0.25).max(4.0).default(1.0).nullish(), }); export type OpenAISpeechCallOptions = z.infer< typeof OpenAIProviderOptionsSchema >; interface OpenAISpeechModelConfig extends OpenAIConfig { _internal?: { currentDate?: () => Date; }; } export class OpenAISpeechModel implements SpeechModelV2 { readonly specificationVersion = 'v2'; get provider(): string { return this.config.provider; } constructor( readonly modelId: OpenAISpeechModelId, private readonly config: OpenAISpeechModelConfig, ) {} private async getArgs({ text, voice = 'alloy', outputFormat = 'mp3', speed, instructions, language, providerOptions, }: Parameters<SpeechModelV2['doGenerate']>[0]) { const warnings: SpeechModelV2CallWarning[] = []; // Parse provider options const openAIOptions = await parseProviderOptions({ provider: 'openai', providerOptions, schema: OpenAIProviderOptionsSchema, }); // Create request body const requestBody: Record<string, unknown> = { model: this.modelId, input: text, voice, response_format: 'mp3', speed, instructions, }; if (outputFormat) { if (['mp3', 'opus', 'aac', 'flac', 'wav', 'pcm'].includes(outputFormat)) { requestBody.response_format = outputFormat; } else { warnings.push({ type: 'unsupported-setting', setting: 'outputFormat', details: `Unsupported output format: ${outputFormat}. Using mp3 instead.`, }); } } // Add provider-specific options if (openAIOptions) { const speechModelOptions: OpenAISpeechAPITypes = {}; for (const key in speechModelOptions) { const value = speechModelOptions[key as keyof OpenAISpeechAPITypes]; if (value !== undefined) { requestBody[key] = value; } } } if (language) { warnings.push({ type: 'unsupported-setting', setting: 'language', details: `OpenAI speech models do not support language selection. Language parameter "${language}" was ignored.`, }); } return { requestBody, warnings, }; } async doGenerate( options: Parameters<SpeechModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<SpeechModelV2['doGenerate']>>> { const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { requestBody, warnings } = await this.getArgs(options); const { value: audio, responseHeaders, rawValue: rawResponse, } = await postJsonToApi({ url: this.config.url({ path: '/audio/speech', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body: requestBody, failedResponseHandler: openaiFailedResponseHandler, successfulResponseHandler: createBinaryResponseHandler(), abortSignal: options.abortSignal, fetch: this.config.fetch, }); return { audio, warnings, request: { body: JSON.stringify(requestBody), }, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, body: rawResponse, }, }; } } --- File: /ai/packages/openai/src/openai-speech-options.ts --- export type OpenAISpeechModelId = | 'tts-1' | 'tts-1-hd' | 'gpt-4o-mini-tts' | (string & {}); --- File: /ai/packages/openai/src/openai-tools.ts --- import { fileSearch } from './tool/file-search'; import { webSearchPreview } from './tool/web-search-preview'; export { fileSearch } from './tool/file-search'; export { webSearchPreview } from './tool/web-search-preview'; export type { OpenAITool, OpenAITools, OpenAIToolChoice, OpenAIFunctionTool, OpenAIFileSearchTool, OpenAIWebSearchPreviewTool, OpenAIWebSearchUserLocation, } from './openai-types'; export const openaiTools = { fileSearch, webSearchPreview, }; --- File: /ai/packages/openai/src/openai-transcription-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { OpenAITranscriptionModel } from './openai-transcription-model'; import { createOpenAI } from './openai-provider'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; const audioData = await readFile(path.join(__dirname, 'transcript-test.mp3')); const provider = createOpenAI({ apiKey: 'test-api-key' }); const model = provider.transcription('whisper-1'); const server = createTestServer({ 'https://api.openai.com/v1/audio/transcriptions': {}, }); describe('doGenerate', () => { function prepareJsonResponse({ headers, }: { headers?: Record<string, string>; } = {}) { server.urls['https://api.openai.com/v1/audio/transcriptions'].response = { type: 'json-value', headers, body: { task: 'transcribe', text: 'Hello from the Vercel AI SDK!', words: [ { word: 'Hello', start: 0, end: 5, }, { word: 'from', start: 5, end: 10, }, { word: 'the', start: 10, end: 15, }, { word: 'Vercel', start: 15, end: 20, }, { word: 'AI', start: 20, end: 25, }, { word: 'SDK', start: 25, end: 30, }, { word: '!', start: 30, end: 35, }, ], durationInSeconds: 35, language: 'en', _request_id: 'req_1234', }, }; } it('should pass the model', async () => { prepareJsonResponse(); await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(await server.calls[0].requestBodyMultipart).toMatchObject({ model: 'whisper-1', }); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createOpenAI({ apiKey: 'test-api-key', organization: 'test-organization', project: 'test-project', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.transcription('whisper-1').doGenerate({ audio: audioData, mediaType: 'audio/wav', headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toMatchObject({ authorization: 'Bearer test-api-key', 'content-type': expect.stringMatching( /^multipart\/form-data; boundary=----formdata-undici-\d+$/, ), 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', 'openai-organization': 'test-organization', 'openai-project': 'test-project', }); }); it('should extract the transcription text', async () => { prepareJsonResponse(); const result = await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.text).toBe('Hello from the Vercel AI SDK!'); }); it('should include response data with timestamp, modelId and headers', async () => { prepareJsonResponse({ headers: { 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); const testDate = new Date(0); const customModel = new OpenAITranscriptionModel('whisper-1', { provider: 'test-provider', url: () => 'https://api.openai.com/v1/audio/transcriptions', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.response).toMatchObject({ timestamp: testDate, modelId: 'whisper-1', headers: { 'content-type': 'application/json', 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); }); it('should use real date when no custom date provider is specified', async () => { prepareJsonResponse(); const testDate = new Date(0); const customModel = new OpenAITranscriptionModel('whisper-1', { provider: 'test-provider', url: () => 'https://api.openai.com/v1/audio/transcriptions', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.response.timestamp.getTime()).toEqual(testDate.getTime()); expect(result.response.modelId).toBe('whisper-1'); }); it('should work when no words, language, or duration are returned', async () => { server.urls['https://api.openai.com/v1/audio/transcriptions'].response = { type: 'json-value', body: { task: 'transcribe', text: 'Hello from the Vercel AI SDK!', _request_id: 'req_1234', }, }; const testDate = new Date(0); const customModel = new OpenAITranscriptionModel('whisper-1', { provider: 'test-provider', url: () => 'https://api.openai.com/v1/audio/transcriptions', headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result).toMatchInlineSnapshot(` { "durationInSeconds": undefined, "language": undefined, "response": { "body": { "_request_id": "req_1234", "task": "transcribe", "text": "Hello from the Vercel AI SDK!", }, "headers": { "content-length": "85", "content-type": "application/json", }, "modelId": "whisper-1", "timestamp": 1970-01-01T00:00:00.000Z, }, "segments": [], "text": "Hello from the Vercel AI SDK!", "warnings": [], } `); }); }); --- File: /ai/packages/openai/src/openai-transcription-model.ts --- import { TranscriptionModelV2, TranscriptionModelV2CallOptions, TranscriptionModelV2CallWarning, } from '@ai-sdk/provider'; import { combineHeaders, convertBase64ToUint8Array, createJsonResponseHandler, parseProviderOptions, postFormDataToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { OpenAIConfig } from './openai-config'; import { openaiFailedResponseHandler } from './openai-error'; import { OpenAITranscriptionModelId, openAITranscriptionProviderOptions, OpenAITranscriptionProviderOptions, } from './openai-transcription-options'; export type OpenAITranscriptionCallOptions = Omit< TranscriptionModelV2CallOptions, 'providerOptions' > & { providerOptions?: { openai?: OpenAITranscriptionProviderOptions; }; }; interface OpenAITranscriptionModelConfig extends OpenAIConfig { _internal?: { currentDate?: () => Date; }; } // https://platform.openai.com/docs/guides/speech-to-text#supported-languages const languageMap = { afrikaans: 'af', arabic: 'ar', armenian: 'hy', azerbaijani: 'az', belarusian: 'be', bosnian: 'bs', bulgarian: 'bg', catalan: 'ca', chinese: 'zh', croatian: 'hr', czech: 'cs', danish: 'da', dutch: 'nl', english: 'en', estonian: 'et', finnish: 'fi', french: 'fr', galician: 'gl', german: 'de', greek: 'el', hebrew: 'he', hindi: 'hi', hungarian: 'hu', icelandic: 'is', indonesian: 'id', italian: 'it', japanese: 'ja', kannada: 'kn', kazakh: 'kk', korean: 'ko', latvian: 'lv', lithuanian: 'lt', macedonian: 'mk', malay: 'ms', marathi: 'mr', maori: 'mi', nepali: 'ne', norwegian: 'no', persian: 'fa', polish: 'pl', portuguese: 'pt', romanian: 'ro', russian: 'ru', serbian: 'sr', slovak: 'sk', slovenian: 'sl', spanish: 'es', swahili: 'sw', swedish: 'sv', tagalog: 'tl', tamil: 'ta', thai: 'th', turkish: 'tr', ukrainian: 'uk', urdu: 'ur', vietnamese: 'vi', welsh: 'cy', }; export class OpenAITranscriptionModel implements TranscriptionModelV2 { readonly specificationVersion = 'v2'; get provider(): string { return this.config.provider; } constructor( readonly modelId: OpenAITranscriptionModelId, private readonly config: OpenAITranscriptionModelConfig, ) {} private async getArgs({ audio, mediaType, providerOptions, }: OpenAITranscriptionCallOptions) { const warnings: TranscriptionModelV2CallWarning[] = []; // Parse provider options const openAIOptions = await parseProviderOptions({ provider: 'openai', providerOptions, schema: openAITranscriptionProviderOptions, }); // Create form data with base fields const formData = new FormData(); const blob = audio instanceof Uint8Array ? new Blob([audio]) : new Blob([convertBase64ToUint8Array(audio)]); formData.append('model', this.modelId); formData.append('file', new File([blob], 'audio', { type: mediaType })); // Add provider-specific options if (openAIOptions) { const transcriptionModelOptions = { include: openAIOptions.include, language: openAIOptions.language, prompt: openAIOptions.prompt, temperature: openAIOptions.temperature, timestamp_granularities: openAIOptions.timestampGranularities, }; for (const [key, value] of Object.entries(transcriptionModelOptions)) { if (value != null) { formData.append(key, String(value)); } } } return { formData, warnings, }; } async doGenerate( options: OpenAITranscriptionCallOptions, ): Promise<Awaited<ReturnType<TranscriptionModelV2['doGenerate']>>> { const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { formData, warnings } = await this.getArgs(options); const { value: response, responseHeaders, rawValue: rawResponse, } = await postFormDataToApi({ url: this.config.url({ path: '/audio/transcriptions', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), formData, failedResponseHandler: openaiFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( openaiTranscriptionResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const language = response.language != null && response.language in languageMap ? languageMap[response.language as keyof typeof languageMap] : undefined; return { text: response.text, segments: response.words?.map(word => ({ text: word.word, startSecond: word.start, endSecond: word.end, })) ?? [], language, durationInSeconds: response.duration ?? undefined, warnings, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, body: rawResponse, }, }; } } const openaiTranscriptionResponseSchema = z.object({ text: z.string(), language: z.string().nullish(), duration: z.number().nullish(), words: z .array( z.object({ word: z.string(), start: z.number(), end: z.number(), }), ) .nullish(), }); --- File: /ai/packages/openai/src/openai-transcription-options.ts --- import { z } from 'zod/v4'; export type OpenAITranscriptionModelId = | 'whisper-1' | 'gpt-4o-mini-transcribe' | 'gpt-4o-transcribe' | (string & {}); // https://platform.openai.com/docs/api-reference/audio/createTranscription export const openAITranscriptionProviderOptions = z.object({ /** * Additional information to include in the transcription response. */ include: z.array(z.string()).optional(), /** * The language of the input audio in ISO-639-1 format. */ language: z.string().optional(), /** * An optional text to guide the model's style or continue a previous audio segment. */ prompt: z.string().optional(), /** * The sampling temperature, between 0 and 1. * @default 0 */ temperature: z.number().min(0).max(1).default(0).optional(), /** * The timestamp granularities to populate for this transcription. * @default ['segment'] */ timestampGranularities: z .array(z.enum(['word', 'segment'])) .default(['segment']) .optional(), }); export type OpenAITranscriptionProviderOptions = z.infer< typeof openAITranscriptionProviderOptions >; --- File: /ai/packages/openai/src/openai-types.ts --- import { JSONSchema7 } from '@ai-sdk/provider'; /** * OpenAI function tool definition */ export interface OpenAIFunctionTool { type: 'function'; function: { name: string; description: string | undefined; parameters: JSONSchema7; strict?: boolean; }; } /** * OpenAI file search tool definition */ export interface OpenAIFileSearchTool { type: 'file_search'; vector_store_ids?: string[]; max_num_results?: number; ranking_options?: { ranker?: 'auto' | 'default-2024-08-21'; }; filters?: | { key: string; type: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte'; value: string | number | boolean; } | { type: 'and' | 'or'; filters: any[]; }; } /** * User location for web search */ export interface OpenAIWebSearchUserLocation { type?: 'approximate'; city?: string; region?: string; country?: string; timezone?: string; } /** * OpenAI web search preview tool definition */ export interface OpenAIWebSearchPreviewTool { type: 'web_search_preview'; search_context_size?: 'low' | 'medium' | 'high'; user_location?: OpenAIWebSearchUserLocation; } /** * Union type for all OpenAI tools */ export type OpenAITool = | OpenAIFunctionTool | OpenAIFileSearchTool | OpenAIWebSearchPreviewTool; /** * OpenAI tool choice options */ export type OpenAIToolChoice = | 'auto' | 'none' | 'required' | { type: 'function'; function: { name: string } }; /** * OpenAI tools array type */ export type OpenAITools = Array<OpenAITool>; --- File: /ai/packages/openai/internal.d.ts --- export * from './dist/internal'; --- File: /ai/packages/openai/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, { entry: ['src/internal/index.ts'], outDir: 'dist/internal', format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/openai/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/openai/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/openai-compatible/src/internal/index.ts --- export { convertToOpenAICompatibleChatMessages } from '../convert-to-openai-compatible-chat-messages'; export { mapOpenAICompatibleFinishReason } from '../map-openai-compatible-finish-reason'; export { getResponseMetadata } from '../get-response-metadata'; export type { OpenAICompatibleChatConfig } from '../openai-compatible-chat-language-model'; --- File: /ai/packages/openai-compatible/src/convert-to-openai-compatible-chat-messages.test.ts --- import { convertToOpenAICompatibleChatMessages } from './convert-to-openai-compatible-chat-messages'; describe('user messages', () => { it('should convert messages with only a text part to a string content', async () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'user', content: [{ type: 'text', text: 'Hello' }], }, ]); expect(result).toEqual([{ role: 'user', content: 'Hello' }]); }); it('should convert messages with image parts', async () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'user', content: [ { type: 'text', text: 'Hello' }, { type: 'file', data: Buffer.from([0, 1, 2, 3]).toString('base64'), mediaType: 'image/png', }, ], }, ]); expect(result).toEqual([ { role: 'user', content: [ { type: 'text', text: 'Hello' }, { type: 'image_url', image_url: { url: 'data:image/png;base64,AAECAw==' }, }, ], }, ]); }); it('should handle URL-based images', async () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'user', content: [ { type: 'file', data: new URL('https://example.com/image.jpg'), mediaType: 'image/*', }, ], }, ]); expect(result).toEqual([ { role: 'user', content: [ { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' }, }, ], }, ]); }); }); describe('tool calls', () => { it('should stringify arguments to tool calls', () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'assistant', content: [ { type: 'tool-call', input: { foo: 'bar123' }, toolCallId: 'quux', toolName: 'thwomp', }, ], }, { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'quux', toolName: 'thwomp', output: { type: 'json', value: { oof: '321rab' } }, }, ], }, ]); expect(result).toEqual([ { role: 'assistant', content: '', tool_calls: [ { type: 'function', id: 'quux', function: { name: 'thwomp', arguments: JSON.stringify({ foo: 'bar123' }), }, }, ], }, { role: 'tool', content: JSON.stringify({ oof: '321rab' }), tool_call_id: 'quux', }, ]); }); it('should handle text output type in tool results', () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'assistant', content: [ { type: 'tool-call', input: { query: 'weather' }, toolCallId: 'call-1', toolName: 'getWeather', }, ], }, { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'call-1', toolName: 'getWeather', output: { type: 'text', value: 'It is sunny today' }, }, ], }, ]); expect(result).toEqual([ { role: 'assistant', content: '', tool_calls: [ { type: 'function', id: 'call-1', function: { name: 'getWeather', arguments: JSON.stringify({ query: 'weather' }), }, }, ], }, { role: 'tool', content: 'It is sunny today', tool_call_id: 'call-1', }, ]); }); }); describe('provider-specific metadata merging', () => { it('should merge system message metadata', async () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'system', content: 'You are a helpful assistant.', providerOptions: { openaiCompatible: { cacheControl: { type: 'ephemeral' }, }, }, }, ]); expect(result).toEqual([ { role: 'system', content: 'You are a helpful assistant.', cacheControl: { type: 'ephemeral' }, }, ]); }); it('should merge user message content metadata', async () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'user', content: [ { type: 'text', text: 'Hello', providerOptions: { openaiCompatible: { cacheControl: { type: 'ephemeral' }, }, }, }, ], }, ]); expect(result).toEqual([ { role: 'user', content: 'Hello', cacheControl: { type: 'ephemeral' }, }, ]); }); it('should prioritize content-level metadata when merging', async () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'user', providerOptions: { openaiCompatible: { messageLevel: true, }, }, content: [ { type: 'text', text: 'Hello', providerOptions: { openaiCompatible: { contentLevel: true, }, }, }, ], }, ]); expect(result).toEqual([ { role: 'user', content: 'Hello', contentLevel: true, }, ]); }); it('should handle tool calls with metadata', async () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'assistant', content: [ { type: 'tool-call', toolCallId: 'call1', toolName: 'calculator', input: { x: 1, y: 2 }, providerOptions: { openaiCompatible: { cacheControl: { type: 'ephemeral' }, }, }, }, ], }, ]); expect(result).toEqual([ { role: 'assistant', content: '', tool_calls: [ { id: 'call1', type: 'function', function: { name: 'calculator', arguments: JSON.stringify({ x: 1, y: 2 }), }, cacheControl: { type: 'ephemeral' }, }, ], }, ]); }); it('should handle image content with metadata', async () => { const imageUrl = new URL('https://example.com/image.jpg'); const result = convertToOpenAICompatibleChatMessages([ { role: 'user', content: [ { type: 'file', data: imageUrl, mediaType: 'image/*', providerOptions: { openaiCompatible: { cacheControl: { type: 'ephemeral' }, }, }, }, ], }, ]); expect(result).toEqual([ { role: 'user', content: [ { type: 'image_url', image_url: { url: imageUrl.toString() }, cacheControl: { type: 'ephemeral' }, }, ], }, ]); }); it('should omit non-openaiCompatible metadata', async () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'system', content: 'Hello', providerOptions: { someOtherProvider: { shouldBeIgnored: true, }, }, }, ]); expect(result).toEqual([ { role: 'system', content: 'Hello', }, ]); }); it('should handle a user message with multiple content parts (text + image)', () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'user', content: [ { type: 'text', text: 'Hello from part 1', providerOptions: { openaiCompatible: { sentiment: 'positive' }, leftoverKey: { foo: 'some leftover data' }, }, }, { type: 'file', data: Buffer.from([0, 1, 2, 3]).toString('base64'), mediaType: 'image/png', providerOptions: { openaiCompatible: { alt_text: 'A sample image' }, }, }, ], providerOptions: { openaiCompatible: { priority: 'high' }, }, }, ]); expect(result).toEqual([ { role: 'user', priority: 'high', // hoisted from message-level providerOptions content: [ { type: 'text', text: 'Hello from part 1', sentiment: 'positive', // hoisted from part-level openaiCompatible }, { type: 'image_url', image_url: { url: 'data:image/png;base64,AAECAw==', }, alt_text: 'A sample image', }, ], }, ]); }); it('should handle a user message with multiple text parts (flattening disabled)', () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'user', content: [ { type: 'text', text: 'Part 1' }, { type: 'text', text: 'Part 2' }, ], }, ]); // Because there are multiple text parts, the converter won't flatten them expect(result).toEqual([ { role: 'user', content: [ { type: 'text', text: 'Part 1' }, { type: 'text', text: 'Part 2' }, ], }, ]); }); it('should handle an assistant message with text plus multiple tool calls', () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'assistant', content: [ { type: 'text', text: 'Checking that now...' }, { type: 'tool-call', toolCallId: 'call1', toolName: 'searchTool', input: { query: 'Weather' }, providerOptions: { openaiCompatible: { function_call_reason: 'user request' }, }, }, { type: 'text', text: 'Almost there...' }, { type: 'tool-call', toolCallId: 'call2', toolName: 'mapsTool', input: { location: 'Paris' }, }, ], }, ]); expect(result).toEqual([ { role: 'assistant', content: 'Checking that now...Almost there...', tool_calls: [ { id: 'call1', type: 'function', function: { name: 'searchTool', arguments: JSON.stringify({ query: 'Weather' }), }, function_call_reason: 'user request', }, { id: 'call2', type: 'function', function: { name: 'mapsTool', arguments: JSON.stringify({ location: 'Paris' }), }, }, ], }, ]); }); it('should handle a single tool role message with multiple tool-result parts', () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'tool', providerOptions: { // this just gets omitted as we prioritize content-level metadata openaiCompatible: { responseTier: 'detailed' }, }, content: [ { type: 'tool-result', toolCallId: 'call123', toolName: 'calculator', output: { type: 'json', value: { stepOne: 'data chunk 1' } }, }, { type: 'tool-result', toolCallId: 'call123', toolName: 'calculator', providerOptions: { openaiCompatible: { partial: true }, }, output: { type: 'json', value: { stepTwo: 'data chunk 2' } }, }, ], }, ]); expect(result).toEqual([ { role: 'tool', tool_call_id: 'call123', content: JSON.stringify({ stepOne: 'data chunk 1' }), }, { role: 'tool', tool_call_id: 'call123', content: JSON.stringify({ stepTwo: 'data chunk 2' }), partial: true, }, ]); }); it('should handle multiple content parts with multiple metadata layers', () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'user', providerOptions: { openaiCompatible: { messageLevel: 'global-metadata' }, leftoverForMessage: { x: 123 }, }, content: [ { type: 'text', text: 'Part A', providerOptions: { openaiCompatible: { textPartLevel: 'localized' }, leftoverForText: { info: 'text leftover' }, }, }, { type: 'file', data: Buffer.from([9, 8, 7, 6]).toString('base64'), mediaType: 'image/png', providerOptions: { openaiCompatible: { imagePartLevel: 'image-data' }, }, }, ], }, ]); expect(result).toEqual([ { role: 'user', messageLevel: 'global-metadata', content: [ { type: 'text', text: 'Part A', textPartLevel: 'localized', }, { type: 'image_url', image_url: { url: 'data:image/png;base64,CQgHBg==', }, imagePartLevel: 'image-data', }, ], }, ]); }); it('should handle different tool metadata vs. message-level metadata', () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'assistant', providerOptions: { openaiCompatible: { globalPriority: 'high' }, }, content: [ { type: 'text', text: 'Initiating tool calls...' }, { type: 'tool-call', toolCallId: 'callXYZ', toolName: 'awesomeTool', input: { param: 'someValue' }, providerOptions: { openaiCompatible: { toolPriority: 'critical', }, }, }, ], }, ]); expect(result).toEqual([ { role: 'assistant', globalPriority: 'high', content: 'Initiating tool calls...', tool_calls: [ { id: 'callXYZ', type: 'function', function: { name: 'awesomeTool', arguments: JSON.stringify({ param: 'someValue' }), }, toolPriority: 'critical', }, ], }, ]); }); it('should handle metadata collisions and overwrites in tool calls', () => { const result = convertToOpenAICompatibleChatMessages([ { role: 'assistant', providerOptions: { openaiCompatible: { cacheControl: { type: 'default' }, sharedKey: 'assistantLevel', }, }, content: [ { type: 'tool-call', toolCallId: 'collisionToolCall', toolName: 'collider', input: { num: 42 }, providerOptions: { openaiCompatible: { cacheControl: { type: 'ephemeral' }, // overwrites top-level sharedKey: 'toolLevel', }, }, }, ], }, ]); expect(result).toEqual([ { role: 'assistant', cacheControl: { type: 'default' }, sharedKey: 'assistantLevel', content: '', tool_calls: [ { id: 'collisionToolCall', type: 'function', function: { name: 'collider', arguments: JSON.stringify({ num: 42 }), }, cacheControl: { type: 'ephemeral' }, sharedKey: 'toolLevel', }, ], }, ]); }); }); --- File: /ai/packages/openai-compatible/src/convert-to-openai-compatible-chat-messages.ts --- import { LanguageModelV2Prompt, SharedV2ProviderMetadata, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { OpenAICompatibleChatPrompt } from './openai-compatible-api-types'; function getOpenAIMetadata(message: { providerOptions?: SharedV2ProviderMetadata; }) { return message?.providerOptions?.openaiCompatible ?? {}; } export function convertToOpenAICompatibleChatMessages( prompt: LanguageModelV2Prompt, ): OpenAICompatibleChatPrompt { const messages: OpenAICompatibleChatPrompt = []; for (const { role, content, ...message } of prompt) { const metadata = getOpenAIMetadata({ ...message }); switch (role) { case 'system': { messages.push({ role: 'system', content, ...metadata }); break; } case 'user': { if (content.length === 1 && content[0].type === 'text') { messages.push({ role: 'user', content: content[0].text, ...getOpenAIMetadata(content[0]), }); break; } messages.push({ role: 'user', content: content.map(part => { const partMetadata = getOpenAIMetadata(part); switch (part.type) { case 'text': { return { type: 'text', text: part.text, ...partMetadata }; } case 'file': { if (part.mediaType.startsWith('image/')) { const mediaType = part.mediaType === 'image/*' ? 'image/jpeg' : part.mediaType; return { type: 'image_url', image_url: { url: part.data instanceof URL ? part.data.toString() : `data:${mediaType};base64,${part.data}`, }, ...partMetadata, }; } else { throw new UnsupportedFunctionalityError({ functionality: `file part media type ${part.mediaType}`, }); } } } }), ...metadata, }); break; } case 'assistant': { let text = ''; const toolCalls: Array<{ id: string; type: 'function'; function: { name: string; arguments: string }; }> = []; for (const part of content) { const partMetadata = getOpenAIMetadata(part); switch (part.type) { case 'text': { text += part.text; break; } case 'tool-call': { toolCalls.push({ id: part.toolCallId, type: 'function', function: { name: part.toolName, arguments: JSON.stringify(part.input), }, ...partMetadata, }); break; } } } messages.push({ role: 'assistant', content: text, tool_calls: toolCalls.length > 0 ? toolCalls : undefined, ...metadata, }); break; } case 'tool': { for (const toolResponse of content) { const output = toolResponse.output; let contentValue: string; switch (output.type) { case 'text': case 'error-text': contentValue = output.value; break; case 'content': case 'json': case 'error-json': contentValue = JSON.stringify(output.value); break; } const toolResponseMetadata = getOpenAIMetadata(toolResponse); messages.push({ role: 'tool', tool_call_id: toolResponse.toolCallId, content: contentValue, ...toolResponseMetadata, }); } break; } default: { const _exhaustiveCheck: never = role; throw new Error(`Unsupported role: ${_exhaustiveCheck}`); } } } return messages; } --- File: /ai/packages/openai-compatible/src/convert-to-openai-compatible-completion-prompt.ts --- import { InvalidPromptError, LanguageModelV2Prompt, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; export function convertToOpenAICompatibleCompletionPrompt({ prompt, user = 'user', assistant = 'assistant', }: { prompt: LanguageModelV2Prompt; user?: string; assistant?: string; }): { prompt: string; stopSequences?: string[]; } { // transform to a chat message format: let text = ''; // if first message is a system message, add it to the text: if (prompt[0].role === 'system') { text += `${prompt[0].content}\n\n`; prompt = prompt.slice(1); } for (const { role, content } of prompt) { switch (role) { case 'system': { throw new InvalidPromptError({ message: 'Unexpected system message in prompt: ${content}', prompt, }); } case 'user': { const userMessage = content .map(part => { switch (part.type) { case 'text': { return part.text; } } }) .filter(Boolean) .join(''); text += `${user}:\n${userMessage}\n\n`; break; } case 'assistant': { const assistantMessage = content .map(part => { switch (part.type) { case 'text': { return part.text; } case 'tool-call': { throw new UnsupportedFunctionalityError({ functionality: 'tool-call messages', }); } } }) .join(''); text += `${assistant}:\n${assistantMessage}\n\n`; break; } case 'tool': { throw new UnsupportedFunctionalityError({ functionality: 'tool messages', }); } default: { const _exhaustiveCheck: never = role; throw new Error(`Unsupported role: ${_exhaustiveCheck}`); } } } // Assistant message prefix: text += `${assistant}:\n`; return { prompt: text, stopSequences: [`\n${user}:`], }; } --- File: /ai/packages/openai-compatible/src/get-response-metadata.ts --- export function getResponseMetadata({ id, model, created, }: { id?: string | undefined | null; created?: number | undefined | null; model?: string | undefined | null; }) { return { id: id ?? undefined, modelId: model ?? undefined, timestamp: created != null ? new Date(created * 1000) : undefined, }; } --- File: /ai/packages/openai-compatible/src/index.ts --- export { OpenAICompatibleChatLanguageModel } from './openai-compatible-chat-language-model'; export type { OpenAICompatibleChatModelId, OpenAICompatibleProviderOptions, } from './openai-compatible-chat-options'; export { OpenAICompatibleCompletionLanguageModel } from './openai-compatible-completion-language-model'; export type { OpenAICompatibleCompletionModelId, OpenAICompatibleCompletionProviderOptions, } from './openai-compatible-completion-options'; export { OpenAICompatibleEmbeddingModel } from './openai-compatible-embedding-model'; export type { OpenAICompatibleEmbeddingModelId, OpenAICompatibleEmbeddingProviderOptions, } from './openai-compatible-embedding-options'; export { OpenAICompatibleImageModel } from './openai-compatible-image-model'; export type { OpenAICompatibleErrorData, ProviderErrorStructure, } from './openai-compatible-error'; export type { MetadataExtractor } from './openai-compatible-metadata-extractor'; export { createOpenAICompatible } from './openai-compatible-provider'; export type { OpenAICompatibleProvider, OpenAICompatibleProviderSettings, } from './openai-compatible-provider'; --- File: /ai/packages/openai-compatible/src/map-openai-compatible-finish-reason.ts --- import { LanguageModelV2FinishReason } from '@ai-sdk/provider'; export function mapOpenAICompatibleFinishReason( finishReason: string | null | undefined, ): LanguageModelV2FinishReason { switch (finishReason) { case 'stop': return 'stop'; case 'length': return 'length'; case 'content_filter': return 'content-filter'; case 'function_call': case 'tool_calls': return 'tool-calls'; default: return 'unknown'; } } --- File: /ai/packages/openai-compatible/src/openai-compatible-api-types.ts --- import { JSONValue } from '@ai-sdk/provider'; export type OpenAICompatibleChatPrompt = Array<OpenAICompatibleMessage>; export type OpenAICompatibleMessage = | OpenAICompatibleSystemMessage | OpenAICompatibleUserMessage | OpenAICompatibleAssistantMessage | OpenAICompatibleToolMessage; // Allow for arbitrary additional properties for general purpose // provider-metadata-specific extensibility. type JsonRecord<T = never> = Record< string, JSONValue | JSONValue[] | T | T[] | undefined >; export interface OpenAICompatibleSystemMessage extends JsonRecord { role: 'system'; content: string; } export interface OpenAICompatibleUserMessage extends JsonRecord<OpenAICompatibleContentPart> { role: 'user'; content: string | Array<OpenAICompatibleContentPart>; } export type OpenAICompatibleContentPart = | OpenAICompatibleContentPartText | OpenAICompatibleContentPartImage; export interface OpenAICompatibleContentPartImage extends JsonRecord { type: 'image_url'; image_url: { url: string }; } export interface OpenAICompatibleContentPartText extends JsonRecord { type: 'text'; text: string; } export interface OpenAICompatibleAssistantMessage extends JsonRecord<OpenAICompatibleMessageToolCall> { role: 'assistant'; content?: string | null; tool_calls?: Array<OpenAICompatibleMessageToolCall>; } export interface OpenAICompatibleMessageToolCall extends JsonRecord { type: 'function'; id: string; function: { arguments: string; name: string; }; } export interface OpenAICompatibleToolMessage extends JsonRecord { role: 'tool'; content: string; tool_call_id: string; } --- File: /ai/packages/openai-compatible/src/openai-compatible-chat-language-model.test.ts --- import { LanguageModelV2Prompt } from '@ai-sdk/provider'; import { convertReadableStreamToArray, createTestServer, isNodeVersion, } from '@ai-sdk/provider-utils/test'; import { OpenAICompatibleChatLanguageModel } from './openai-compatible-chat-language-model'; import { createOpenAICompatible } from './openai-compatible-provider'; const TEST_PROMPT: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; const provider = createOpenAICompatible({ baseURL: 'https://my.api.com/v1/', name: 'test-provider', headers: { Authorization: `Bearer test-api-key`, }, }); const model = provider('grok-beta'); const server = createTestServer({ 'https://my.api.com/v1/chat/completions': {}, }); describe('config', () => { it('should extract base name from provider string', () => { const model = new OpenAICompatibleChatLanguageModel('gpt-4', { provider: 'anthropic.beta', url: () => '', headers: () => ({}), }); expect(model['providerOptionsName']).toBe('anthropic'); }); it('should handle provider without dot notation', () => { const model = new OpenAICompatibleChatLanguageModel('gpt-4', { provider: 'openai', url: () => '', headers: () => ({}), }); expect(model['providerOptionsName']).toBe('openai'); }); it('should return empty for empty provider', () => { const model = new OpenAICompatibleChatLanguageModel('gpt-4', { provider: '', url: () => '', headers: () => ({}), }); expect(model['providerOptionsName']).toBe(''); }); }); describe('doGenerate', () => { function prepareJsonResponse({ content = '', reasoning_content = '', tool_calls, function_call, usage = { prompt_tokens: 4, total_tokens: 34, completion_tokens: 30, }, finish_reason = 'stop', id = 'chatcmpl-95ZTZkhr0mHNKqerQfiwkuox3PHAd', created = 1711115037, model = 'grok-beta', headers, }: { content?: string; reasoning_content?: string; tool_calls?: Array<{ id: string; type: 'function'; function: { name: string; arguments: string; }; }>; function_call?: { name: string; arguments: string; }; usage?: { prompt_tokens?: number; total_tokens?: number; completion_tokens?: number; prompt_tokens_details?: { cached_tokens?: number; }; completion_tokens_details?: { reasoning_tokens?: number; accepted_prediction_tokens?: number; rejected_prediction_tokens?: number; }; }; finish_reason?: string; created?: number; id?: string; model?: string; headers?: Record<string, string>; } = {}) { server.urls['https://my.api.com/v1/chat/completions'].response = { type: 'json-value', headers, body: { id, object: 'chat.completion', created, model, choices: [ { index: 0, message: { role: 'assistant', content, reasoning_content, tool_calls, function_call, }, finish_reason, }, ], usage, system_fingerprint: 'fp_3bc1b5746c', }, }; } it('should pass user setting to requests', async () => { prepareJsonResponse({ content: 'Hello, World!' }); const modelWithUser = provider('grok-beta'); await modelWithUser.doGenerate({ prompt: TEST_PROMPT, providerOptions: { xai: { user: 'test-user-id', }, }, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "messages": [ { "content": "Hello", "role": "user", }, ], "model": "grok-beta", } `); }); it('should extract text response', async () => { prepareJsonResponse({ content: 'Hello, World!' }); const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "text": "Hello, World!", "type": "text", }, ] `); }); it('should extract reasoning content', async () => { prepareJsonResponse({ content: 'Hello, World!', reasoning_content: 'This is the reasoning behind the response', }); const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "text": "Hello, World!", "type": "text", }, { "text": "This is the reasoning behind the response", "type": "reasoning", }, ] `); }); it('should extract usage', async () => { prepareJsonResponse({ usage: { prompt_tokens: 20, total_tokens: 25, completion_tokens: 5 }, }); const { usage } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(usage).toMatchInlineSnapshot(` { "cachedInputTokens": undefined, "inputTokens": 20, "outputTokens": 5, "reasoningTokens": undefined, "totalTokens": 25, } `); }); it('should send additional response information', async () => { prepareJsonResponse({ id: 'test-id', created: 123, model: 'test-model', }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response).toMatchInlineSnapshot(` { "body": { "choices": [ { "finish_reason": "stop", "index": 0, "message": { "content": "", "reasoning_content": "", "role": "assistant", }, }, ], "created": 123, "id": "test-id", "model": "test-model", "object": "chat.completion", "system_fingerprint": "fp_3bc1b5746c", "usage": { "completion_tokens": 30, "prompt_tokens": 4, "total_tokens": 34, }, }, "headers": { "content-length": "298", "content-type": "application/json", }, "id": "test-id", "modelId": "test-model", "timestamp": 1970-01-01T00:02:03.000Z, } `); }); it('should support partial usage', async () => { prepareJsonResponse({ usage: { prompt_tokens: 20, total_tokens: 20 }, }); const { usage } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(usage).toMatchInlineSnapshot(` { "cachedInputTokens": undefined, "inputTokens": 20, "outputTokens": undefined, "reasoningTokens": undefined, "totalTokens": 20, } `); }); it('should extract finish reason', async () => { prepareJsonResponse({ finish_reason: 'stop', }); const response = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response.finishReason).toStrictEqual('stop'); }); it('should support unknown finish reason', async () => { prepareJsonResponse({ finish_reason: 'eos', }); const response = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response.finishReason).toStrictEqual('unknown'); }); it('should expose the raw response headers', async () => { prepareJsonResponse({ headers: { 'test-header': 'test-value' }, }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-length': '335', 'content-type': 'application/json', // custom header 'test-header': 'test-value', }); }); it('should pass the model and the messages', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'grok-beta', messages: [{ role: 'user', content: 'Hello' }], }); }); it('should pass settings', async () => { prepareJsonResponse(); await provider('grok-beta').doGenerate({ prompt: TEST_PROMPT, providerOptions: { 'openai-compatible': { user: 'test-user-id', }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'grok-beta', messages: [{ role: 'user', content: 'Hello' }], user: 'test-user-id', }); }); it('should include provider-specific options', async () => { prepareJsonResponse(); await provider('grok-beta').doGenerate({ providerOptions: { 'test-provider': { someCustomOption: 'test-value', }, }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'grok-beta', messages: [{ role: 'user', content: 'Hello' }], someCustomOption: 'test-value', }); }); it('should not include provider-specific options for different provider', async () => { prepareJsonResponse(); await provider('grok-beta').doGenerate({ providerOptions: { notThisProviderName: { someCustomOption: 'test-value', }, }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'grok-beta', messages: [{ role: 'user', content: 'Hello' }], }); }); it('should pass tools and toolChoice', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], toolChoice: { type: 'tool', toolName: 'test-tool', }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'grok-beta', messages: [{ role: 'user', content: 'Hello' }], tools: [ { type: 'function', function: { name: 'test-tool', parameters: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }, ], tool_choice: { type: 'function', function: { name: 'test-tool' }, }, }); }); it('should pass headers', async () => { prepareJsonResponse({ content: '' }); const provider = createOpenAICompatible({ baseURL: 'https://my.api.com/v1/', name: 'test-provider', headers: { Authorization: `Bearer test-api-key`, 'Custom-Provider-Header': 'provider-header-value', }, }); await provider('grok-beta').doGenerate({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should parse tool results', async () => { prepareJsonResponse({ tool_calls: [ { id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', type: 'function', function: { name: 'test-tool', arguments: '{"value":"Spark"}', }, }, ], }); const result = await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], toolChoice: { type: 'tool', toolName: 'test-tool', }, prompt: TEST_PROMPT, }); expect(result.content).toMatchInlineSnapshot(` [ { "input": "{"value":"Spark"}", "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, ] `); }); describe('response format', () => { it('should not send a response_format when response format is text', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = new OpenAICompatibleChatLanguageModel('gpt-4o-2024-08-06', { provider: 'test-provider', url: () => 'https://my.api.com/v1/chat/completions', headers: () => ({}), supportsStructuredOutputs: false, }); await model.doGenerate({ prompt: TEST_PROMPT, responseFormat: { type: 'text' }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o-2024-08-06', messages: [{ role: 'user', content: 'Hello' }], }); }); it('should forward json response format as "json_object" without schema', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = provider('gpt-4o-2024-08-06'); await model.doGenerate({ prompt: TEST_PROMPT, responseFormat: { type: 'json' }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o-2024-08-06', messages: [{ role: 'user', content: 'Hello' }], response_format: { type: 'json_object' }, }); }); it('should forward json response format as "json_object" and omit schema when structuredOutputs are disabled', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = new OpenAICompatibleChatLanguageModel('gpt-4o-2024-08-06', { provider: 'test-provider', url: () => 'https://my.api.com/v1/chat/completions', headers: () => ({}), supportsStructuredOutputs: false, }); const { warnings } = await model.doGenerate({ prompt: TEST_PROMPT, responseFormat: { type: 'json', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o-2024-08-06', messages: [{ role: 'user', content: 'Hello' }], response_format: { type: 'json_object' }, }); expect(warnings).toEqual([ { details: 'JSON response format schema is only supported with structuredOutputs', setting: 'responseFormat', type: 'unsupported-setting', }, ]); }); it('should forward json response format as "json_object" and include schema when structuredOutputs are enabled', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = new OpenAICompatibleChatLanguageModel('gpt-4o-2024-08-06', { provider: 'test-provider', url: () => 'https://my.api.com/v1/chat/completions', headers: () => ({}), supportsStructuredOutputs: true, }); const { warnings } = await model.doGenerate({ prompt: TEST_PROMPT, responseFormat: { type: 'json', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o-2024-08-06', messages: [{ role: 'user', content: 'Hello' }], response_format: { type: 'json_schema', json_schema: { name: 'response', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }, }); expect(warnings).toEqual([]); }); it('should respect the reasoningEffort provider option', async () => { prepareJsonResponse({ content: '{"value":"test"}' }); const model = new OpenAICompatibleChatLanguageModel('gpt-4o-2024-08-06', { provider: 'test-provider', url: () => 'https://my.api.com/v1/chat/completions', headers: () => ({}), }); await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { 'openai-compatible': { reasoningEffort: 'low', }, }, }); const body = await server.calls[0].requestBodyJson; expect(body.reasoning_effort).toBe('low'); }); it('should use json_schema & strict with responseFormat json when structuredOutputs are enabled', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = new OpenAICompatibleChatLanguageModel('gpt-4o-2024-08-06', { provider: 'test-provider', url: () => 'https://my.api.com/v1/chat/completions', headers: () => ({}), supportsStructuredOutputs: true, }); await model.doGenerate({ responseFormat: { type: 'json', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o-2024-08-06', messages: [{ role: 'user', content: 'Hello' }], response_format: { type: 'json_schema', json_schema: { name: 'response', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }, }); }); it('should set name & description with responseFormat json when structuredOutputs are enabled', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = new OpenAICompatibleChatLanguageModel('gpt-4o-2024-08-06', { provider: 'test-provider', url: () => 'https://my.api.com/v1/chat/completions', headers: () => ({}), supportsStructuredOutputs: true, }); await model.doGenerate({ responseFormat: { type: 'json', name: 'test-name', description: 'test description', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o-2024-08-06', messages: [{ role: 'user', content: 'Hello' }], response_format: { type: 'json_schema', json_schema: { name: 'test-name', description: 'test description', schema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }, }); }); it('should allow for undefined schema with responseFormat json when structuredOutputs are enabled', async () => { prepareJsonResponse({ content: '{"value":"Spark"}' }); const model = new OpenAICompatibleChatLanguageModel('gpt-4o-2024-08-06', { provider: 'test-provider', url: () => 'https://my.api.com/v1/chat/completions', headers: () => ({}), supportsStructuredOutputs: true, }); await model.doGenerate({ responseFormat: { type: 'json', name: 'test-name', description: 'test description', }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4o-2024-08-06', messages: [{ role: 'user', content: 'Hello' }], response_format: { type: 'json_object', }, }); }); }); it('should send request body', async () => { prepareJsonResponse({ content: '' }); const { request } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(request).toStrictEqual({ body: '{"model":"grok-beta","messages":[{"role":"user","content":"Hello"}]}', }); }); describe('usage details', () => { it('should extract detailed token usage when available', async () => { prepareJsonResponse({ usage: { prompt_tokens: 20, completion_tokens: 30, total_tokens: 50, prompt_tokens_details: { cached_tokens: 5, }, completion_tokens_details: { reasoning_tokens: 10, accepted_prediction_tokens: 15, rejected_prediction_tokens: 5, }, }, }); const result = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(result.usage).toMatchInlineSnapshot(` { "cachedInputTokens": 5, "inputTokens": 20, "outputTokens": 30, "reasoningTokens": 10, "totalTokens": 50, } `); expect(result.providerMetadata).toMatchInlineSnapshot(` { "test-provider": { "acceptedPredictionTokens": 15, "rejectedPredictionTokens": 5, }, } `); }); it('should handle missing token details', async () => { prepareJsonResponse({ usage: { prompt_tokens: 20, completion_tokens: 30, // No token details provided }, }); const result = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(result.providerMetadata!['test-provider']).toStrictEqual({}); }); it('should handle partial token details', async () => { prepareJsonResponse({ usage: { prompt_tokens: 20, completion_tokens: 30, total_tokens: 50, prompt_tokens_details: { cached_tokens: 5, }, completion_tokens_details: { // Only reasoning tokens provided reasoning_tokens: 10, }, }, }); const result = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(result.usage).toMatchInlineSnapshot(` { "cachedInputTokens": 5, "inputTokens": 20, "outputTokens": 30, "reasoningTokens": 10, "totalTokens": 50, } `); }); }); }); describe('doStream', () => { function prepareStreamResponse({ content = [], finish_reason = 'stop', headers, }: { content?: string[]; finish_reason?: string; headers?: Record<string, string>; }) { server.urls['https://my.api.com/v1/chat/completions'].response = { type: 'stream-chunks', headers, chunks: [ `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1702657020,"model":"grok-beta",` + `"system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}\n\n`, ...content.map(text => { return ( `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1702657020,"model":"grok-beta",` + `"system_fingerprint":null,"choices":[{"index":1,"delta":{"content":"${text}"},"finish_reason":null}]}\n\n` ); }), `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1702657020,"model":"grok-beta",` + `"system_fingerprint":null,"choices":[{"index":0,"delta":{},"finish_reason":"${finish_reason}"}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"grok-beta",` + `"system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"finish_reason":"${finish_reason}"}],` + `"usage":{"queue_time":0.061348671,"prompt_tokens":18,"prompt_time":0.000211569,` + `"completion_tokens":439,"completion_time":0.798181818,"total_tokens":457,"total_time":0.798393387}}\n\n`, 'data: [DONE]\n\n', ], }; } it('should respect the includeUsage option', async () => { prepareStreamResponse({ content: ['Hello', ', ', 'World!'], finish_reason: 'stop', }); const model = new OpenAICompatibleChatLanguageModel('gpt-4o-2024-08-06', { provider: 'test-provider', url: () => 'https://my.api.com/v1/chat/completions', headers: () => ({}), includeUsage: true, }); await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const body = await server.calls[0].requestBodyJson; expect(body.stream).toBe(true); expect(body.stream_options).toStrictEqual({ include_usage: true }); }); it('should stream text deltas', async () => { prepareStreamResponse({ content: ['Hello', ', ', 'World!'], finish_reason: 'stop', }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); // note: space moved to last chunk bc of trimming expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", "modelId": "grok-beta", "timestamp": 2023-12-15T16:17:00.000Z, "type": "response-metadata", }, { "id": "txt-0", "type": "text-start", }, { "delta": "Hello", "id": "txt-0", "type": "text-delta", }, { "delta": ", ", "id": "txt-0", "type": "text-delta", }, { "delta": "World!", "id": "txt-0", "type": "text-delta", }, { "id": "txt-0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "test-provider": {}, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 18, "outputTokens": 439, "reasoningTokens": undefined, "totalTokens": 457, }, }, ] `); }); it('should stream reasoning content before text deltas', async () => { server.urls['https://my.api.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":"", "reasoning_content":"Let me think"},"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":"", "reasoning_content":" about this"},"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":"Here's"},"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":" my response"},"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"grok-beta",` + `"system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],` + `"usage":{"prompt_tokens":18,"completion_tokens":439}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", "modelId": "grok-beta", "timestamp": 2024-03-25T09:06:38.000Z, "type": "response-metadata", }, { "id": "reasoning-0", "type": "reasoning-start", }, { "delta": "Let me think", "id": "reasoning-0", "type": "reasoning-delta", }, { "delta": " about this", "id": "reasoning-0", "type": "reasoning-delta", }, { "id": "txt-0", "type": "text-start", }, { "delta": "Here's", "id": "txt-0", "type": "text-delta", }, { "delta": " my response", "id": "txt-0", "type": "text-delta", }, { "id": "reasoning-0", "type": "reasoning-end", }, { "id": "txt-0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "test-provider": {}, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 18, "outputTokens": 439, "reasoningTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it('should stream tool deltas', async () => { server.urls['https://my.api.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,` + `"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":""}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\""}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"value"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\":\\""}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Spark"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"le"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" Day"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"}"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"grok-beta",` + `"system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],` + `"usage":{"queue_time":0.061348671,"prompt_tokens":18,"prompt_time":0.000211569,` + `"completion_tokens":439,"completion_time":0.798181818,"total_tokens":457,"total_time":0.798393387}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", "modelId": "grok-beta", "timestamp": 2024-03-25T09:06:38.000Z, "type": "response-metadata", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-input-start", }, { "delta": "{"", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "value", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "":"", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "Spark", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "le", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": " Day", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": ""}", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-end", }, { "input": "{"value":"Sparkle Day"}", "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "tool-calls", "providerMetadata": { "test-provider": {}, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 18, "outputTokens": 439, "reasoningTokens": undefined, "totalTokens": 457, }, }, ] `); }); it('should stream tool call deltas when tool call arguments are passed in the first chunk', async () => { server.urls['https://my.api.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,` + `"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":"{\\""}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"va"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"lue"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\":\\""}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Spark"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"le"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" Day"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"}"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"grok-beta",` + `"system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],` + `"usage":{"queue_time":0.061348671,"prompt_tokens":18,"prompt_time":0.000211569,` + `"completion_tokens":439,"completion_time":0.798181818,"total_tokens":457,"total_time":0.798393387}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", "modelId": "grok-beta", "timestamp": 2024-03-25T09:06:38.000Z, "type": "response-metadata", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-input-start", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-input-start", }, { "delta": "va", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "lue", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "":"", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "Spark", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "le", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": " Day", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": ""}", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-end", }, { "input": "{"value":"Sparkle Day"}", "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "tool-calls", "providerMetadata": { "test-provider": {}, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 18, "outputTokens": 439, "reasoningTokens": undefined, "totalTokens": 457, }, }, ] `); }); it('should not duplicate tool calls when there is an additional empty chunk after the tool call has been completed', async () => { server.urls['https://my.api.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":226,"completion_tokens":0}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"id":"chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa",` + `"type":"function","index":0,"function":{"name":"searchGoogle"}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":233,"completion_tokens":7}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":"{\\"query\\": \\""}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":241,"completion_tokens":15}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":"latest"}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":242,"completion_tokens":16}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":" news"}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":243,"completion_tokens":17}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":" on"}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":244,"completion_tokens":18}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":" ai\\"}"}}]},"logprobs":null,"finish_reason":null}],` + `"usage":{"prompt_tokens":226,"total_tokens":245,"completion_tokens":19}}\n\n`, // empty arguments chunk after the tool call has already been finished: `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` + `"function":{"arguments":""}}]},"logprobs":null,"finish_reason":"tool_calls","stop_reason":128008}],` + `"usage":{"prompt_tokens":226,"total_tokens":246,"completion_tokens":20}}\n\n`, `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` + `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[],` + `"usage":{"prompt_tokens":226,"total_tokens":246,"completion_tokens":20}}\n\n`, `data: [DONE]\n\n`, ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'searchGoogle', inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chat-2267f7e2910a4254bac0650ba74cfc1c", "modelId": "meta/llama-3.1-8b-instruct:fp8", "timestamp": 2024-12-02T17:57:21.000Z, "type": "response-metadata", }, { "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "toolName": "searchGoogle", "type": "tool-input-start", }, { "delta": "{"query": "", "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-delta", }, { "delta": "latest", "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-delta", }, { "delta": " news", "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-delta", }, { "delta": " on", "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-delta", }, { "delta": " ai"}", "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-delta", }, { "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "type": "tool-input-end", }, { "input": "{"query": "latest news on ai"}", "toolCallId": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", "toolName": "searchGoogle", "type": "tool-call", }, { "finishReason": "tool-calls", "providerMetadata": { "test-provider": {}, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 226, "outputTokens": 20, "reasoningTokens": undefined, "totalTokens": 246, }, }, ] `); }); it('should stream tool call that is sent in one chunk', async () => { server.urls['https://my.api.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,` + `"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":"{\\"value\\":\\"Sparkle Day\\"}"}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"grok-beta",` + `"system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],` + `"usage":{"queue_time":0.061348671,"prompt_tokens":18,"prompt_time":0.000211569,` + `"completion_tokens":439,"completion_time":0.798181818,"total_tokens":457,"total_time":0.798393387}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", "modelId": "grok-beta", "timestamp": 2024-03-25T09:06:38.000Z, "type": "response-metadata", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-input-start", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-input-start", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-end", }, { "input": "{"value":"Sparkle Day"}", "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "tool-calls", "providerMetadata": { "test-provider": {}, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 18, "outputTokens": 439, "reasoningTokens": undefined, "totalTokens": 457, }, }, ] `); }); it('should stream empty tool call that is sent in one chunk', async () => { server.urls['https://my.api.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` + `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,` + `"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":""}}]},` + `"finish_reason":null}]}\n\n`, `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"grok-beta",` + `"system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],` + `"usage":{"queue_time":0.061348671,"prompt_tokens":18,"prompt_time":0.000211569,` + `"completion_tokens":439,"completion_time":0.798181818,"total_tokens":457,"total_time":0.798393387}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: {}, additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", "modelId": "grok-beta", "timestamp": 2024-03-25T09:06:38.000Z, "type": "response-metadata", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-input-start", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-end", }, { "input": "", "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "tool-calls", "providerMetadata": { "test-provider": {}, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": 18, "outputTokens": 439, "reasoningTokens": undefined, "totalTokens": 457, }, }, ] `); }); it('should handle error stream parts', async () => { server.urls['https://my.api.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"error": {"message": "Incorrect API key provided: as***T7. You can obtain an API key from https://console.api.com.", "code": "Client specified an invalid argument"}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "error": "Incorrect API key provided: as***T7. You can obtain an API key from https://console.api.com.", "type": "error", }, { "finishReason": "error", "providerMetadata": { "test-provider": {}, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": undefined, "outputTokens": undefined, "reasoningTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it.skipIf(isNodeVersion(20))( 'should handle unparsable stream parts', async () => { server.urls['https://my.api.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [`data: {unparsable}\n\n`, 'data: [DONE]\n\n'], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "error": [AI_JSONParseError: JSON parsing failed: Text: {unparsable}. Error message: Expected property name or '}' in JSON at position 1 (line 1 column 2)], "type": "error", }, { "finishReason": "error", "providerMetadata": { "test-provider": {}, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": undefined, "outputTokens": undefined, "reasoningTokens": undefined, "totalTokens": undefined, }, }, ] `); }, ); it('should expose the raw response headers', async () => { prepareStreamResponse({ headers: { 'test-header': 'test-value' }, }); const { response } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', // custom header 'test-header': 'test-value', }); }); it('should pass the messages and the model', async () => { prepareStreamResponse({ content: [] }); await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ stream: true, model: 'grok-beta', messages: [{ role: 'user', content: 'Hello' }], }); }); it('should pass headers', async () => { prepareStreamResponse({ content: [] }); const provider = createOpenAICompatible({ baseURL: 'https://my.api.com/v1', name: 'test-provider', headers: { Authorization: `Bearer test-api-key`, 'Custom-Provider-Header': 'provider-header-value', }, }); await provider('grok-beta').doStream({ prompt: TEST_PROMPT, includeRawChunks: false, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(await server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should include provider-specific options', async () => { prepareStreamResponse({ content: [] }); await provider('grok-beta').doStream({ providerOptions: { 'test-provider': { someCustomOption: 'test-value', }, }, prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ stream: true, model: 'grok-beta', messages: [{ role: 'user', content: 'Hello' }], someCustomOption: 'test-value', }); }); it('should not include provider-specific options for different provider', async () => { prepareStreamResponse({ content: [] }); await provider('grok-beta').doStream({ providerOptions: { notThisProviderName: { someCustomOption: 'test-value', }, }, prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ stream: true, model: 'grok-beta', messages: [{ role: 'user', content: 'Hello' }], }); }); it('should send request body', async () => { prepareStreamResponse({ content: [] }); const { request } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(request).toMatchInlineSnapshot(` { "body": { "frequency_penalty": undefined, "max_tokens": undefined, "messages": [ { "content": "Hello", "role": "user", }, ], "model": "grok-beta", "presence_penalty": undefined, "reasoning_effort": undefined, "response_format": undefined, "seed": undefined, "stop": undefined, "stream": true, "stream_options": undefined, "temperature": undefined, "tool_choice": undefined, "tools": undefined, "top_p": undefined, "user": undefined, }, } `); }); describe('usage details in streaming', () => { it('should extract detailed token usage from stream finish', async () => { server.urls['https://my.api.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chat-id","choices":[{"delta":{"content":"Hello"}}]}\n\n`, `data: {"choices":[{"delta":{},"finish_reason":"stop"}],` + `"usage":{"prompt_tokens":20,"completion_tokens":30,` + `"prompt_tokens_details":{"cached_tokens":5},` + `"completion_tokens_details":{` + `"reasoning_tokens":10,` + `"accepted_prediction_tokens":15,` + `"rejected_prediction_tokens":5}}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const parts = await convertReadableStreamToArray(stream); const finishPart = parts.find(part => part.type === 'finish'); expect(finishPart).toMatchInlineSnapshot(` { "finishReason": "stop", "providerMetadata": { "test-provider": { "acceptedPredictionTokens": 15, "rejectedPredictionTokens": 5, }, }, "type": "finish", "usage": { "cachedInputTokens": 5, "inputTokens": 20, "outputTokens": 30, "reasoningTokens": 10, "totalTokens": undefined, }, } `); }); it('should handle missing token details in stream', async () => { server.urls['https://my.api.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chat-id","choices":[{"delta":{"content":"Hello"}}]}\n\n`, `data: {"choices":[{"delta":{},"finish_reason":"stop"}],` + `"usage":{"prompt_tokens":20,"completion_tokens":30}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const parts = await convertReadableStreamToArray(stream); const finishPart = parts.find(part => part.type === 'finish'); expect(finishPart?.providerMetadata!['test-provider']).toStrictEqual({}); }); it('should handle partial token details in stream', async () => { server.urls['https://my.api.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chat-id","choices":[{"delta":{"content":"Hello"}}]}\n\n`, `data: {"choices":[{"delta":{},"finish_reason":"stop"}],` + `"usage":{"prompt_tokens":20,"completion_tokens":30,` + `"total_tokens":50,` + `"prompt_tokens_details":{"cached_tokens":5},` + `"completion_tokens_details":{"reasoning_tokens":10}}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const parts = await convertReadableStreamToArray(stream); const finishPart = parts.find(part => part.type === 'finish'); expect(finishPart).toMatchInlineSnapshot(` { "finishReason": "stop", "providerMetadata": { "test-provider": {}, }, "type": "finish", "usage": { "cachedInputTokens": 5, "inputTokens": 20, "outputTokens": 30, "reasoningTokens": 10, "totalTokens": 50, }, } `); }); }); }); describe('metadata extraction', () => { const testMetadataExtractor = { extractMetadata: async ({ parsedBody }: { parsedBody: unknown }) => { if ( typeof parsedBody !== 'object' || !parsedBody || !('test_field' in parsedBody) ) { return undefined; } return { 'test-provider': { value: parsedBody.test_field as string, }, }; }, createStreamExtractor: () => { let accumulatedValue: string | undefined; return { processChunk: (chunk: unknown) => { if ( typeof chunk === 'object' && chunk && 'choices' in chunk && Array.isArray(chunk.choices) && chunk.choices[0]?.finish_reason === 'stop' && 'test_field' in chunk ) { accumulatedValue = chunk.test_field as string; } }, buildMetadata: () => accumulatedValue ? { 'test-provider': { value: accumulatedValue, }, } : undefined, }; }, }; it('should process metadata from complete response', async () => { server.urls['https://my.api.com/v1/chat/completions'].response = { type: 'json-value', body: { id: 'chatcmpl-123', object: 'chat.completion', created: 1711115037, model: 'gpt-4', choices: [ { index: 0, message: { role: 'assistant', }, finish_reason: 'stop', }, ], test_field: 'test_value', }, }; const model = new OpenAICompatibleChatLanguageModel('gpt-4', { provider: 'test-provider', url: () => 'https://my.api.com/v1/chat/completions', headers: () => ({}), metadataExtractor: testMetadataExtractor, }); const result = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(result.providerMetadata).toEqual({ 'test-provider': { value: 'test_value', }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4', messages: [{ role: 'user', content: 'Hello' }], }); }); it('should process metadata from streaming response', async () => { server.urls['https://my.api.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ 'data: {"choices":[{"delta":{"content":"Hello"}}]}\n\n', 'data: {"choices":[{"finish_reason":"stop"}],"test_field":"test_value"}\n\n', 'data: [DONE]\n\n', ], }; const model = new OpenAICompatibleChatLanguageModel('gpt-4', { provider: 'test-provider', url: () => 'https://my.api.com/v1/chat/completions', headers: () => ({}), metadataExtractor: testMetadataExtractor, }); const result = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const parts = await convertReadableStreamToArray(result.stream); const finishPart = parts.find(part => part.type === 'finish'); expect(finishPart?.providerMetadata).toEqual({ 'test-provider': { value: 'test_value', }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'gpt-4', messages: [{ role: 'user', content: 'Hello' }], stream: true, }); }); }); describe('raw chunks', () => { it('should include raw chunks when includeRawChunks is true', async () => { server.urls['https://my.api.com/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"chat-id","choices":[{"delta":{"content":"Hello"}}]}\n\n`, `data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: true, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "rawValue": { "choices": [ { "delta": { "content": "Hello", }, }, ], "id": "chat-id", }, "type": "raw", }, { "id": "chat-id", "modelId": undefined, "timestamp": undefined, "type": "response-metadata", }, { "id": "txt-0", "type": "text-start", }, { "delta": "Hello", "id": "txt-0", "type": "text-delta", }, { "rawValue": { "choices": [ { "delta": {}, "finish_reason": "stop", }, ], }, "type": "raw", }, { "id": "txt-0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "test-provider": {}, }, "type": "finish", "usage": { "cachedInputTokens": undefined, "inputTokens": undefined, "outputTokens": undefined, "reasoningTokens": undefined, "totalTokens": undefined, }, }, ] `); }); }); --- File: /ai/packages/openai-compatible/src/openai-compatible-chat-language-model.ts --- import { APICallError, InvalidResponseDataError, LanguageModelV2, LanguageModelV2CallWarning, LanguageModelV2Content, LanguageModelV2FinishReason, LanguageModelV2StreamPart, SharedV2ProviderMetadata, } from '@ai-sdk/provider'; import { combineHeaders, createEventSourceResponseHandler, createJsonErrorResponseHandler, createJsonResponseHandler, FetchFunction, generateId, isParsableJson, parseProviderOptions, ParseResult, postJsonToApi, ResponseHandler, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { convertToOpenAICompatibleChatMessages } from './convert-to-openai-compatible-chat-messages'; import { getResponseMetadata } from './get-response-metadata'; import { mapOpenAICompatibleFinishReason } from './map-openai-compatible-finish-reason'; import { OpenAICompatibleChatModelId, openaiCompatibleProviderOptions, } from './openai-compatible-chat-options'; import { defaultOpenAICompatibleErrorStructure, ProviderErrorStructure, } from './openai-compatible-error'; import { MetadataExtractor } from './openai-compatible-metadata-extractor'; import { prepareTools } from './openai-compatible-prepare-tools'; export type OpenAICompatibleChatConfig = { provider: string; headers: () => Record<string, string | undefined>; url: (options: { modelId: string; path: string }) => string; fetch?: FetchFunction; includeUsage?: boolean; errorStructure?: ProviderErrorStructure<any>; metadataExtractor?: MetadataExtractor; /** * Whether the model supports structured outputs. */ supportsStructuredOutputs?: boolean; /** * The supported URLs for the model. */ supportedUrls?: () => LanguageModelV2['supportedUrls']; }; export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { readonly specificationVersion = 'v2'; readonly supportsStructuredOutputs: boolean; readonly modelId: OpenAICompatibleChatModelId; private readonly config: OpenAICompatibleChatConfig; private readonly failedResponseHandler: ResponseHandler<APICallError>; private readonly chunkSchema; // type inferred via constructor constructor( modelId: OpenAICompatibleChatModelId, config: OpenAICompatibleChatConfig, ) { this.modelId = modelId; this.config = config; // initialize error handling: const errorStructure = config.errorStructure ?? defaultOpenAICompatibleErrorStructure; this.chunkSchema = createOpenAICompatibleChatChunkSchema( errorStructure.errorSchema, ); this.failedResponseHandler = createJsonErrorResponseHandler(errorStructure); this.supportsStructuredOutputs = config.supportsStructuredOutputs ?? false; } get provider(): string { return this.config.provider; } private get providerOptionsName(): string { return this.config.provider.split('.')[0].trim(); } get supportedUrls() { return this.config.supportedUrls?.() ?? {}; } private async getArgs({ prompt, maxOutputTokens, temperature, topP, topK, frequencyPenalty, presencePenalty, providerOptions, stopSequences, responseFormat, seed, toolChoice, tools, }: Parameters<LanguageModelV2['doGenerate']>[0]) { const warnings: LanguageModelV2CallWarning[] = []; // Parse provider options const compatibleOptions = Object.assign( (await parseProviderOptions({ provider: 'openai-compatible', providerOptions, schema: openaiCompatibleProviderOptions, })) ?? {}, (await parseProviderOptions({ provider: this.providerOptionsName, providerOptions, schema: openaiCompatibleProviderOptions, })) ?? {}, ); if (topK != null) { warnings.push({ type: 'unsupported-setting', setting: 'topK' }); } if ( responseFormat?.type === 'json' && responseFormat.schema != null && !this.supportsStructuredOutputs ) { warnings.push({ type: 'unsupported-setting', setting: 'responseFormat', details: 'JSON response format schema is only supported with structuredOutputs', }); } const { tools: openaiTools, toolChoice: openaiToolChoice, toolWarnings, } = prepareTools({ tools, toolChoice, }); return { args: { // model id: model: this.modelId, // model specific settings: user: compatibleOptions.user, // standardized settings: max_tokens: maxOutputTokens, temperature, top_p: topP, frequency_penalty: frequencyPenalty, presence_penalty: presencePenalty, response_format: responseFormat?.type === 'json' ? this.supportsStructuredOutputs === true && responseFormat.schema != null ? { type: 'json_schema', json_schema: { schema: responseFormat.schema, name: responseFormat.name ?? 'response', description: responseFormat.description, }, } : { type: 'json_object' } : undefined, stop: stopSequences, seed, ...providerOptions?.[this.providerOptionsName], reasoning_effort: compatibleOptions.reasoningEffort, // messages: messages: convertToOpenAICompatibleChatMessages(prompt), // tools: tools: openaiTools, tool_choice: openaiToolChoice, }, warnings: [...warnings, ...toolWarnings], }; } async doGenerate( options: Parameters<LanguageModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>> { const { args, warnings } = await this.getArgs({ ...options }); const body = JSON.stringify(args); const { responseHeaders, value: responseBody, rawValue: rawResponse, } = await postJsonToApi({ url: this.config.url({ path: '/chat/completions', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body: args, failedResponseHandler: this.failedResponseHandler, successfulResponseHandler: createJsonResponseHandler( OpenAICompatibleChatResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const choice = responseBody.choices[0]; const content: Array<LanguageModelV2Content> = []; // text content: const text = choice.message.content; if (text != null && text.length > 0) { content.push({ type: 'text', text }); } // reasoning content: const reasoning = choice.message.reasoning_content; if (reasoning != null && reasoning.length > 0) { content.push({ type: 'reasoning', text: reasoning, }); } // tool calls: if (choice.message.tool_calls != null) { for (const toolCall of choice.message.tool_calls) { content.push({ type: 'tool-call', toolCallId: toolCall.id ?? generateId(), toolName: toolCall.function.name, input: toolCall.function.arguments!, }); } } // provider metadata: const providerMetadata: SharedV2ProviderMetadata = { [this.providerOptionsName]: {}, ...(await this.config.metadataExtractor?.extractMetadata?.({ parsedBody: rawResponse, })), }; const completionTokenDetails = responseBody.usage?.completion_tokens_details; if (completionTokenDetails?.accepted_prediction_tokens != null) { providerMetadata[this.providerOptionsName].acceptedPredictionTokens = completionTokenDetails?.accepted_prediction_tokens; } if (completionTokenDetails?.rejected_prediction_tokens != null) { providerMetadata[this.providerOptionsName].rejectedPredictionTokens = completionTokenDetails?.rejected_prediction_tokens; } return { content, finishReason: mapOpenAICompatibleFinishReason(choice.finish_reason), usage: { inputTokens: responseBody.usage?.prompt_tokens ?? undefined, outputTokens: responseBody.usage?.completion_tokens ?? undefined, totalTokens: responseBody.usage?.total_tokens ?? undefined, reasoningTokens: responseBody.usage?.completion_tokens_details?.reasoning_tokens ?? undefined, cachedInputTokens: responseBody.usage?.prompt_tokens_details?.cached_tokens ?? undefined, }, providerMetadata, request: { body }, response: { ...getResponseMetadata(responseBody), headers: responseHeaders, body: rawResponse, }, warnings, }; } async doStream( options: Parameters<LanguageModelV2['doStream']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>> { const { args, warnings } = await this.getArgs({ ...options }); const body = { ...args, stream: true, // only include stream_options when in strict compatibility mode: stream_options: this.config.includeUsage ? { include_usage: true } : undefined, }; const metadataExtractor = this.config.metadataExtractor?.createStreamExtractor(); const { responseHeaders, value: response } = await postJsonToApi({ url: this.config.url({ path: '/chat/completions', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body, failedResponseHandler: this.failedResponseHandler, successfulResponseHandler: createEventSourceResponseHandler( this.chunkSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const toolCalls: Array<{ id: string; type: 'function'; function: { name: string; arguments: string; }; hasFinished: boolean; }> = []; let finishReason: LanguageModelV2FinishReason = 'unknown'; const usage: { completionTokens: number | undefined; completionTokensDetails: { reasoningTokens: number | undefined; acceptedPredictionTokens: number | undefined; rejectedPredictionTokens: number | undefined; }; promptTokens: number | undefined; promptTokensDetails: { cachedTokens: number | undefined; }; totalTokens: number | undefined; } = { completionTokens: undefined, completionTokensDetails: { reasoningTokens: undefined, acceptedPredictionTokens: undefined, rejectedPredictionTokens: undefined, }, promptTokens: undefined, promptTokensDetails: { cachedTokens: undefined, }, totalTokens: undefined, }; let isFirstChunk = true; const providerOptionsName = this.providerOptionsName; let isActiveReasoning = false; let isActiveText = false; return { stream: response.pipeThrough( new TransformStream< ParseResult<z.infer<typeof this.chunkSchema>>, LanguageModelV2StreamPart >({ start(controller) { controller.enqueue({ type: 'stream-start', warnings }); }, // TODO we lost type safety on Chunk, most likely due to the error schema. MUST FIX transform(chunk, controller) { // Emit raw chunk if requested (before anything else) if (options.includeRawChunks) { controller.enqueue({ type: 'raw', rawValue: chunk.rawValue }); } // handle failed chunk parsing / validation: if (!chunk.success) { finishReason = 'error'; controller.enqueue({ type: 'error', error: chunk.error }); return; } const value = chunk.value; metadataExtractor?.processChunk(chunk.rawValue); // handle error chunks: if ('error' in value) { finishReason = 'error'; controller.enqueue({ type: 'error', error: value.error.message }); return; } if (isFirstChunk) { isFirstChunk = false; controller.enqueue({ type: 'response-metadata', ...getResponseMetadata(value), }); } if (value.usage != null) { const { prompt_tokens, completion_tokens, total_tokens, prompt_tokens_details, completion_tokens_details, } = value.usage; usage.promptTokens = prompt_tokens ?? undefined; usage.completionTokens = completion_tokens ?? undefined; usage.totalTokens = total_tokens ?? undefined; if (completion_tokens_details?.reasoning_tokens != null) { usage.completionTokensDetails.reasoningTokens = completion_tokens_details?.reasoning_tokens; } if ( completion_tokens_details?.accepted_prediction_tokens != null ) { usage.completionTokensDetails.acceptedPredictionTokens = completion_tokens_details?.accepted_prediction_tokens; } if ( completion_tokens_details?.rejected_prediction_tokens != null ) { usage.completionTokensDetails.rejectedPredictionTokens = completion_tokens_details?.rejected_prediction_tokens; } if (prompt_tokens_details?.cached_tokens != null) { usage.promptTokensDetails.cachedTokens = prompt_tokens_details?.cached_tokens; } } const choice = value.choices[0]; if (choice?.finish_reason != null) { finishReason = mapOpenAICompatibleFinishReason( choice.finish_reason, ); } if (choice?.delta == null) { return; } const delta = choice.delta; // enqueue reasoning before text deltas: if (delta.reasoning_content) { if (!isActiveReasoning) { controller.enqueue({ type: 'reasoning-start', id: 'reasoning-0', }); isActiveReasoning = true; } controller.enqueue({ type: 'reasoning-delta', id: 'reasoning-0', delta: delta.reasoning_content, }); } if (delta.content) { if (!isActiveText) { controller.enqueue({ type: 'text-start', id: 'txt-0' }); isActiveText = true; } controller.enqueue({ type: 'text-delta', id: 'txt-0', delta: delta.content, }); } if (delta.tool_calls != null) { for (const toolCallDelta of delta.tool_calls) { const index = toolCallDelta.index; if (toolCalls[index] == null) { if (toolCallDelta.id == null) { throw new InvalidResponseDataError({ data: toolCallDelta, message: `Expected 'id' to be a string.`, }); } if (toolCallDelta.function?.name == null) { throw new InvalidResponseDataError({ data: toolCallDelta, message: `Expected 'function.name' to be a string.`, }); } controller.enqueue({ type: 'tool-input-start', id: toolCallDelta.id, toolName: toolCallDelta.function.name, }); toolCalls[index] = { id: toolCallDelta.id, type: 'function', function: { name: toolCallDelta.function.name, arguments: toolCallDelta.function.arguments ?? '', }, hasFinished: false, }; const toolCall = toolCalls[index]; if ( toolCall.function?.name != null && toolCall.function?.arguments != null ) { // send delta if the argument text has already started: if (toolCall.function.arguments.length > 0) { controller.enqueue({ type: 'tool-input-start', id: toolCall.id, toolName: toolCall.function.name, }); } // check if tool call is complete // (some providers send the full tool call in one chunk): if (isParsableJson(toolCall.function.arguments)) { controller.enqueue({ type: 'tool-input-end', id: toolCall.id, }); controller.enqueue({ type: 'tool-call', toolCallId: toolCall.id ?? generateId(), toolName: toolCall.function.name, input: toolCall.function.arguments, }); toolCall.hasFinished = true; } } continue; } // existing tool call, merge if not finished const toolCall = toolCalls[index]; if (toolCall.hasFinished) { continue; } if (toolCallDelta.function?.arguments != null) { toolCall.function!.arguments += toolCallDelta.function?.arguments ?? ''; } // send delta controller.enqueue({ type: 'tool-input-delta', id: toolCall.id, delta: toolCallDelta.function.arguments ?? '', }); // check if tool call is complete if ( toolCall.function?.name != null && toolCall.function?.arguments != null && isParsableJson(toolCall.function.arguments) ) { controller.enqueue({ type: 'tool-input-end', id: toolCall.id, }); controller.enqueue({ type: 'tool-call', toolCallId: toolCall.id ?? generateId(), toolName: toolCall.function.name, input: toolCall.function.arguments, }); toolCall.hasFinished = true; } } } }, flush(controller) { if (isActiveReasoning) { controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' }); } if (isActiveText) { controller.enqueue({ type: 'text-end', id: 'txt-0' }); } // go through all tool calls and send the ones that are not finished for (const toolCall of toolCalls.filter( toolCall => !toolCall.hasFinished, )) { controller.enqueue({ type: 'tool-input-end', id: toolCall.id, }); controller.enqueue({ type: 'tool-call', toolCallId: toolCall.id ?? generateId(), toolName: toolCall.function.name, input: toolCall.function.arguments, }); } const providerMetadata: SharedV2ProviderMetadata = { [providerOptionsName]: {}, ...metadataExtractor?.buildMetadata(), }; if ( usage.completionTokensDetails.acceptedPredictionTokens != null ) { providerMetadata[providerOptionsName].acceptedPredictionTokens = usage.completionTokensDetails.acceptedPredictionTokens; } if ( usage.completionTokensDetails.rejectedPredictionTokens != null ) { providerMetadata[providerOptionsName].rejectedPredictionTokens = usage.completionTokensDetails.rejectedPredictionTokens; } controller.enqueue({ type: 'finish', finishReason, usage: { inputTokens: usage.promptTokens ?? undefined, outputTokens: usage.completionTokens ?? undefined, totalTokens: usage.totalTokens ?? undefined, reasoningTokens: usage.completionTokensDetails.reasoningTokens ?? undefined, cachedInputTokens: usage.promptTokensDetails.cachedTokens ?? undefined, }, providerMetadata, }); }, }), ), request: { body }, response: { headers: responseHeaders }, }; } } const openaiCompatibleTokenUsageSchema = z .object({ prompt_tokens: z.number().nullish(), completion_tokens: z.number().nullish(), total_tokens: z.number().nullish(), prompt_tokens_details: z .object({ cached_tokens: z.number().nullish(), }) .nullish(), completion_tokens_details: z .object({ reasoning_tokens: z.number().nullish(), accepted_prediction_tokens: z.number().nullish(), rejected_prediction_tokens: z.number().nullish(), }) .nullish(), }) .nullish(); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const OpenAICompatibleChatResponseSchema = z.object({ id: z.string().nullish(), created: z.number().nullish(), model: z.string().nullish(), choices: z.array( z.object({ message: z.object({ role: z.literal('assistant').nullish(), content: z.string().nullish(), reasoning_content: z.string().nullish(), tool_calls: z .array( z.object({ id: z.string().nullish(), function: z.object({ name: z.string(), arguments: z.string(), }), }), ) .nullish(), }), finish_reason: z.string().nullish(), }), ), usage: openaiCompatibleTokenUsageSchema, }); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const createOpenAICompatibleChatChunkSchema = < ERROR_SCHEMA extends z.core.$ZodType, >( errorSchema: ERROR_SCHEMA, ) => z.union([ z.object({ id: z.string().nullish(), created: z.number().nullish(), model: z.string().nullish(), choices: z.array( z.object({ delta: z .object({ role: z.enum(['assistant']).nullish(), content: z.string().nullish(), reasoning_content: z.string().nullish(), tool_calls: z .array( z.object({ index: z.number(), id: z.string().nullish(), function: z.object({ name: z.string().nullish(), arguments: z.string().nullish(), }), }), ) .nullish(), }) .nullish(), finish_reason: z.string().nullish(), }), ), usage: openaiCompatibleTokenUsageSchema, }), errorSchema, ]); --- File: /ai/packages/openai-compatible/src/openai-compatible-chat-options.ts --- import { z } from 'zod/v4'; export type OpenAICompatibleChatModelId = string; export const openaiCompatibleProviderOptions = z.object({ /** * A unique identifier representing your end-user, which can help the provider to * monitor and detect abuse. */ user: z.string().optional(), /** * Reasoning effort for reasoning models. Defaults to `medium`. */ reasoningEffort: z.string().optional(), }); export type OpenAICompatibleProviderOptions = z.infer< typeof openaiCompatibleProviderOptions >; --- File: /ai/packages/openai-compatible/src/openai-compatible-completion-language-model.test.ts --- import { LanguageModelV2Prompt } from '@ai-sdk/provider'; import { convertReadableStreamToArray, createTestServer, isNodeVersion, } from '@ai-sdk/provider-utils/test'; import { OpenAICompatibleChatLanguageModel } from './openai-compatible-chat-language-model'; import { createOpenAICompatible } from './openai-compatible-provider'; const TEST_PROMPT: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; const provider = createOpenAICompatible({ baseURL: 'https://my.api.com/v1/', name: 'test-provider', headers: { Authorization: `Bearer test-api-key`, }, }); const model = provider.completionModel('gpt-3.5-turbo-instruct'); const server = createTestServer({ 'https://my.api.com/v1/completions': {}, }); describe('config', () => { it('should extract base name from provider string', () => { const model = new OpenAICompatibleChatLanguageModel('gpt-4', { provider: 'anthropic.beta', url: () => '', headers: () => ({}), }); expect(model['providerOptionsName']).toBe('anthropic'); }); it('should handle provider without dot notation', () => { const model = new OpenAICompatibleChatLanguageModel('gpt-4', { provider: 'openai', url: () => '', headers: () => ({}), }); expect(model['providerOptionsName']).toBe('openai'); }); it('should return empty for empty provider', () => { const model = new OpenAICompatibleChatLanguageModel( 'gpt-4', { provider: '', url: () => '', headers: () => ({}), }, ); expect(model['providerOptionsName']).toBe(''); }); }); describe('doGenerate', () => { function prepareJsonResponse({ content = '', usage = { prompt_tokens: 4, total_tokens: 34, completion_tokens: 30, }, finish_reason = 'stop', id = 'cmpl-96cAM1v77r4jXa4qb2NSmRREV5oWB', created = 1711363706, model = 'gpt-3.5-turbo-instruct', headers, }: { content?: string; usage?: { prompt_tokens: number; total_tokens: number; completion_tokens: number; }; finish_reason?: string; id?: string; created?: number; model?: string; headers?: Record<string, string>; }) { server.urls['https://my.api.com/v1/completions'].response = { type: 'json-value', headers, body: { id, object: 'text_completion', created, model, choices: [ { text: content, index: 0, finish_reason, }, ], usage, }, }; } it('should extract text response', async () => { prepareJsonResponse({ content: 'Hello, World!' }); const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "text": "Hello, World!", "type": "text", }, ] `); }); it('should extract usage', async () => { prepareJsonResponse({ usage: { prompt_tokens: 20, total_tokens: 25, completion_tokens: 5 }, }); const { usage } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(usage).toMatchInlineSnapshot(` { "inputTokens": 20, "outputTokens": 5, "totalTokens": 25, } `); }); it('should send request body', async () => { prepareJsonResponse({}); const { request } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(request).toMatchInlineSnapshot(` { "body": { "echo": undefined, "frequency_penalty": undefined, "logit_bias": undefined, "max_tokens": undefined, "model": "gpt-3.5-turbo-instruct", "presence_penalty": undefined, "prompt": "user: Hello assistant: ", "seed": undefined, "stop": [ " user:", ], "suffix": undefined, "temperature": undefined, "top_p": undefined, "user": undefined, }, } `); }); it('should send additional response information', async () => { prepareJsonResponse({ id: 'test-id', created: 123, model: 'test-model', }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response).toMatchInlineSnapshot(` { "body": { "choices": [ { "finish_reason": "stop", "index": 0, "text": "", }, ], "created": 123, "id": "test-id", "model": "test-model", "object": "text_completion", "usage": { "completion_tokens": 30, "prompt_tokens": 4, "total_tokens": 34, }, }, "headers": { "content-length": "204", "content-type": "application/json", }, "id": "test-id", "modelId": "test-model", "timestamp": 1970-01-01T00:02:03.000Z, } `); }); it('should extract finish reason', async () => { prepareJsonResponse({ finish_reason: 'stop', }); const { finishReason } = await provider .completionModel('gpt-3.5-turbo-instruct') .doGenerate({ prompt: TEST_PROMPT, }); expect(finishReason).toStrictEqual('stop'); }); it('should support unknown finish reason', async () => { prepareJsonResponse({ finish_reason: 'eos', }); const { finishReason } = await provider .completionModel('gpt-3.5-turbo-instruct') .doGenerate({ prompt: TEST_PROMPT, }); expect(finishReason).toStrictEqual('unknown'); }); it('should expose the raw response headers', async () => { prepareJsonResponse({ headers: { 'test-header': 'test-value' }, }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-length': '250', 'content-type': 'application/json', // custom header 'test-header': 'test-value', }); }); it('should pass the model and the prompt', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "model": "gpt-3.5-turbo-instruct", "prompt": "user: Hello assistant: ", "stop": [ " user:", ], } `); }); it('should pass headers', async () => { prepareJsonResponse({ content: '' }); const provider = createOpenAICompatible({ baseURL: 'https://my.api.com/v1/', name: 'test-provider', headers: { Authorization: `Bearer test-api-key`, 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.completionModel('gpt-3.5-turbo-instruct').doGenerate({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should include provider-specific options', async () => { prepareJsonResponse({ content: '' }); await provider.completionModel('gpt-3.5-turbo-instruct').doGenerate({ prompt: TEST_PROMPT, providerOptions: { 'test-provider': { someCustomOption: 'test-value', }, }, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "model": "gpt-3.5-turbo-instruct", "prompt": "user: Hello assistant: ", "someCustomOption": "test-value", "stop": [ " user:", ], } `); }); it('should not include provider-specific options for different provider', async () => { prepareJsonResponse({ content: '' }); await provider.completionModel('gpt-3.5-turbo-instruct').doGenerate({ prompt: TEST_PROMPT, providerOptions: { notThisProviderName: { someCustomOption: 'test-value', }, }, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "model": "gpt-3.5-turbo-instruct", "prompt": "user: Hello assistant: ", "stop": [ " user:", ], } `); }); }); describe('doStream', () => { function prepareEmptyStreamResponse(headers?: Record<string, string>) { server.urls['https://my.api.com/v1/completions'].response = { type: 'stream-chunks', headers, chunks: [ `data: {"id":"cmpl-96c3yLQE1TtZCd6n6OILVmzev8M8H","object":"text_completion","created":1711363310,"model":"gpt-3.5-turbo-instruct","choices":[{"text":"","index":0,"logprobs":null,"finish_reason":"stop"}]}\n\n`, `data: {"id":"cmpl-96c3yLQE1TtZCd6n6OILVmzev8M8H","object":"text_completion","created":1711363310,"model":"gpt-3.5-turbo-instruct","usage":{"prompt_tokens":10,"completion_tokens":0,"total_tokens":10},"choices":[]}\n\n`, 'data: [DONE]\n\n', ], }; } it('should stream text deltas', async () => { server.urls['https://my.api.com/v1/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"cmpl-96c64EdfhOw8pjFFgVpLuT8k2MtdT","object":"text_completion","created":1711363440,"model":"gpt-3.5-turbo-instruct","choices":[{"text":"Hello","index":0,"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"cmpl-96c64EdfhOw8pjFFgVpLuT8k2MtdT","object":"text_completion","created":1711363440,"model":"gpt-3.5-turbo-instruct","choices":[{"text":",","index":0,"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"cmpl-96c64EdfhOw8pjFFgVpLuT8k2MtdT","object":"text_completion","created":1711363440,"model":"gpt-3.5-turbo-instruct","choices":[{"text":" World!","index":0,"logprobs":null,"finish_reason":null}]}\n\n`, `data: {"id":"cmpl-96c3yLQE1TtZCd6n6OILVmzev8M8H","object":"text_completion","created":1711363310,"model":"gpt-3.5-turbo-instruct","choices":[{"text":"","index":0,"logprobs":null,"finish_reason":"stop"}]}\n\n`, `data: {"id":"cmpl-96c3yLQE1TtZCd6n6OILVmzev8M8H","object":"text_completion","created":1711363310,"model":"gpt-3.5-turbo-instruct","usage":{"prompt_tokens":10,"completion_tokens":362,"total_tokens":372},"choices":[]}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "cmpl-96c64EdfhOw8pjFFgVpLuT8k2MtdT", "modelId": "gpt-3.5-turbo-instruct", "timestamp": 2024-03-25T10:44:00.000Z, "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "delta": ",", "id": "0", "type": "text-delta", }, { "delta": " World!", "id": "0", "type": "text-delta", }, { "delta": "", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 10, "outputTokens": 362, "totalTokens": 372, }, }, ] `); }); it('should handle error stream parts', async () => { server.urls['https://my.api.com/v1/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"error":{"message":"The server had an error processing your request. Sorry about that! You can retry your request, or contact us through our help center at help.openai.com if you keep seeing this error.","type":"server_error","param":null,"code":null}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "error": { "code": null, "message": "The server had an error processing your request. Sorry about that! You can retry your request, or contact us through our help center at help.openai.com if you keep seeing this error.", "param": null, "type": "server_error", }, "type": "error", }, { "finishReason": "error", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); it.skipIf(isNodeVersion(20))( 'should handle unparsable stream parts', async () => { server.urls['https://my.api.com/v1/completions'].response = { type: 'stream-chunks', chunks: [`data: {unparsable}\n\n`, 'data: [DONE]\n\n'], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "error": [AI_JSONParseError: JSON parsing failed: Text: {unparsable}. Error message: Expected property name or '}' in JSON at position 1 (line 1 column 2)], "type": "error", }, { "finishReason": "error", "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }, ); it('should send request body', async () => { prepareEmptyStreamResponse(); const { request } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(request).toMatchInlineSnapshot(` { "body": { "echo": undefined, "frequency_penalty": undefined, "logit_bias": undefined, "max_tokens": undefined, "model": "gpt-3.5-turbo-instruct", "presence_penalty": undefined, "prompt": "user: Hello assistant: ", "seed": undefined, "stop": [ " user:", ], "stream": true, "stream_options": undefined, "suffix": undefined, "temperature": undefined, "top_p": undefined, "user": undefined, }, } `); }); it('should expose the raw response headers', async () => { prepareEmptyStreamResponse({ 'test-header': 'test-value' }); const { response } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', // custom header 'test-header': 'test-value', }); }); it('should pass the model and the prompt', async () => { prepareEmptyStreamResponse(); await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "model": "gpt-3.5-turbo-instruct", "prompt": "user: Hello assistant: ", "stop": [ " user:", ], "stream": true, } `); }); it('should pass headers', async () => { prepareEmptyStreamResponse(); const provider = createOpenAICompatible({ baseURL: 'https://my.api.com/v1/', name: 'test-provider', headers: { Authorization: `Bearer test-api-key`, 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.completionModel('gpt-3.5-turbo-instruct').doStream({ prompt: TEST_PROMPT, includeRawChunks: false, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should include provider-specific options', async () => { prepareEmptyStreamResponse(); await model.doStream({ providerOptions: { 'test-provider': { someCustomOption: 'test-value', }, }, prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "model": "gpt-3.5-turbo-instruct", "prompt": "user: Hello assistant: ", "someCustomOption": "test-value", "stop": [ " user:", ], "stream": true, } `); }); it('should not include provider-specific options for different provider', async () => { prepareEmptyStreamResponse(); await model.doStream({ providerOptions: { notThisProviderName: { someCustomOption: 'test-value', }, }, prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "model": "gpt-3.5-turbo-instruct", "prompt": "user: Hello assistant: ", "stop": [ " user:", ], "stream": true, } `); }); }); --- File: /ai/packages/openai-compatible/src/openai-compatible-completion-language-model.ts --- import { APICallError, LanguageModelV2, LanguageModelV2CallWarning, LanguageModelV2Content, LanguageModelV2FinishReason, LanguageModelV2StreamPart, LanguageModelV2Usage, } from '@ai-sdk/provider'; import { combineHeaders, createEventSourceResponseHandler, createJsonErrorResponseHandler, createJsonResponseHandler, FetchFunction, parseProviderOptions, ParseResult, postJsonToApi, ResponseHandler, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { convertToOpenAICompatibleCompletionPrompt } from './convert-to-openai-compatible-completion-prompt'; import { getResponseMetadata } from './get-response-metadata'; import { mapOpenAICompatibleFinishReason } from './map-openai-compatible-finish-reason'; import { OpenAICompatibleCompletionModelId, openaiCompatibleCompletionProviderOptions, } from './openai-compatible-completion-options'; import { defaultOpenAICompatibleErrorStructure, ProviderErrorStructure, } from './openai-compatible-error'; type OpenAICompatibleCompletionConfig = { provider: string; includeUsage?: boolean; headers: () => Record<string, string | undefined>; url: (options: { modelId: string; path: string }) => string; fetch?: FetchFunction; errorStructure?: ProviderErrorStructure<any>; /** * The supported URLs for the model. */ supportedUrls?: () => LanguageModelV2['supportedUrls']; }; export class OpenAICompatibleCompletionLanguageModel implements LanguageModelV2 { readonly specificationVersion = 'v2'; readonly modelId: OpenAICompatibleCompletionModelId; private readonly config: OpenAICompatibleCompletionConfig; private readonly failedResponseHandler: ResponseHandler<APICallError>; private readonly chunkSchema; // type inferred via constructor constructor( modelId: OpenAICompatibleCompletionModelId, config: OpenAICompatibleCompletionConfig, ) { this.modelId = modelId; this.config = config; // initialize error handling: const errorStructure = config.errorStructure ?? defaultOpenAICompatibleErrorStructure; this.chunkSchema = createOpenAICompatibleCompletionChunkSchema( errorStructure.errorSchema, ); this.failedResponseHandler = createJsonErrorResponseHandler(errorStructure); } get provider(): string { return this.config.provider; } private get providerOptionsName(): string { return this.config.provider.split('.')[0].trim(); } get supportedUrls() { return this.config.supportedUrls?.() ?? {}; } private async getArgs({ prompt, maxOutputTokens, temperature, topP, topK, frequencyPenalty, presencePenalty, stopSequences: userStopSequences, responseFormat, seed, providerOptions, tools, toolChoice, }: Parameters<LanguageModelV2['doGenerate']>[0]) { const warnings: LanguageModelV2CallWarning[] = []; // Parse provider options const completionOptions = (await parseProviderOptions({ provider: this.providerOptionsName, providerOptions, schema: openaiCompatibleCompletionProviderOptions, })) ?? {}; if (topK != null) { warnings.push({ type: 'unsupported-setting', setting: 'topK' }); } if (tools?.length) { warnings.push({ type: 'unsupported-setting', setting: 'tools' }); } if (toolChoice != null) { warnings.push({ type: 'unsupported-setting', setting: 'toolChoice' }); } if (responseFormat != null && responseFormat.type !== 'text') { warnings.push({ type: 'unsupported-setting', setting: 'responseFormat', details: 'JSON response format is not supported.', }); } const { prompt: completionPrompt, stopSequences } = convertToOpenAICompatibleCompletionPrompt({ prompt }); const stop = [...(stopSequences ?? []), ...(userStopSequences ?? [])]; return { args: { // model id: model: this.modelId, // model specific settings: echo: completionOptions.echo, logit_bias: completionOptions.logitBias, suffix: completionOptions.suffix, user: completionOptions.user, // standardized settings: max_tokens: maxOutputTokens, temperature, top_p: topP, frequency_penalty: frequencyPenalty, presence_penalty: presencePenalty, seed, ...providerOptions?.[this.providerOptionsName], // prompt: prompt: completionPrompt, // stop sequences: stop: stop.length > 0 ? stop : undefined, }, warnings, }; } async doGenerate( options: Parameters<LanguageModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>> { const { args, warnings } = await this.getArgs(options); const { responseHeaders, value: response, rawValue: rawResponse, } = await postJsonToApi({ url: this.config.url({ path: '/completions', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body: args, failedResponseHandler: this.failedResponseHandler, successfulResponseHandler: createJsonResponseHandler( openaiCompatibleCompletionResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const choice = response.choices[0]; const content: Array<LanguageModelV2Content> = []; // text content: if (choice.text != null && choice.text.length > 0) { content.push({ type: 'text', text: choice.text }); } return { content, usage: { inputTokens: response.usage?.prompt_tokens ?? undefined, outputTokens: response.usage?.completion_tokens ?? undefined, totalTokens: response.usage?.total_tokens ?? undefined, }, finishReason: mapOpenAICompatibleFinishReason(choice.finish_reason), request: { body: args }, response: { ...getResponseMetadata(response), headers: responseHeaders, body: rawResponse, }, warnings, }; } async doStream( options: Parameters<LanguageModelV2['doStream']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>> { const { args, warnings } = await this.getArgs(options); const body = { ...args, stream: true, // only include stream_options when in strict compatibility mode: stream_options: this.config.includeUsage ? { include_usage: true } : undefined, }; const { responseHeaders, value: response } = await postJsonToApi({ url: this.config.url({ path: '/completions', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body, failedResponseHandler: this.failedResponseHandler, successfulResponseHandler: createEventSourceResponseHandler( this.chunkSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); let finishReason: LanguageModelV2FinishReason = 'unknown'; const usage: LanguageModelV2Usage = { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, }; let isFirstChunk = true; return { stream: response.pipeThrough( new TransformStream< ParseResult<z.infer<typeof this.chunkSchema>>, LanguageModelV2StreamPart >({ start(controller) { controller.enqueue({ type: 'stream-start', warnings }); }, transform(chunk, controller) { if (options.includeRawChunks) { controller.enqueue({ type: 'raw', rawValue: chunk.rawValue }); } // handle failed chunk parsing / validation: if (!chunk.success) { finishReason = 'error'; controller.enqueue({ type: 'error', error: chunk.error }); return; } const value = chunk.value; // handle error chunks: if ('error' in value) { finishReason = 'error'; controller.enqueue({ type: 'error', error: value.error }); return; } if (isFirstChunk) { isFirstChunk = false; controller.enqueue({ type: 'response-metadata', ...getResponseMetadata(value), }); controller.enqueue({ type: 'text-start', id: '0', }); } if (value.usage != null) { usage.inputTokens = value.usage.prompt_tokens ?? undefined; usage.outputTokens = value.usage.completion_tokens ?? undefined; usage.totalTokens = value.usage.total_tokens ?? undefined; } const choice = value.choices[0]; if (choice?.finish_reason != null) { finishReason = mapOpenAICompatibleFinishReason( choice.finish_reason, ); } if (choice?.text != null) { controller.enqueue({ type: 'text-delta', id: '0', delta: choice.text, }); } }, flush(controller) { if (!isFirstChunk) { controller.enqueue({ type: 'text-end', id: '0' }); } controller.enqueue({ type: 'finish', finishReason, usage, }); }, }), ), request: { body }, response: { headers: responseHeaders }, }; } } const usageSchema = z.object({ prompt_tokens: z.number(), completion_tokens: z.number(), total_tokens: z.number(), }); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const openaiCompatibleCompletionResponseSchema = z.object({ id: z.string().nullish(), created: z.number().nullish(), model: z.string().nullish(), choices: z.array( z.object({ text: z.string(), finish_reason: z.string(), }), ), usage: usageSchema.nullish(), }); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const createOpenAICompatibleCompletionChunkSchema = < ERROR_SCHEMA extends z.core.$ZodType, >( errorSchema: ERROR_SCHEMA, ) => z.union([ z.object({ id: z.string().nullish(), created: z.number().nullish(), model: z.string().nullish(), choices: z.array( z.object({ text: z.string(), finish_reason: z.string().nullish(), index: z.number(), }), ), usage: usageSchema.nullish(), }), errorSchema, ]); --- File: /ai/packages/openai-compatible/src/openai-compatible-completion-options.ts --- import { z } from 'zod/v4'; export type OpenAICompatibleCompletionModelId = string; export const openaiCompatibleCompletionProviderOptions = z.object({ /** * Echo back the prompt in addition to the completion. */ echo: z.boolean().optional(), /** * Modify the likelihood of specified tokens appearing in the completion. * * Accepts a JSON object that maps tokens (specified by their token ID in * the GPT tokenizer) to an associated bias value from -100 to 100. */ logitBias: z.record(z.string(), z.number()).optional(), /** * The suffix that comes after a completion of inserted text. */ suffix: z.string().optional(), /** * A unique identifier representing your end-user, which can help providers to * monitor and detect abuse. */ user: z.string().optional(), }); export type OpenAICompatibleCompletionProviderOptions = z.infer< typeof openaiCompatibleCompletionProviderOptions >; --- File: /ai/packages/openai-compatible/src/openai-compatible-embedding-model.test.ts --- import { EmbeddingModelV2Embedding } from '@ai-sdk/provider'; import { createTestServer } from '@ai-sdk/provider-utils/test'; import { createOpenAICompatible } from './openai-compatible-provider'; const dummyEmbeddings = [ [0.1, 0.2, 0.3, 0.4, 0.5], [0.6, 0.7, 0.8, 0.9, 1.0], ]; const testValues = ['sunny day at the beach', 'rainy day in the city']; const provider = createOpenAICompatible({ baseURL: 'https://my.api.com/v1/', name: 'test-provider', headers: { Authorization: `Bearer test-api-key`, }, }); const model = provider.textEmbeddingModel('text-embedding-3-large'); const server = createTestServer({ 'https://my.api.com/v1/embeddings': {}, }); describe('doEmbed', () => { function prepareJsonResponse({ embeddings = dummyEmbeddings, usage = { prompt_tokens: 8, total_tokens: 8 }, headers, }: { embeddings?: EmbeddingModelV2Embedding[]; usage?: { prompt_tokens: number; total_tokens: number }; headers?: Record<string, string>; } = {}) { server.urls['https://my.api.com/v1/embeddings'].response = { type: 'json-value', headers, body: { object: 'list', data: embeddings.map((embedding, i) => ({ object: 'embedding', index: i, embedding, })), model: 'text-embedding-3-large', usage, }, }; } it('should extract embedding', async () => { prepareJsonResponse(); const { embeddings } = await model.doEmbed({ values: testValues }); expect(embeddings).toStrictEqual(dummyEmbeddings); }); it('should expose the raw response headers', async () => { prepareJsonResponse({ headers: { 'test-header': 'test-value' }, }); const { response } = await model.doEmbed({ values: testValues }); expect(response?.headers).toStrictEqual({ // default headers: 'content-length': '236', 'content-type': 'application/json', // custom header 'test-header': 'test-value', }); }); it('should extract usage', async () => { prepareJsonResponse({ usage: { prompt_tokens: 20, total_tokens: 20 }, }); const { usage } = await model.doEmbed({ values: testValues }); expect(usage).toStrictEqual({ tokens: 20 }); }); it('should pass the model and the values', async () => { prepareJsonResponse(); await model.doEmbed({ values: testValues }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'text-embedding-3-large', input: testValues, encoding_format: 'float', }); }); it('should pass the dimensions setting', async () => { prepareJsonResponse(); await provider.textEmbeddingModel('text-embedding-3-large').doEmbed({ values: testValues, providerOptions: { 'openai-compatible': { dimensions: 64, }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'text-embedding-3-large', input: testValues, encoding_format: 'float', dimensions: 64, }); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createOpenAICompatible({ baseURL: 'https://my.api.com/v1/', name: 'test-provider', headers: { Authorization: `Bearer test-api-key`, 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.textEmbeddingModel('text-embedding-3-large').doEmbed({ values: testValues, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); }); --- File: /ai/packages/openai-compatible/src/openai-compatible-embedding-model.ts --- import { EmbeddingModelV2, TooManyEmbeddingValuesForCallError, } from '@ai-sdk/provider'; import { combineHeaders, createJsonErrorResponseHandler, createJsonResponseHandler, FetchFunction, parseProviderOptions, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { OpenAICompatibleEmbeddingModelId, openaiCompatibleEmbeddingProviderOptions, } from './openai-compatible-embedding-options'; import { defaultOpenAICompatibleErrorStructure, ProviderErrorStructure, } from './openai-compatible-error'; type OpenAICompatibleEmbeddingConfig = { /** Override the maximum number of embeddings per call. */ maxEmbeddingsPerCall?: number; /** Override the parallelism of embedding calls. */ supportsParallelCalls?: boolean; provider: string; url: (options: { modelId: string; path: string }) => string; headers: () => Record<string, string | undefined>; fetch?: FetchFunction; errorStructure?: ProviderErrorStructure<any>; }; export class OpenAICompatibleEmbeddingModel implements EmbeddingModelV2<string> { readonly specificationVersion = 'v2'; readonly modelId: OpenAICompatibleEmbeddingModelId; private readonly config: OpenAICompatibleEmbeddingConfig; get provider(): string { return this.config.provider; } get maxEmbeddingsPerCall(): number { return this.config.maxEmbeddingsPerCall ?? 2048; } get supportsParallelCalls(): boolean { return this.config.supportsParallelCalls ?? true; } constructor( modelId: OpenAICompatibleEmbeddingModelId, config: OpenAICompatibleEmbeddingConfig, ) { this.modelId = modelId; this.config = config; } private get providerOptionsName(): string { return this.config.provider.split('.')[0].trim(); } async doEmbed({ values, headers, abortSignal, providerOptions, }: Parameters<EmbeddingModelV2<string>['doEmbed']>[0]): Promise< Awaited<ReturnType<EmbeddingModelV2<string>['doEmbed']>> > { const compatibleOptions = Object.assign( (await parseProviderOptions({ provider: 'openai-compatible', providerOptions, schema: openaiCompatibleEmbeddingProviderOptions, })) ?? {}, (await parseProviderOptions({ provider: this.providerOptionsName, providerOptions, schema: openaiCompatibleEmbeddingProviderOptions, })) ?? {}, ); if (values.length > this.maxEmbeddingsPerCall) { throw new TooManyEmbeddingValuesForCallError({ provider: this.provider, modelId: this.modelId, maxEmbeddingsPerCall: this.maxEmbeddingsPerCall, values, }); } const { responseHeaders, value: response, rawValue, } = await postJsonToApi({ url: this.config.url({ path: '/embeddings', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), headers), body: { model: this.modelId, input: values, encoding_format: 'float', dimensions: compatibleOptions.dimensions, user: compatibleOptions.user, }, failedResponseHandler: createJsonErrorResponseHandler( this.config.errorStructure ?? defaultOpenAICompatibleErrorStructure, ), successfulResponseHandler: createJsonResponseHandler( openaiTextEmbeddingResponseSchema, ), abortSignal, fetch: this.config.fetch, }); return { embeddings: response.data.map(item => item.embedding), usage: response.usage ? { tokens: response.usage.prompt_tokens } : undefined, providerMetadata: response.providerMetadata, response: { headers: responseHeaders, body: rawValue }, }; } } // minimal version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const openaiTextEmbeddingResponseSchema = z.object({ data: z.array(z.object({ embedding: z.array(z.number()) })), usage: z.object({ prompt_tokens: z.number() }).nullish(), providerMetadata: z .record(z.string(), z.record(z.string(), z.any())) .optional(), }); --- File: /ai/packages/openai-compatible/src/openai-compatible-embedding-options.ts --- import { z } from 'zod/v4'; export type OpenAICompatibleEmbeddingModelId = string; export const openaiCompatibleEmbeddingProviderOptions = z.object({ /** * The number of dimensions the resulting output embeddings should have. * Only supported in text-embedding-3 and later models. */ dimensions: z.number().optional(), /** * A unique identifier representing your end-user, which can help providers to * monitor and detect abuse. */ user: z.string().optional(), }); export type OpenAICompatibleEmbeddingProviderOptions = z.infer< typeof openaiCompatibleEmbeddingProviderOptions >; --- File: /ai/packages/openai-compatible/src/openai-compatible-error.ts --- import { z, ZodType } from 'zod/v4'; export const openaiCompatibleErrorDataSchema = z.object({ error: z.object({ message: z.string(), // The additional information below is handled loosely to support // OpenAI-compatible providers that have slightly different error // responses: type: z.string().nullish(), param: z.any().nullish(), code: z.union([z.string(), z.number()]).nullish(), }), }); export type OpenAICompatibleErrorData = z.infer< typeof openaiCompatibleErrorDataSchema >; export type ProviderErrorStructure<T> = { errorSchema: ZodType<T>; errorToMessage: (error: T) => string; isRetryable?: (response: Response, error?: T) => boolean; }; export const defaultOpenAICompatibleErrorStructure: ProviderErrorStructure<OpenAICompatibleErrorData> = { errorSchema: openaiCompatibleErrorDataSchema, errorToMessage: data => data.error.message, }; --- File: /ai/packages/openai-compatible/src/openai-compatible-image-model.test.ts --- import { FetchFunction } from '@ai-sdk/provider-utils'; import { createTestServer } from '@ai-sdk/provider-utils/test'; import { OpenAICompatibleImageModel } from './openai-compatible-image-model'; import { z } from 'zod/v4'; import { ProviderErrorStructure } from './openai-compatible-error'; import { ImageModelV2CallOptions } from '@ai-sdk/provider'; const prompt = 'A photorealistic astronaut riding a horse'; function createBasicModel({ headers, fetch, currentDate, errorStructure, }: { headers?: () => Record<string, string | undefined>; fetch?: FetchFunction; currentDate?: () => Date; errorStructure?: ProviderErrorStructure<any>; } = {}) { return new OpenAICompatibleImageModel('dall-e-3', { provider: 'openai-compatible', headers: headers ?? (() => ({ Authorization: 'Bearer test-key' })), url: ({ modelId, path }) => `https://api.example.com/${modelId}${path}`, fetch, errorStructure, _internal: { currentDate, }, }); } function createDefaultGenerateParams(overrides = {}): ImageModelV2CallOptions { return { prompt: 'A photorealistic astronaut riding a horse', n: 1, size: '1024x1024', aspectRatio: undefined, seed: undefined, providerOptions: {}, headers: {}, abortSignal: undefined, ...overrides, }; } describe('OpenAICompatibleImageModel', () => { const server = createTestServer({ 'https://api.example.com/dall-e-3/images/generations': { response: { type: 'json-value', body: { data: [ { b64_json: 'test1234', }, { b64_json: 'test5678', }, ], }, }, }, }); describe('constructor', () => { it('should expose correct provider and model information', () => { const model = createBasicModel(); expect(model.provider).toBe('openai-compatible'); expect(model.modelId).toBe('dall-e-3'); expect(model.specificationVersion).toBe('v2'); expect(model.maxImagesPerCall).toBe(10); }); }); describe('doGenerate', () => { it('should pass the correct parameters', async () => { const model = createBasicModel(); await model.doGenerate( createDefaultGenerateParams({ n: 2, providerOptions: { openai: { quality: 'hd' } }, }), ); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'dall-e-3', prompt, n: 2, size: '1024x1024', quality: 'hd', response_format: 'b64_json', }); }); it('should add warnings for unsupported settings', async () => { const model = createBasicModel(); const result = await model.doGenerate( createDefaultGenerateParams({ aspectRatio: '16:9', seed: 123, }), ); expect(result.warnings).toHaveLength(2); expect(result.warnings).toContainEqual({ type: 'unsupported-setting', setting: 'aspectRatio', details: 'This model does not support aspect ratio. Use `size` instead.', }); expect(result.warnings).toContainEqual({ type: 'unsupported-setting', setting: 'seed', }); }); it('should pass headers', async () => { const modelWithHeaders = createBasicModel({ headers: () => ({ 'Custom-Provider-Header': 'provider-header-value', }), }); await modelWithHeaders.doGenerate( createDefaultGenerateParams({ headers: { 'Custom-Request-Header': 'request-header-value', }, }), ); expect(server.calls[0].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should handle API errors with custom error structure', async () => { // Define a custom error schema different from OpenAI's format const customErrorSchema = z.object({ status: z.string(), details: z.object({ errorMessage: z.string(), errorCode: z.number(), }), }); server.urls[ 'https://api.example.com/dall-e-3/images/generations' ].response = { type: 'error', status: 400, body: JSON.stringify({ status: 'error', details: { errorMessage: 'Custom provider error format', errorCode: 1234, }, }), }; const model = createBasicModel({ errorStructure: { errorSchema: customErrorSchema, errorToMessage: data => `Error ${data.details.errorCode}: ${data.details.errorMessage}`, }, }); await expect( model.doGenerate(createDefaultGenerateParams()), ).rejects.toMatchObject({ message: 'Error 1234: Custom provider error format', statusCode: 400, url: 'https://api.example.com/dall-e-3/images/generations', }); }); it('should handle API errors with default error structure', async () => { server.urls[ 'https://api.example.com/dall-e-3/images/generations' ].response = { type: 'error', status: 400, body: JSON.stringify({ error: { message: 'Invalid prompt content', type: 'invalid_request_error', param: null, code: null, }, }), }; const model = createBasicModel(); await expect( model.doGenerate(createDefaultGenerateParams()), ).rejects.toMatchObject({ message: 'Invalid prompt content', statusCode: 400, url: 'https://api.example.com/dall-e-3/images/generations', }); }); it('should return the raw b64_json content', async () => { const model = createBasicModel(); const result = await model.doGenerate( createDefaultGenerateParams({ n: 2, }), ); expect(result.images).toHaveLength(2); expect(result.images[0]).toBe('test1234'); expect(result.images[1]).toBe('test5678'); }); describe('response metadata', () => { it('should include timestamp, headers and modelId in response', async () => { const testDate = new Date('2024-01-01T00:00:00Z'); const model = createBasicModel({ currentDate: () => testDate, }); const result = await model.doGenerate(createDefaultGenerateParams()); expect(result.response).toStrictEqual({ timestamp: testDate, modelId: 'dall-e-3', headers: expect.any(Object), }); }); }); it('should use real date when no custom date provider is specified', async () => { const beforeDate = new Date(); const model = new OpenAICompatibleImageModel('dall-e-3', { provider: 'openai-compatible', headers: () => ({ Authorization: 'Bearer test-key' }), url: ({ modelId, path }) => `https://api.example.com/${modelId}${path}`, }); const result = await model.doGenerate(createDefaultGenerateParams()); const afterDate = new Date(); expect(result.response.timestamp.getTime()).toBeGreaterThanOrEqual( beforeDate.getTime(), ); expect(result.response.timestamp.getTime()).toBeLessThanOrEqual( afterDate.getTime(), ); expect(result.response.modelId).toBe('dall-e-3'); }); it('should pass the user setting in the request', async () => { const model = createBasicModel(); await model.doGenerate( createDefaultGenerateParams({ providerOptions: { openai: { user: 'test-user-id', }, }, }), ); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'dall-e-3', prompt, n: 1, size: '1024x1024', user: 'test-user-id', response_format: 'b64_json', }); }); it('should not include user field in request when not set via provider options', async () => { const model = createBasicModel(); await model.doGenerate( createDefaultGenerateParams({ providerOptions: { openai: {}, }, }), ); const requestBody = await server.calls[0].requestBodyJson; expect(requestBody).toStrictEqual({ model: 'dall-e-3', prompt, n: 1, size: '1024x1024', response_format: 'b64_json', }); expect(requestBody).not.toHaveProperty('user'); }); }); }); --- File: /ai/packages/openai-compatible/src/openai-compatible-image-model.ts --- import { ImageModelV2, ImageModelV2CallWarning } from '@ai-sdk/provider'; import { combineHeaders, createJsonErrorResponseHandler, createJsonResponseHandler, FetchFunction, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { defaultOpenAICompatibleErrorStructure, ProviderErrorStructure, } from './openai-compatible-error'; import { OpenAICompatibleImageModelId } from './openai-compatible-image-settings'; export type OpenAICompatibleImageModelConfig = { provider: string; headers: () => Record<string, string | undefined>; url: (options: { modelId: string; path: string }) => string; fetch?: FetchFunction; errorStructure?: ProviderErrorStructure<any>; _internal?: { currentDate?: () => Date; }; }; export class OpenAICompatibleImageModel implements ImageModelV2 { readonly specificationVersion = 'v2'; readonly maxImagesPerCall = 10; get provider(): string { return this.config.provider; } constructor( readonly modelId: OpenAICompatibleImageModelId, private readonly config: OpenAICompatibleImageModelConfig, ) {} async doGenerate({ prompt, n, size, aspectRatio, seed, providerOptions, headers, abortSignal, }: Parameters<ImageModelV2['doGenerate']>[0]): Promise< Awaited<ReturnType<ImageModelV2['doGenerate']>> > { const warnings: Array<ImageModelV2CallWarning> = []; if (aspectRatio != null) { warnings.push({ type: 'unsupported-setting', setting: 'aspectRatio', details: 'This model does not support aspect ratio. Use `size` instead.', }); } if (seed != null) { warnings.push({ type: 'unsupported-setting', setting: 'seed' }); } const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { value: response, responseHeaders } = await postJsonToApi({ url: this.config.url({ path: '/images/generations', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), headers), body: { model: this.modelId, prompt, n, size, ...(providerOptions.openai ?? {}), response_format: 'b64_json', }, failedResponseHandler: createJsonErrorResponseHandler( this.config.errorStructure ?? defaultOpenAICompatibleErrorStructure, ), successfulResponseHandler: createJsonResponseHandler( openaiCompatibleImageResponseSchema, ), abortSignal, fetch: this.config.fetch, }); return { images: response.data.map(item => item.b64_json), warnings, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, }, }; } } // minimal version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const openaiCompatibleImageResponseSchema = z.object({ data: z.array(z.object({ b64_json: z.string() })), }); --- File: /ai/packages/openai-compatible/src/openai-compatible-image-settings.ts --- export type OpenAICompatibleImageModelId = string; --- File: /ai/packages/openai-compatible/src/openai-compatible-metadata-extractor.ts --- import { SharedV2ProviderMetadata } from '@ai-sdk/provider'; /** Extracts provider-specific metadata from API responses. Used to standardize metadata handling across different LLM providers while allowing provider-specific metadata to be captured. */ export type MetadataExtractor = { /** * Extracts provider metadata from a complete, non-streaming response. * * @param parsedBody - The parsed response JSON body from the provider's API. * * @returns Provider-specific metadata or undefined if no metadata is available. * The metadata should be under a key indicating the provider id. */ extractMetadata: ({ parsedBody, }: { parsedBody: unknown; }) => Promise<SharedV2ProviderMetadata | undefined>; /** * Creates an extractor for handling streaming responses. The returned object provides * methods to process individual chunks and build the final metadata from the accumulated * stream data. * * @returns An object with methods to process chunks and build metadata from a stream */ createStreamExtractor: () => { /** * Process an individual chunk from the stream. Called for each chunk in the response stream * to accumulate metadata throughout the streaming process. * * @param parsedChunk - The parsed JSON response chunk from the provider's API */ processChunk(parsedChunk: unknown): void; /** * Builds the metadata object after all chunks have been processed. * Called at the end of the stream to generate the complete provider metadata. * * @returns Provider-specific metadata or undefined if no metadata is available. * The metadata should be under a key indicating the provider id. */ buildMetadata(): SharedV2ProviderMetadata | undefined; }; }; --- File: /ai/packages/openai-compatible/src/openai-compatible-prepare-tools.ts --- import { LanguageModelV2CallOptions, LanguageModelV2CallWarning, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; export function prepareTools({ tools, toolChoice, }: { tools: LanguageModelV2CallOptions['tools']; toolChoice?: LanguageModelV2CallOptions['toolChoice']; }): { tools: | undefined | Array<{ type: 'function'; function: { name: string; description: string | undefined; parameters: unknown; }; }>; toolChoice: | { type: 'function'; function: { name: string } } | 'auto' | 'none' | 'required' | undefined; toolWarnings: LanguageModelV2CallWarning[]; } { // when the tools array is empty, change it to undefined to prevent errors: tools = tools?.length ? tools : undefined; const toolWarnings: LanguageModelV2CallWarning[] = []; if (tools == null) { return { tools: undefined, toolChoice: undefined, toolWarnings }; } const openaiCompatTools: Array<{ type: 'function'; function: { name: string; description: string | undefined; parameters: unknown; }; }> = []; for (const tool of tools) { if (tool.type === 'provider-defined') { toolWarnings.push({ type: 'unsupported-tool', tool }); } else { openaiCompatTools.push({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.inputSchema, }, }); } } if (toolChoice == null) { return { tools: openaiCompatTools, toolChoice: undefined, toolWarnings }; } const type = toolChoice.type; switch (type) { case 'auto': case 'none': case 'required': return { tools: openaiCompatTools, toolChoice: type, toolWarnings }; case 'tool': return { tools: openaiCompatTools, toolChoice: { type: 'function', function: { name: toolChoice.toolName }, }, toolWarnings, }; default: { const _exhaustiveCheck: never = type; throw new UnsupportedFunctionalityError({ functionality: `tool choice type: ${_exhaustiveCheck}`, }); } } } --- File: /ai/packages/openai-compatible/src/openai-compatible-provider.test.ts --- import { createOpenAICompatible } from './openai-compatible-provider'; import { OpenAICompatibleChatLanguageModel } from './openai-compatible-chat-language-model'; import { OpenAICompatibleCompletionLanguageModel } from './openai-compatible-completion-language-model'; import { OpenAICompatibleEmbeddingModel } from './openai-compatible-embedding-model'; const OpenAICompatibleChatLanguageModelMock = vi.mocked( OpenAICompatibleChatLanguageModel, ); const OpenAICompatibleCompletionLanguageModelMock = vi.mocked( OpenAICompatibleCompletionLanguageModel, ); const OpenAICompatibleEmbeddingModelMock = vi.mocked( OpenAICompatibleEmbeddingModel, ); vi.mock('./openai-compatible-chat-language-model', () => ({ OpenAICompatibleChatLanguageModel: vi.fn(), })); vi.mock('./openai-compatible-completion-language-model', () => ({ OpenAICompatibleCompletionLanguageModel: vi.fn(), })); vi.mock('./openai-compatible-embedding-model', () => ({ OpenAICompatibleEmbeddingModel: vi.fn(), })); describe('OpenAICompatibleProvider', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('createOpenAICompatible', () => { it('should create provider with correct configuration', () => { const options = { baseURL: 'https://api.example.com', name: 'test-provider', apiKey: 'test-api-key', headers: { 'Custom-Header': 'value' }, queryParams: { 'Custom-Param': 'value' }, }; const provider = createOpenAICompatible(options); provider('model-id'); const constructorCall = OpenAICompatibleChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; const headers = config.headers(); expect(headers).toEqual({ Authorization: 'Bearer test-api-key', 'Custom-Header': 'value', }); expect(config.provider).toBe('test-provider.chat'); expect(config.url({ modelId: 'model-id', path: '/v1/chat' })).toBe( 'https://api.example.com/v1/chat?Custom-Param=value', ); }); it('should create headers without Authorization when no apiKey provided', () => { const options = { baseURL: 'https://api.example.com', name: 'test-provider', headers: { 'Custom-Header': 'value' }, }; const provider = createOpenAICompatible(options); provider('model-id'); const constructorCall = OpenAICompatibleChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; const headers = config.headers(); expect(headers).toEqual({ 'Custom-Header': 'value', }); }); }); describe('model creation methods', () => { const defaultOptions = { baseURL: 'https://api.example.com', name: 'test-provider', apiKey: 'test-api-key', headers: { 'Custom-Header': 'value' }, queryParams: { 'Custom-Param': 'value' }, }; it('should create chat model with correct configuration', () => { const provider = createOpenAICompatible(defaultOptions); provider.chatModel('chat-model'); const constructorCall = OpenAICompatibleChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; const headers = config.headers(); expect(headers).toEqual({ Authorization: 'Bearer test-api-key', 'Custom-Header': 'value', }); expect(config.provider).toBe('test-provider.chat'); expect(config.url({ modelId: 'model-id', path: '/v1/chat' })).toBe( 'https://api.example.com/v1/chat?Custom-Param=value', ); }); it('should create completion model with correct configuration', () => { const provider = createOpenAICompatible(defaultOptions); provider.completionModel('completion-model'); const constructorCall = OpenAICompatibleCompletionLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; const headers = config.headers(); expect(headers).toEqual({ Authorization: 'Bearer test-api-key', 'Custom-Header': 'value', }); expect(config.provider).toBe('test-provider.completion'); expect( config.url({ modelId: 'completion-model', path: '/v1/completions' }), ).toBe('https://api.example.com/v1/completions?Custom-Param=value'); }); it('should create embedding model with correct configuration', () => { const provider = createOpenAICompatible(defaultOptions); provider.textEmbeddingModel('embedding-model'); const constructorCall = OpenAICompatibleEmbeddingModelMock.mock.calls[0]; const config = constructorCall[1]; const headers = config.headers(); expect(headers).toEqual({ Authorization: 'Bearer test-api-key', 'Custom-Header': 'value', }); expect(config.provider).toBe('test-provider.embedding'); expect( config.url({ modelId: 'embedding-model', path: '/v1/embeddings' }), ).toBe('https://api.example.com/v1/embeddings?Custom-Param=value'); }); it('should use languageModel as default when called as function', () => { const provider = createOpenAICompatible(defaultOptions); provider('model-id'); expect(OpenAICompatibleChatLanguageModel).toHaveBeenCalledWith( 'model-id', expect.objectContaining({ provider: 'test-provider.chat', }), ); }); it('should create URL without query parameters when queryParams is not specified', () => { const options = { baseURL: 'https://api.example.com', name: 'test-provider', apiKey: 'test-api-key', }; const provider = createOpenAICompatible(options); provider('model-id'); const constructorCall = OpenAICompatibleChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; expect(config.url({ modelId: 'model-id', path: '/v1/chat' })).toBe( 'https://api.example.com/v1/chat', ); }); }); describe('includeUsage setting', () => { it('should pass includeUsage: true to all model types when specified in provider settings', () => { const options = { baseURL: 'https://api.example.com', name: 'test-provider', includeUsage: true, }; const provider = createOpenAICompatible(options); provider.chatModel('chat-model'); expect( OpenAICompatibleChatLanguageModelMock.mock.calls[0][1].includeUsage, ).toBe(true); provider.completionModel('completion-model'); expect( OpenAICompatibleCompletionLanguageModelMock.mock.calls[0][1] .includeUsage, ).toBe(true); provider('model-id'); expect( OpenAICompatibleChatLanguageModelMock.mock.calls[1][1].includeUsage, ).toBe(true); }); it('should pass includeUsage: false to all model types when specified in provider settings', () => { const options = { baseURL: 'https://api.example.com', name: 'test-provider', includeUsage: false, }; const provider = createOpenAICompatible(options); provider.chatModel('chat-model'); expect( OpenAICompatibleChatLanguageModelMock.mock.calls[0][1].includeUsage, ).toBe(false); provider.completionModel('completion-model'); expect( OpenAICompatibleCompletionLanguageModelMock.mock.calls[0][1] .includeUsage, ).toBe(false); provider('model-id'); expect( OpenAICompatibleChatLanguageModelMock.mock.calls[1][1].includeUsage, ).toBe(false); }); it('should pass includeUsage: undefined to all model types when not specified in provider settings', () => { const options = { baseURL: 'https://api.example.com', name: 'test-provider', }; const provider = createOpenAICompatible(options); provider.chatModel('chat-model'); expect( OpenAICompatibleChatLanguageModelMock.mock.calls[0][1].includeUsage, ).toBeUndefined(); provider.completionModel('completion-model'); expect( OpenAICompatibleCompletionLanguageModelMock.mock.calls[0][1] .includeUsage, ).toBeUndefined(); provider('model-id'); expect( OpenAICompatibleChatLanguageModelMock.mock.calls[1][1].includeUsage, ).toBeUndefined(); }); }); }); --- File: /ai/packages/openai-compatible/src/openai-compatible-provider.ts --- import { EmbeddingModelV2, ImageModelV2, LanguageModelV2, ProviderV2, } from '@ai-sdk/provider'; import { FetchFunction, withoutTrailingSlash } from '@ai-sdk/provider-utils'; import { OpenAICompatibleChatLanguageModel } from './openai-compatible-chat-language-model'; import { OpenAICompatibleCompletionLanguageModel } from './openai-compatible-completion-language-model'; import { OpenAICompatibleEmbeddingModel } from './openai-compatible-embedding-model'; import { OpenAICompatibleImageModel } from './openai-compatible-image-model'; export interface OpenAICompatibleProvider< CHAT_MODEL_IDS extends string = string, COMPLETION_MODEL_IDS extends string = string, EMBEDDING_MODEL_IDS extends string = string, IMAGE_MODEL_IDS extends string = string, > extends Omit<ProviderV2, 'imageModel'> { (modelId: CHAT_MODEL_IDS): LanguageModelV2; languageModel(modelId: CHAT_MODEL_IDS): LanguageModelV2; chatModel(modelId: CHAT_MODEL_IDS): LanguageModelV2; completionModel(modelId: COMPLETION_MODEL_IDS): LanguageModelV2; textEmbeddingModel(modelId: EMBEDDING_MODEL_IDS): EmbeddingModelV2<string>; imageModel(modelId: IMAGE_MODEL_IDS): ImageModelV2; } export interface OpenAICompatibleProviderSettings { /** Base URL for the API calls. */ baseURL: string; /** Provider name. */ name: string; /** API key for authenticating requests. If specified, adds an `Authorization` header to request headers with the value `Bearer <apiKey>`. This will be added before any headers potentially specified in the `headers` option. */ apiKey?: string; /** Optional custom headers to include in requests. These will be added to request headers after any headers potentially added by use of the `apiKey` option. */ headers?: Record<string, string>; /** Optional custom url query parameters to include in request urls. */ queryParams?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; /** Include usage information in streaming responses. */ includeUsage?: boolean; } /** Create an OpenAICompatible provider instance. */ export function createOpenAICompatible< CHAT_MODEL_IDS extends string, COMPLETION_MODEL_IDS extends string, EMBEDDING_MODEL_IDS extends string, IMAGE_MODEL_IDS extends string, >( options: OpenAICompatibleProviderSettings, ): OpenAICompatibleProvider< CHAT_MODEL_IDS, COMPLETION_MODEL_IDS, EMBEDDING_MODEL_IDS, IMAGE_MODEL_IDS > { const baseURL = withoutTrailingSlash(options.baseURL); const providerName = options.name; interface CommonModelConfig { provider: string; url: ({ path }: { path: string }) => string; headers: () => Record<string, string>; fetch?: FetchFunction; } const getHeaders = () => ({ ...(options.apiKey && { Authorization: `Bearer ${options.apiKey}` }), ...options.headers, }); const getCommonModelConfig = (modelType: string): CommonModelConfig => ({ provider: `${providerName}.${modelType}`, url: ({ path }) => { const url = new URL(`${baseURL}${path}`); if (options.queryParams) { url.search = new URLSearchParams(options.queryParams).toString(); } return url.toString(); }, headers: getHeaders, fetch: options.fetch, }); const createLanguageModel = (modelId: CHAT_MODEL_IDS) => createChatModel(modelId); const createChatModel = (modelId: CHAT_MODEL_IDS) => new OpenAICompatibleChatLanguageModel(modelId, { ...getCommonModelConfig('chat'), includeUsage: options.includeUsage, }); const createCompletionModel = (modelId: COMPLETION_MODEL_IDS) => new OpenAICompatibleCompletionLanguageModel(modelId, { ...getCommonModelConfig('completion'), includeUsage: options.includeUsage, }); const createEmbeddingModel = (modelId: EMBEDDING_MODEL_IDS) => new OpenAICompatibleEmbeddingModel(modelId, { ...getCommonModelConfig('embedding'), }); const createImageModel = (modelId: IMAGE_MODEL_IDS) => new OpenAICompatibleImageModel(modelId, getCommonModelConfig('image')); const provider = (modelId: CHAT_MODEL_IDS) => createLanguageModel(modelId); provider.languageModel = createLanguageModel; provider.chatModel = createChatModel; provider.completionModel = createCompletionModel; provider.textEmbeddingModel = createEmbeddingModel; provider.imageModel = createImageModel; return provider as OpenAICompatibleProvider< CHAT_MODEL_IDS, COMPLETION_MODEL_IDS, EMBEDDING_MODEL_IDS, IMAGE_MODEL_IDS >; } --- File: /ai/packages/openai-compatible/internal.d.ts --- export * from './dist/internal'; --- File: /ai/packages/openai-compatible/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, { entry: ['src/internal/index.ts'], outDir: 'dist/internal', format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/openai-compatible/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/openai-compatible/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/perplexity/src/convert-to-perplexity-messages.test.ts --- import { convertToPerplexityMessages } from './convert-to-perplexity-messages'; import { UnsupportedFunctionalityError } from '@ai-sdk/provider'; describe('convertToPerplexityMessages', () => { describe('system messages', () => { it('should convert a system message with text content', () => { expect( convertToPerplexityMessages([ { role: 'system', content: 'System initialization', }, ]), ).toMatchSnapshot(); }); }); describe('user messages', () => { it('should convert a user message with text parts', () => { expect( convertToPerplexityMessages([ { role: 'user', content: [ { type: 'text', text: 'Hello ' }, { type: 'text', text: 'World' }, ], }, ]), ).toMatchSnapshot(); }); it('should convert a user message with image parts', () => { expect( convertToPerplexityMessages([ { role: 'user', content: [ { type: 'text', text: 'Hello ' }, { type: 'file', data: new Uint8Array([0, 1, 2, 3]), mediaType: 'image/png', }, ], }, ]), ).toMatchSnapshot(); }); }); describe('assistant messages', () => { it('should convert an assistant message with text content', () => { expect( convertToPerplexityMessages([ { role: 'assistant', content: [{ type: 'text', text: 'Assistant reply' }], }, ]), ).toMatchSnapshot(); }); }); describe('tool messages', () => { it('should throw an error for tool messages', () => { expect(() => { convertToPerplexityMessages([ { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'dummy-tool-call-id', toolName: 'dummy-tool-name', output: { type: 'text', value: 'This should fail' }, }, ], }, ]); }).toThrow(UnsupportedFunctionalityError); }); }); }); --- File: /ai/packages/perplexity/src/convert-to-perplexity-messages.ts --- import { LanguageModelV2Prompt, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { PerplexityMessageContent, PerplexityPrompt, } from './perplexity-language-model-prompt'; import { convertUint8ArrayToBase64 } from '@ai-sdk/provider-utils'; export function convertToPerplexityMessages( prompt: LanguageModelV2Prompt, ): PerplexityPrompt { const messages: PerplexityPrompt = []; for (const { role, content } of prompt) { switch (role) { case 'system': { messages.push({ role: 'system', content }); break; } case 'user': case 'assistant': { const hasImage = content.some( part => part.type === 'file' && part.mediaType.startsWith('image/'), ); const messageContent = content .map(part => { switch (part.type) { case 'text': { return { type: 'text', text: part.text, }; } case 'file': { return part.data instanceof URL ? { type: 'image_url', image_url: { url: part.data.toString(), }, } : { type: 'image_url', image_url: { url: `data:${part.mediaType ?? 'image/jpeg'};base64,${ typeof part.data === 'string' ? part.data : convertUint8ArrayToBase64(part.data) }`, }, }; } } }) .filter(Boolean) as PerplexityMessageContent[]; messages.push({ role, content: hasImage ? messageContent : messageContent .filter(part => part.type === 'text') .map(part => part.text) .join(''), }); break; } case 'tool': { throw new UnsupportedFunctionalityError({ functionality: 'Tool messages', }); } default: { const _exhaustiveCheck: never = role; throw new Error(`Unsupported role: ${_exhaustiveCheck}`); } } } return messages; } --- File: /ai/packages/perplexity/src/index.ts --- export { createPerplexity, perplexity } from './perplexity-provider'; export type { PerplexityProvider, PerplexityProviderSettings, } from './perplexity-provider'; --- File: /ai/packages/perplexity/src/map-perplexity-finish-reason.ts --- import { LanguageModelV2FinishReason } from '@ai-sdk/provider'; export function mapPerplexityFinishReason( finishReason: string | null | undefined, ): LanguageModelV2FinishReason { switch (finishReason) { case 'stop': case 'length': return finishReason; default: return 'unknown'; } } --- File: /ai/packages/perplexity/src/perplexity-language-model-options.ts --- // https://docs.perplexity.ai/models/model-cards export type PerplexityLanguageModelId = | 'sonar-deep-research' | 'sonar-reasoning-pro' | 'sonar-reasoning' | 'sonar-pro' | 'sonar' | (string & {}); --- File: /ai/packages/perplexity/src/perplexity-language-model-prompt.ts --- export type PerplexityPrompt = Array<PerplexityMessage>; export type PerplexityMessage = { role: 'system' | 'user' | 'assistant'; content: string | PerplexityMessageContent[]; }; export type PerplexityMessageContent = | { type: 'text'; text: string; } | { type: 'image_url'; image_url: { url: string; }; }; --- File: /ai/packages/perplexity/src/perplexity-language-model.test.ts --- import { LanguageModelV2Prompt } from '@ai-sdk/provider'; import { convertReadableStreamToArray, createTestServer, mockId, } from '@ai-sdk/provider-utils/test'; import { z } from 'zod/v4'; import { perplexityImageSchema, PerplexityLanguageModel, } from './perplexity-language-model'; const TEST_PROMPT: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; describe('PerplexityLanguageModel', () => { describe('doGenerate', () => { const modelId = 'perplexity-001'; const perplexityModel = new PerplexityLanguageModel(modelId, { baseURL: 'https://api.perplexity.ai', headers: () => ({ authorization: 'Bearer test-token', 'content-type': 'application/json', }), generateId: mockId(), }); // Create a unified test server to handle JSON responses. const jsonServer = createTestServer({ 'https://api.perplexity.ai/chat/completions': { response: { type: 'json-value', headers: { 'content-type': 'application/json' }, body: {}, }, }, }); // Helper to prepare the JSON response for doGenerate. function prepareJsonResponse({ content = '', usage = { prompt_tokens: 10, completion_tokens: 20 }, id = 'test-id', created = 1680000000, model = modelId, headers = {}, citations = [], images, }: { content?: string; usage?: { prompt_tokens: number; completion_tokens: number; citation_tokens?: number; num_search_queries?: number; }; id?: string; created?: number; model?: string; headers?: Record<string, string>; citations?: string[]; images?: z.infer<typeof perplexityImageSchema>[]; } = {}) { jsonServer.urls['https://api.perplexity.ai/chat/completions'].response = { type: 'json-value', headers: { 'content-type': 'application/json', ...headers }, body: { id, created, model, choices: [ { message: { role: 'assistant', content, }, finish_reason: 'stop', }, ], citations, images, usage, }, }; } it('should extract content correctly', async () => { prepareJsonResponse({ content: 'Hello, World!' }); const result = await perplexityModel.doGenerate({ prompt: TEST_PROMPT, }); expect(result.content).toMatchInlineSnapshot(` [ { "text": "Hello, World!", "type": "text", }, ] `); expect(result.usage).toEqual({ inputTokens: 10, outputTokens: 20 }); expect({ id: result.response?.id, timestamp: result.response?.timestamp, modelId: result.response?.modelId, }).toStrictEqual({ id: 'test-id', timestamp: new Date(1680000000 * 1000), modelId, }); }); it('should send the correct request body', async () => { prepareJsonResponse({ content: '' }); await perplexityModel.doGenerate({ prompt: TEST_PROMPT, }); expect(await jsonServer.calls[0].requestBodyJson).toEqual({ model: modelId, messages: [{ role: 'user', content: 'Hello' }], }); }); it('should pass through perplexity provider options', async () => { prepareJsonResponse({ content: '' }); await perplexityModel.doGenerate({ prompt: TEST_PROMPT, providerOptions: { perplexity: { search_recency_filter: 'month', return_images: true, }, }, }); expect(await jsonServer.calls[0].requestBodyJson).toEqual({ model: modelId, messages: [{ role: 'user', content: 'Hello' }], search_recency_filter: 'month', return_images: true, }); }); it('should extract citations as sources', async () => { prepareJsonResponse({ citations: ['http://example.com/123', 'https://example.com/456'], }); const result = await perplexityModel.doGenerate({ prompt: TEST_PROMPT, }); expect(result.content).toMatchInlineSnapshot(` [ { "id": "id-0", "sourceType": "url", "type": "source", "url": "http://example.com/123", }, { "id": "id-1", "sourceType": "url", "type": "source", "url": "https://example.com/456", }, ] `); }); it('should extract images', async () => { prepareJsonResponse({ images: [ { image_url: 'https://example.com/image.jpg', origin_url: 'https://example.com/image.jpg', height: 100, width: 100, }, ], }); const result = await perplexityModel.doGenerate({ prompt: TEST_PROMPT, }); expect(result.providerMetadata).toStrictEqual({ perplexity: { images: [ { imageUrl: 'https://example.com/image.jpg', originUrl: 'https://example.com/image.jpg', height: 100, width: 100, }, ], usage: { citationTokens: null, numSearchQueries: null, }, }, }); }); it('should extract usage', async () => { prepareJsonResponse({ usage: { prompt_tokens: 10, completion_tokens: 20, citation_tokens: 30, num_search_queries: 40, }, }); const result = await perplexityModel.doGenerate({ prompt: TEST_PROMPT, }); expect(result.usage).toEqual({ inputTokens: 10, outputTokens: 20 }); expect(result.providerMetadata).toEqual({ perplexity: { images: null, usage: { citationTokens: 30, numSearchQueries: 40, }, }, }); }); it('should pass headers from provider and request', async () => { prepareJsonResponse({ content: '' }); const lmWithCustomHeaders = new PerplexityLanguageModel(modelId, { baseURL: 'https://api.perplexity.ai', headers: () => ({ authorization: 'Bearer test-api-key', 'Custom-Provider-Header': 'provider-header-value', }), generateId: mockId(), }); await lmWithCustomHeaders.doGenerate({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value' }, }); expect(jsonServer.calls[0].requestHeaders).toEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); }); describe('doStream', () => { const modelId = 'perplexity-001'; const streamServer = createTestServer({ 'https://api.perplexity.ai/chat/completions': { response: { type: 'stream-chunks', headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', }, chunks: [], }, }, }); const perplexityLM = new PerplexityLanguageModel(modelId, { baseURL: 'https://api.perplexity.ai', headers: () => ({ authorization: 'Bearer test-token' }), generateId: mockId(), }); // Helper to prepare the stream response. function prepareStreamResponse({ contents, usage = { prompt_tokens: 10, completion_tokens: 20 }, citations = [], images, }: { contents: string[]; usage?: { prompt_tokens: number; completion_tokens: number; citation_tokens?: number; num_search_queries?: number; }; citations?: string[]; images?: z.infer<typeof perplexityImageSchema>[]; }) { const baseChunk = ( content: string, finish_reason: string | null = null, includeUsage = false, ) => { const chunkObj: any = { id: 'stream-id', created: 1680003600, model: modelId, images, citations, choices: [ { delta: { role: 'assistant', content }, finish_reason, }, ], }; if (includeUsage) { chunkObj.usage = usage; } return `data: ${JSON.stringify(chunkObj)}\n\n`; }; streamServer.urls['https://api.perplexity.ai/chat/completions'].response = { type: 'stream-chunks', headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', }, chunks: [ ...contents.slice(0, -1).map(text => baseChunk(text)), // Final chunk: include finish_reason and usage. baseChunk(contents[contents.length - 1], 'stop', true), 'data: [DONE]\n\n', ], }; } it('should stream text deltas', async () => { prepareStreamResponse({ contents: ['Hello', ', ', 'World!'] }); const { stream } = await perplexityLM.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const result = await convertReadableStreamToArray(stream); expect(result).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "stream-id", "modelId": "perplexity-001", "timestamp": 2023-03-28T11:40:00.000Z, "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "delta": ", ", "id": "0", "type": "text-delta", }, { "delta": "World!", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "perplexity": { "images": null, "usage": { "citationTokens": null, "numSearchQueries": null, }, }, }, "type": "finish", "usage": { "inputTokens": 10, "outputTokens": 20, "totalTokens": undefined, }, }, ] `); }); it('should stream sources', async () => { prepareStreamResponse({ contents: ['Hello', ', ', 'World!'], citations: ['http://example.com/123', 'https://example.com/456'], }); const { stream } = await perplexityLM.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const result = await convertReadableStreamToArray(stream); expect(result).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "stream-id", "modelId": "perplexity-001", "timestamp": 2023-03-28T11:40:00.000Z, "type": "response-metadata", }, { "id": "id-0", "sourceType": "url", "type": "source", "url": "http://example.com/123", }, { "id": "id-1", "sourceType": "url", "type": "source", "url": "https://example.com/456", }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "delta": ", ", "id": "0", "type": "text-delta", }, { "delta": "World!", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "perplexity": { "images": null, "usage": { "citationTokens": null, "numSearchQueries": null, }, }, }, "type": "finish", "usage": { "inputTokens": 10, "outputTokens": 20, "totalTokens": undefined, }, }, ] `); }); it('should send the correct streaming request body', async () => { prepareStreamResponse({ contents: [] }); await perplexityLM.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await streamServer.calls[0].requestBodyJson).toEqual({ model: modelId, messages: [{ role: 'user', content: 'Hello' }], stream: true, }); }); it('should send usage', async () => { prepareStreamResponse({ contents: ['Hello', ', ', 'World!'], images: [ { image_url: 'https://example.com/image.jpg', origin_url: 'https://example.com/image.jpg', height: 100, width: 100, }, ], }); const { stream } = await perplexityLM.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const result = await convertReadableStreamToArray(stream); expect(result).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "stream-id", "modelId": "perplexity-001", "timestamp": 2023-03-28T11:40:00.000Z, "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "delta": ", ", "id": "0", "type": "text-delta", }, { "delta": "World!", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "perplexity": { "images": [ { "height": 100, "imageUrl": "https://example.com/image.jpg", "originUrl": "https://example.com/image.jpg", "width": 100, }, ], "usage": { "citationTokens": null, "numSearchQueries": null, }, }, }, "type": "finish", "usage": { "inputTokens": 10, "outputTokens": 20, "totalTokens": undefined, }, }, ] `); }); it('should send images', async () => { prepareStreamResponse({ contents: ['Hello', ', ', 'World!'], usage: { prompt_tokens: 11, completion_tokens: 21, citation_tokens: 30, num_search_queries: 40, }, }); const { stream } = await perplexityLM.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); const result = await convertReadableStreamToArray(stream); expect(result).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "stream-id", "modelId": "perplexity-001", "timestamp": 2023-03-28T11:40:00.000Z, "type": "response-metadata", }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "delta": ", ", "id": "0", "type": "text-delta", }, { "delta": "World!", "id": "0", "type": "text-delta", }, { "id": "0", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": { "perplexity": { "images": null, "usage": { "citationTokens": 30, "numSearchQueries": 40, }, }, }, "type": "finish", "usage": { "inputTokens": 11, "outputTokens": 21, "totalTokens": undefined, }, }, ] `); }); it('should pass headers', async () => { prepareStreamResponse({ contents: [] }); const lmWithCustomHeaders = new PerplexityLanguageModel(modelId, { baseURL: 'https://api.perplexity.ai', headers: () => ({ authorization: 'Bearer test-api-key', 'Custom-Provider-Header': 'provider-header-value', }), generateId: mockId(), }); await lmWithCustomHeaders.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, headers: { 'Custom-Request-Header': 'request-header-value' }, }); expect(streamServer.calls[0].requestHeaders).toEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should stream raw chunks when includeRawChunks is true', async () => { streamServer.urls['https://api.perplexity.ai/chat/completions'].response = { type: 'stream-chunks', headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', }, chunks: [ `data: {"id":"ppl-123","object":"chat.completion.chunk","created":1234567890,"model":"perplexity-001","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null}],"citations":["https://example.com"]}\n\n`, `data: {"id":"ppl-456","object":"chat.completion.chunk","created":1234567890,"model":"perplexity-001","choices":[{"index":0,"delta":{"content":" world"},"finish_reason":null}]}\n\n`, `data: {"id":"ppl-789","object":"chat.completion.chunk","created":1234567890,"model":"perplexity-001","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15,"citation_tokens":2,"num_search_queries":1}}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await perplexityLM.doStream({ prompt: TEST_PROMPT, includeRawChunks: true, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "rawValue": { "choices": [ { "delta": { "content": "Hello", "role": "assistant", }, "finish_reason": null, "index": 0, }, ], "citations": [ "https://example.com", ], "created": 1234567890, "id": "ppl-123", "model": "perplexity-001", "object": "chat.completion.chunk", }, "type": "raw", }, { "id": "ppl-123", "modelId": "perplexity-001", "timestamp": 2009-02-13T23:31:30.000Z, "type": "response-metadata", }, { "id": "id-2", "sourceType": "url", "type": "source", "url": "https://example.com", }, { "id": "0", "type": "text-start", }, { "delta": "Hello", "id": "0", "type": "text-delta", }, { "rawValue": { "choices": [ { "delta": { "content": " world", }, "finish_reason": null, "index": 0, }, ], "created": 1234567890, "id": "ppl-456", "model": "perplexity-001", "object": "chat.completion.chunk", }, "type": "raw", }, { "error": [AI_TypeValidationError: Type validation failed: Value: {"id":"ppl-456","object":"chat.completion.chunk","created":1234567890,"model":"perplexity-001","choices":[{"index":0,"delta":{"content":" world"},"finish_reason":null}]}. Error message: [{"code":"invalid_value","values":["assistant"],"path":["choices",0,"delta","role"],"message":"Invalid input: expected \\"assistant\\""}]], "type": "error", }, { "rawValue": { "choices": [ { "delta": {}, "finish_reason": "stop", "index": 0, }, ], "created": 1234567890, "id": "ppl-789", "model": "perplexity-001", "object": "chat.completion.chunk", "usage": { "citation_tokens": 2, "completion_tokens": 5, "num_search_queries": 1, "prompt_tokens": 10, "total_tokens": 15, }, }, "type": "raw", }, { "error": [AI_TypeValidationError: Type validation failed: Value: {"id":"ppl-789","object":"chat.completion.chunk","created":1234567890,"model":"perplexity-001","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15,"citation_tokens":2,"num_search_queries":1}}. Error message: [{"code":"invalid_value","values":["assistant"],"path":["choices",0,"delta","role"],"message":"Invalid input: expected \\"assistant\\""},{"expected":"string","code":"invalid_type","path":["choices",0,"delta","content"],"message":"Invalid input: expected string, received undefined"}]], "type": "error", }, { "id": "0", "type": "text-end", }, { "finishReason": "unknown", "providerMetadata": { "perplexity": { "images": null, "usage": { "citationTokens": null, "numSearchQueries": null, }, }, }, "type": "finish", "usage": { "inputTokens": undefined, "outputTokens": undefined, "totalTokens": undefined, }, }, ] `); }); }); }); --- File: /ai/packages/perplexity/src/perplexity-language-model.ts --- import { LanguageModelV2, LanguageModelV2CallWarning, LanguageModelV2Content, LanguageModelV2FinishReason, LanguageModelV2StreamPart, LanguageModelV2Usage, } from '@ai-sdk/provider'; import { FetchFunction, ParseResult, combineHeaders, createEventSourceResponseHandler, createJsonErrorResponseHandler, createJsonResponseHandler, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { convertToPerplexityMessages } from './convert-to-perplexity-messages'; import { mapPerplexityFinishReason } from './map-perplexity-finish-reason'; import { PerplexityLanguageModelId } from './perplexity-language-model-options'; type PerplexityChatConfig = { baseURL: string; headers: () => Record<string, string | undefined>; generateId: () => string; fetch?: FetchFunction; }; export class PerplexityLanguageModel implements LanguageModelV2 { readonly specificationVersion = 'v2'; readonly provider = 'perplexity'; readonly modelId: PerplexityLanguageModelId; private readonly config: PerplexityChatConfig; constructor( modelId: PerplexityLanguageModelId, config: PerplexityChatConfig, ) { this.modelId = modelId; this.config = config; } readonly supportedUrls: Record<string, RegExp[]> = { // No URLs are supported. }; private getArgs({ prompt, maxOutputTokens, temperature, topP, topK, frequencyPenalty, presencePenalty, stopSequences, responseFormat, seed, providerOptions, }: Parameters<LanguageModelV2['doGenerate']>[0]) { const warnings: LanguageModelV2CallWarning[] = []; if (topK != null) { warnings.push({ type: 'unsupported-setting', setting: 'topK', }); } if (stopSequences != null) { warnings.push({ type: 'unsupported-setting', setting: 'stopSequences', }); } if (seed != null) { warnings.push({ type: 'unsupported-setting', setting: 'seed', }); } return { args: { // model id: model: this.modelId, // standardized settings: frequency_penalty: frequencyPenalty, max_tokens: maxOutputTokens, presence_penalty: presencePenalty, temperature, top_k: topK, top_p: topP, // response format: response_format: responseFormat?.type === 'json' ? { type: 'json_schema', json_schema: { schema: responseFormat.schema }, } : undefined, // provider extensions ...(providerOptions?.perplexity ?? {}), // messages: messages: convertToPerplexityMessages(prompt), }, warnings, }; } async doGenerate( options: Parameters<LanguageModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>> { const { args: body, warnings } = this.getArgs(options); const { responseHeaders, value: response, rawValue: rawResponse, } = await postJsonToApi({ url: `${this.config.baseURL}/chat/completions`, headers: combineHeaders(this.config.headers(), options.headers), body, failedResponseHandler: createJsonErrorResponseHandler({ errorSchema: perplexityErrorSchema, errorToMessage, }), successfulResponseHandler: createJsonResponseHandler( perplexityResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const choice = response.choices[0]; const content: Array<LanguageModelV2Content> = []; // text content: const text = choice.message.content; if (text.length > 0) { content.push({ type: 'text', text }); } // sources: if (response.citations != null) { for (const url of response.citations) { content.push({ type: 'source', sourceType: 'url', id: this.config.generateId(), url, }); } } return { content, finishReason: mapPerplexityFinishReason(choice.finish_reason), usage: { inputTokens: response.usage?.prompt_tokens, outputTokens: response.usage?.completion_tokens, totalTokens: response.usage?.total_tokens ?? undefined, }, request: { body }, response: { ...getResponseMetadata(response), headers: responseHeaders, body: rawResponse, }, warnings, providerMetadata: { perplexity: { images: response.images?.map(image => ({ imageUrl: image.image_url, originUrl: image.origin_url, height: image.height, width: image.width, })) ?? null, usage: { citationTokens: response.usage?.citation_tokens ?? null, numSearchQueries: response.usage?.num_search_queries ?? null, }, }, }, }; } async doStream( options: Parameters<LanguageModelV2['doStream']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>> { const { args, warnings } = this.getArgs(options); const body = { ...args, stream: true }; const { responseHeaders, value: response } = await postJsonToApi({ url: `${this.config.baseURL}/chat/completions`, headers: combineHeaders(this.config.headers(), options.headers), body, failedResponseHandler: createJsonErrorResponseHandler({ errorSchema: perplexityErrorSchema, errorToMessage, }), successfulResponseHandler: createEventSourceResponseHandler( perplexityChunkSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); let finishReason: LanguageModelV2FinishReason = 'unknown'; const usage: LanguageModelV2Usage = { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, }; const providerMetadata: { perplexity: { usage: { citationTokens: number | null; numSearchQueries: number | null; }; images: Array<{ imageUrl: string; originUrl: string; height: number; width: number; }> | null; }; } = { perplexity: { usage: { citationTokens: null, numSearchQueries: null, }, images: null, }, }; let isFirstChunk = true; let isActive = false; const self = this; return { stream: response.pipeThrough( new TransformStream< ParseResult<z.infer<typeof perplexityChunkSchema>>, LanguageModelV2StreamPart >({ start(controller) { controller.enqueue({ type: 'stream-start', warnings }); }, transform(chunk, controller) { // Emit raw chunk if requested (before anything else) if (options.includeRawChunks) { controller.enqueue({ type: 'raw', rawValue: chunk.rawValue }); } if (!chunk.success) { controller.enqueue({ type: 'error', error: chunk.error }); return; } const value = chunk.value; if (isFirstChunk) { controller.enqueue({ type: 'response-metadata', ...getResponseMetadata(value), }); value.citations?.forEach(url => { controller.enqueue({ type: 'source', sourceType: 'url', id: self.config.generateId(), url, }); }); isFirstChunk = false; } if (value.usage != null) { usage.inputTokens = value.usage.prompt_tokens; usage.outputTokens = value.usage.completion_tokens; providerMetadata.perplexity.usage = { citationTokens: value.usage.citation_tokens ?? null, numSearchQueries: value.usage.num_search_queries ?? null, }; } if (value.images != null) { providerMetadata.perplexity.images = value.images.map(image => ({ imageUrl: image.image_url, originUrl: image.origin_url, height: image.height, width: image.width, })); } const choice = value.choices[0]; if (choice?.finish_reason != null) { finishReason = mapPerplexityFinishReason(choice.finish_reason); } if (choice?.delta == null) { return; } const delta = choice.delta; const textContent = delta.content; if (textContent != null) { if (!isActive) { controller.enqueue({ type: 'text-start', id: '0' }); isActive = true; } controller.enqueue({ type: 'text-delta', id: '0', delta: textContent, }); } }, flush(controller) { if (isActive) { controller.enqueue({ type: 'text-end', id: '0' }); } controller.enqueue({ type: 'finish', finishReason, usage, providerMetadata, }); }, }), ), request: { body }, response: { headers: responseHeaders }, }; } } function getResponseMetadata({ id, model, created, }: { id: string; created: number; model: string; }) { return { id, modelId: model, timestamp: new Date(created * 1000), }; } const perplexityUsageSchema = z.object({ prompt_tokens: z.number(), completion_tokens: z.number(), total_tokens: z.number().nullish(), citation_tokens: z.number().nullish(), num_search_queries: z.number().nullish(), }); export const perplexityImageSchema = z.object({ image_url: z.string(), origin_url: z.string(), height: z.number(), width: z.number(), }); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const perplexityResponseSchema = z.object({ id: z.string(), created: z.number(), model: z.string(), choices: z.array( z.object({ message: z.object({ role: z.literal('assistant'), content: z.string(), }), finish_reason: z.string().nullish(), }), ), citations: z.array(z.string()).nullish(), images: z.array(perplexityImageSchema).nullish(), usage: perplexityUsageSchema.nullish(), }); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const perplexityChunkSchema = z.object({ id: z.string(), created: z.number(), model: z.string(), choices: z.array( z.object({ delta: z.object({ role: z.literal('assistant'), content: z.string(), }), finish_reason: z.string().nullish(), }), ), citations: z.array(z.string()).nullish(), images: z.array(perplexityImageSchema).nullish(), usage: perplexityUsageSchema.nullish(), }); export const perplexityErrorSchema = z.object({ error: z.object({ code: z.number(), message: z.string().nullish(), type: z.string().nullish(), }), }); export type PerplexityErrorData = z.infer<typeof perplexityErrorSchema>; const errorToMessage = (data: PerplexityErrorData) => { return data.error.message ?? data.error.type ?? 'unknown error'; }; --- File: /ai/packages/perplexity/src/perplexity-provider.ts --- import { LanguageModelV2, NoSuchModelError, ProviderV2, } from '@ai-sdk/provider'; import { FetchFunction, generateId, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { PerplexityLanguageModel } from './perplexity-language-model'; import { PerplexityLanguageModelId } from './perplexity-language-model-options'; export interface PerplexityProvider extends ProviderV2 { /** Creates an Perplexity chat model for text generation. */ (modelId: PerplexityLanguageModelId): LanguageModelV2; /** Creates an Perplexity language model for text generation. */ languageModel(modelId: PerplexityLanguageModelId): LanguageModelV2; } export interface PerplexityProviderSettings { /** Base URL for the perplexity API calls. */ baseURL?: string; /** API key for authenticating requests. */ apiKey?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } export function createPerplexity( options: PerplexityProviderSettings = {}, ): PerplexityProvider { const getHeaders = () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'PERPLEXITY_API_KEY', description: 'Perplexity', })}`, ...options.headers, }); const createLanguageModel = (modelId: PerplexityLanguageModelId) => { return new PerplexityLanguageModel(modelId, { baseURL: withoutTrailingSlash( options.baseURL ?? 'https://api.perplexity.ai', )!, headers: getHeaders, generateId, fetch: options.fetch, }); }; const provider = (modelId: PerplexityLanguageModelId) => createLanguageModel(modelId); provider.languageModel = createLanguageModel; provider.textEmbeddingModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'textEmbeddingModel' }); }; provider.imageModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'imageModel' }); }; return provider; } export const perplexity = createPerplexity(); --- File: /ai/packages/perplexity/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/perplexity/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/perplexity/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/provider/src/embedding-model/v2/embedding-model-v2-embedding.ts --- /** An embedding is a vector, i.e. an array of numbers. It is e.g. used to represent a text as a vector of word embeddings. */ export type EmbeddingModelV2Embedding = Array<number>; --- File: /ai/packages/provider/src/embedding-model/v2/embedding-model-v2.ts --- import { SharedV2Headers, SharedV2ProviderOptions, SharedV2ProviderMetadata, } from '../../shared'; import { EmbeddingModelV2Embedding } from './embedding-model-v2-embedding'; /** Specification for an embedding model that implements the embedding model interface version 1. VALUE is the type of the values that the model can embed. This will allow us to go beyond text embeddings in the future, e.g. to support image embeddings */ export type EmbeddingModelV2<VALUE> = { /** The embedding model must specify which embedding model interface version it implements. This will allow us to evolve the embedding model interface and retain backwards compatibility. The different implementation versions can be handled as a discriminated union on our side. */ readonly specificationVersion: 'v2'; /** Name of the provider for logging purposes. */ readonly provider: string; /** Provider-specific model ID for logging purposes. */ readonly modelId: string; /** Limit of how many embeddings can be generated in a single API call. Use Infinity for models that do not have a limit. */ readonly maxEmbeddingsPerCall: | PromiseLike<number | undefined> | number | undefined; /** True if the model can handle multiple embedding calls in parallel. */ readonly supportsParallelCalls: PromiseLike<boolean> | boolean; /** Generates a list of embeddings for the given input text. Naming: "do" prefix to prevent accidental direct usage of the method by the user. */ doEmbed(options: { /** List of values to embed. */ values: Array<VALUE>; /** Abort signal for cancelling the operation. */ abortSignal?: AbortSignal; /** Additional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: SharedV2ProviderOptions; /** Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. */ headers?: Record<string, string | undefined>; }): PromiseLike<{ /** Generated embeddings. They are in the same order as the input values. */ embeddings: Array<EmbeddingModelV2Embedding>; /** Token usage. We only have input tokens for embeddings. */ usage?: { tokens: number }; /** Additional provider-specific metadata. They are passed through from the provider to the AI SDK and enable provider-specific results that can be fully encapsulated in the provider. */ providerMetadata?: SharedV2ProviderMetadata; /** Optional response information for debugging purposes. */ response?: { /** Response headers. */ headers?: SharedV2Headers; /** The response body. */ body?: unknown; }; }>; }; --- File: /ai/packages/provider/src/embedding-model/v2/index.ts --- export * from './embedding-model-v2'; export * from './embedding-model-v2-embedding'; --- File: /ai/packages/provider/src/embedding-model/index.ts --- export * from './v2/index'; --- File: /ai/packages/provider/src/errors/ai-sdk-error.ts --- /** * Symbol used for identifying AI SDK Error instances. * Enables checking if an error is an instance of AISDKError across package versions. */ const marker = 'vercel.ai.error'; const symbol = Symbol.for(marker); /** * Custom error class for AI SDK related errors. * @extends Error */ export class AISDKError extends Error { private readonly [symbol] = true; // used in isInstance /** * The underlying cause of the error, if any. */ readonly cause?: unknown; /** * Creates an AI SDK Error. * * @param {Object} params - The parameters for creating the error. * @param {string} params.name - The name of the error. * @param {string} params.message - The error message. * @param {unknown} [params.cause] - The underlying cause of the error. */ constructor({ name, message, cause, }: { name: string; message: string; cause?: unknown; }) { super(message); this.name = name; this.cause = cause; } /** * Checks if the given error is an AI SDK Error. * @param {unknown} error - The error to check. * @returns {boolean} True if the error is an AI SDK Error, false otherwise. */ static isInstance(error: unknown): error is AISDKError { return AISDKError.hasMarker(error, marker); } protected static hasMarker(error: unknown, marker: string): boolean { const markerSymbol = Symbol.for(marker); return ( error != null && typeof error === 'object' && markerSymbol in error && typeof error[markerSymbol] === 'boolean' && error[markerSymbol] === true ); } } --- File: /ai/packages/provider/src/errors/api-call-error.ts --- import { AISDKError } from './ai-sdk-error'; const name = 'AI_APICallError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class APICallError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly url: string; readonly requestBodyValues: unknown; readonly statusCode?: number; readonly responseHeaders?: Record<string, string>; readonly responseBody?: string; readonly isRetryable: boolean; readonly data?: unknown; constructor({ message, url, requestBodyValues, statusCode, responseHeaders, responseBody, cause, isRetryable = statusCode != null && (statusCode === 408 || // request timeout statusCode === 409 || // conflict statusCode === 429 || // too many requests statusCode >= 500), // server error data, }: { message: string; url: string; requestBodyValues: unknown; statusCode?: number; responseHeaders?: Record<string, string>; responseBody?: string; cause?: unknown; isRetryable?: boolean; data?: unknown; }) { super({ name, message, cause }); this.url = url; this.requestBodyValues = requestBodyValues; this.statusCode = statusCode; this.responseHeaders = responseHeaders; this.responseBody = responseBody; this.isRetryable = isRetryable; this.data = data; } static isInstance(error: unknown): error is APICallError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/provider/src/errors/empty-response-body-error.ts --- import { AISDKError } from './ai-sdk-error'; const name = 'AI_EmptyResponseBodyError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class EmptyResponseBodyError extends AISDKError { private readonly [symbol] = true; // used in isInstance constructor({ message = 'Empty response body' }: { message?: string } = {}) { super({ name, message }); } static isInstance(error: unknown): error is EmptyResponseBodyError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/provider/src/errors/get-error-message.ts --- export function getErrorMessage(error: unknown | undefined) { if (error == null) { return 'unknown error'; } if (typeof error === 'string') { return error; } if (error instanceof Error) { return error.message; } return JSON.stringify(error); } --- File: /ai/packages/provider/src/errors/index.ts --- export { AISDKError } from './ai-sdk-error'; export { APICallError } from './api-call-error'; export { EmptyResponseBodyError } from './empty-response-body-error'; export { getErrorMessage } from './get-error-message'; export { InvalidArgumentError } from './invalid-argument-error'; export { InvalidPromptError } from './invalid-prompt-error'; export { InvalidResponseDataError } from './invalid-response-data-error'; export { JSONParseError } from './json-parse-error'; export { LoadAPIKeyError } from './load-api-key-error'; export { LoadSettingError } from './load-setting-error'; export { NoContentGeneratedError } from './no-content-generated-error'; export { NoSuchModelError } from './no-such-model-error'; export { TooManyEmbeddingValuesForCallError } from './too-many-embedding-values-for-call-error'; export { TypeValidationError } from './type-validation-error'; export { UnsupportedFunctionalityError } from './unsupported-functionality-error'; --- File: /ai/packages/provider/src/errors/invalid-argument-error.ts --- import { AISDKError } from './ai-sdk-error'; const name = 'AI_InvalidArgumentError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); /** * A function argument is invalid. */ export class InvalidArgumentError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly argument: string; constructor({ message, cause, argument, }: { argument: string; message: string; cause?: unknown; }) { super({ name, message, cause }); this.argument = argument; } static isInstance(error: unknown): error is InvalidArgumentError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/provider/src/errors/invalid-prompt-error.ts --- import { AISDKError } from './ai-sdk-error'; const name = 'AI_InvalidPromptError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); /** * A prompt is invalid. This error should be thrown by providers when they cannot * process a prompt. */ export class InvalidPromptError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly prompt: unknown; constructor({ prompt, message, cause, }: { prompt: unknown; message: string; cause?: unknown; }) { super({ name, message: `Invalid prompt: ${message}`, cause }); this.prompt = prompt; } static isInstance(error: unknown): error is InvalidPromptError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/provider/src/errors/invalid-response-data-error.ts --- import { AISDKError } from './ai-sdk-error'; const name = 'AI_InvalidResponseDataError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); /** * Server returned a response with invalid data content. * This should be thrown by providers when they cannot parse the response from the API. */ export class InvalidResponseDataError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly data: unknown; constructor({ data, message = `Invalid response data: ${JSON.stringify(data)}.`, }: { data: unknown; message?: string; }) { super({ name, message }); this.data = data; } static isInstance(error: unknown): error is InvalidResponseDataError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/provider/src/errors/json-parse-error.ts --- import { AISDKError } from './ai-sdk-error'; import { getErrorMessage } from './get-error-message'; const name = 'AI_JSONParseError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); // TODO v5: rename to ParseError export class JSONParseError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly text: string; constructor({ text, cause }: { text: string; cause: unknown }) { super({ name, message: `JSON parsing failed: ` + `Text: ${text}.\n` + `Error message: ${getErrorMessage(cause)}`, cause, }); this.text = text; } static isInstance(error: unknown): error is JSONParseError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/provider/src/errors/load-api-key-error.ts --- import { AISDKError } from './ai-sdk-error'; const name = 'AI_LoadAPIKeyError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class LoadAPIKeyError extends AISDKError { private readonly [symbol] = true; // used in isInstance constructor({ message }: { message: string }) { super({ name, message }); } static isInstance(error: unknown): error is LoadAPIKeyError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/provider/src/errors/load-setting-error.ts --- import { AISDKError } from './ai-sdk-error'; const name = 'AI_LoadSettingError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class LoadSettingError extends AISDKError { private readonly [symbol] = true; // used in isInstance constructor({ message }: { message: string }) { super({ name, message }); } static isInstance(error: unknown): error is LoadSettingError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/provider/src/errors/no-content-generated-error.ts --- import { AISDKError } from './ai-sdk-error'; const name = 'AI_NoContentGeneratedError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); /** Thrown when the AI provider fails to generate any content. */ export class NoContentGeneratedError extends AISDKError { private readonly [symbol] = true; // used in isInstance constructor({ message = 'No content generated.', }: { message?: string } = {}) { super({ name, message }); } static isInstance(error: unknown): error is NoContentGeneratedError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/provider/src/errors/no-such-model-error.ts --- import { AISDKError } from './ai-sdk-error'; const name = 'AI_NoSuchModelError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class NoSuchModelError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly modelId: string; readonly modelType: | 'languageModel' | 'textEmbeddingModel' | 'imageModel' | 'transcriptionModel' | 'speechModel'; constructor({ errorName = name, modelId, modelType, message = `No such ${modelType}: ${modelId}`, }: { errorName?: string; modelId: string; modelType: | 'languageModel' | 'textEmbeddingModel' | 'imageModel' | 'transcriptionModel' | 'speechModel'; message?: string; }) { super({ name: errorName, message }); this.modelId = modelId; this.modelType = modelType; } static isInstance(error: unknown): error is NoSuchModelError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/provider/src/errors/too-many-embedding-values-for-call-error.ts --- import { AISDKError } from './ai-sdk-error'; const name = 'AI_TooManyEmbeddingValuesForCallError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class TooManyEmbeddingValuesForCallError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly provider: string; readonly modelId: string; readonly maxEmbeddingsPerCall: number; readonly values: Array<unknown>; constructor(options: { provider: string; modelId: string; maxEmbeddingsPerCall: number; values: Array<unknown>; }) { super({ name, message: `Too many values for a single embedding call. ` + `The ${options.provider} model "${options.modelId}" can only embed up to ` + `${options.maxEmbeddingsPerCall} values per call, but ${options.values.length} values were provided.`, }); this.provider = options.provider; this.modelId = options.modelId; this.maxEmbeddingsPerCall = options.maxEmbeddingsPerCall; this.values = options.values; } static isInstance( error: unknown, ): error is TooManyEmbeddingValuesForCallError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/provider/src/errors/type-validation-error.ts --- import { AISDKError } from './ai-sdk-error'; import { getErrorMessage } from './get-error-message'; const name = 'AI_TypeValidationError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class TypeValidationError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly value: unknown; constructor({ value, cause }: { value: unknown; cause: unknown }) { super({ name, message: `Type validation failed: ` + `Value: ${JSON.stringify(value)}.\n` + `Error message: ${getErrorMessage(cause)}`, cause, }); this.value = value; } static isInstance(error: unknown): error is TypeValidationError { return AISDKError.hasMarker(error, marker); } /** * Wraps an error into a TypeValidationError. * If the cause is already a TypeValidationError with the same value, it returns the cause. * Otherwise, it creates a new TypeValidationError. * * @param {Object} params - The parameters for wrapping the error. * @param {unknown} params.value - The value that failed validation. * @param {unknown} params.cause - The original error or cause of the validation failure. * @returns {TypeValidationError} A TypeValidationError instance. */ static wrap({ value, cause, }: { value: unknown; cause: unknown; }): TypeValidationError { return TypeValidationError.isInstance(cause) && cause.value === value ? cause : new TypeValidationError({ value, cause }); } } --- File: /ai/packages/provider/src/errors/unsupported-functionality-error.ts --- import { AISDKError } from './ai-sdk-error'; const name = 'AI_UnsupportedFunctionalityError'; const marker = `vercel.ai.error.${name}`; const symbol = Symbol.for(marker); export class UnsupportedFunctionalityError extends AISDKError { private readonly [symbol] = true; // used in isInstance readonly functionality: string; constructor({ functionality, message = `'${functionality}' functionality not supported.`, }: { functionality: string; message?: string; }) { super({ name, message }); this.functionality = functionality; } static isInstance(error: unknown): error is UnsupportedFunctionalityError { return AISDKError.hasMarker(error, marker); } } --- File: /ai/packages/provider/src/image-model/v2/image-model-v2-call-options.ts --- import { SharedV2ProviderOptions } from '../../shared'; export type ImageModelV2CallOptions = { /** Prompt for the image generation. */ prompt: string; /** Number of images to generate. */ n: number; /** Size of the images to generate. Must have the format `{width}x{height}`. `undefined` will use the provider's default size. */ size: `${number}x${number}` | undefined; /** Aspect ratio of the images to generate. Must have the format `{width}:{height}`. `undefined` will use the provider's default aspect ratio. */ aspectRatio: `${number}:${number}` | undefined; /** Seed for the image generation. `undefined` will use the provider's default seed. */ seed: number | undefined; /** Additional provider-specific options that are passed through to the provider as body parameters. The outer record is keyed by the provider name, and the inner record is keyed by the provider-specific metadata key. ```ts { "openai": { "style": "vivid" } } ``` */ providerOptions: SharedV2ProviderOptions; /** Abort signal for cancelling the operation. */ abortSignal?: AbortSignal; /** Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. */ headers?: Record<string, string | undefined>; }; --- File: /ai/packages/provider/src/image-model/v2/image-model-v2-call-warning.ts --- import { ImageModelV2CallOptions } from './image-model-v2-call-options'; /** Warning from the model provider for this call. The call will proceed, but e.g. some settings might not be supported, which can lead to suboptimal results. */ export type ImageModelV2CallWarning = | { type: 'unsupported-setting'; setting: keyof ImageModelV2CallOptions; details?: string; } | { type: 'other'; message: string; }; --- File: /ai/packages/provider/src/image-model/v2/image-model-v2.ts --- import { JSONArray, JSONValue } from '../../json-value'; import { ImageModelV2CallOptions } from './image-model-v2-call-options'; import { ImageModelV2CallWarning } from './image-model-v2-call-warning'; export type ImageModelV2ProviderMetadata = Record< string, { images: JSONArray; } & JSONValue >; type GetMaxImagesPerCallFunction = (options: { modelId: string; }) => PromiseLike<number | undefined> | number | undefined; /** Image generation model specification version 2. */ export type ImageModelV2 = { /** The image model must specify which image model interface version it implements. This will allow us to evolve the image model interface and retain backwards compatibility. The different implementation versions can be handled as a discriminated union on our side. */ readonly specificationVersion: 'v2'; /** Name of the provider for logging purposes. */ readonly provider: string; /** Provider-specific model ID for logging purposes. */ readonly modelId: string; /** Limit of how many images can be generated in a single API call. Can be set to a number for a fixed limit, to undefined to use the global limit, or a function that returns a number or undefined, optionally as a promise. */ readonly maxImagesPerCall: number | undefined | GetMaxImagesPerCallFunction; /** Generates an array of images. */ doGenerate(options: ImageModelV2CallOptions): PromiseLike<{ /** Generated images as base64 encoded strings or binary data. The images should be returned without any unnecessary conversion. If the API returns base64 encoded strings, the images should be returned as base64 encoded strings. If the API returns binary data, the images should be returned as binary data. */ images: Array<string> | Array<Uint8Array>; /** Warnings for the call, e.g. unsupported settings. */ warnings: Array<ImageModelV2CallWarning>; /** Additional provider-specific metadata. They are passed through from the provider to the AI SDK and enable provider-specific results that can be fully encapsulated in the provider. The outer record is keyed by the provider name, and the inner record is provider-specific metadata. It always includes an `images` key with image-specific metadata ```ts { "openai": { "images": ["revisedPrompt": "Revised prompt here."] } } ``` */ providerMetadata?: ImageModelV2ProviderMetadata; /** Response information for telemetry and debugging purposes. */ response: { /** Timestamp for the start of the generated response. */ timestamp: Date; /** The ID of the response model that was used to generate the response. */ modelId: string; /** Response headers. */ headers: Record<string, string> | undefined; }; }>; }; --- File: /ai/packages/provider/src/image-model/v2/index.ts --- export type { ImageModelV2, ImageModelV2ProviderMetadata, } from './image-model-v2'; export type { ImageModelV2CallOptions } from './image-model-v2-call-options'; export type { ImageModelV2CallWarning } from './image-model-v2-call-warning'; --- File: /ai/packages/provider/src/image-model/index.ts --- export * from './v2/index'; --- File: /ai/packages/provider/src/json-value/index.ts --- export { isJSONArray, isJSONObject, isJSONValue } from './is-json'; export type { JSONArray, JSONObject, JSONValue } from './json-value'; --- File: /ai/packages/provider/src/json-value/is-json.ts --- import { JSONArray, JSONObject, JSONValue } from './json-value'; export function isJSONValue(value: unknown): value is JSONValue { if ( value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ) { return true; } if (Array.isArray(value)) { return value.every(isJSONValue); } if (typeof value === 'object') { return Object.entries(value).every( ([key, val]) => typeof key === 'string' && isJSONValue(val), ); } return false; } export function isJSONArray(value: unknown): value is JSONArray { return Array.isArray(value) && value.every(isJSONValue); } export function isJSONObject(value: unknown): value is JSONObject { return ( value != null && typeof value === 'object' && Object.entries(value).every( ([key, val]) => typeof key === 'string' && isJSONValue(val), ) ); } --- File: /ai/packages/provider/src/json-value/json-value.ts --- /** A JSON value can be a string, number, boolean, object, array, or null. JSON values can be serialized and deserialized by the JSON.stringify and JSON.parse methods. */ export type JSONValue = | null | string | number | boolean | JSONObject | JSONArray; export type JSONObject = { [key: string]: JSONValue; }; export type JSONArray = JSONValue[]; --- File: /ai/packages/provider/src/language-model/v2/index.ts --- export * from './language-model-v2'; export * from './language-model-v2-call-options'; export * from './language-model-v2-call-warning'; export * from './language-model-v2-content'; export * from './language-model-v2-data-content'; export * from './language-model-v2-file'; export * from './language-model-v2-finish-reason'; export * from './language-model-v2-function-tool'; export * from './language-model-v2-prompt'; export * from './language-model-v2-provider-defined-tool'; export * from './language-model-v2-reasoning'; export * from './language-model-v2-response-metadata'; export * from './language-model-v2-source'; export * from './language-model-v2-stream-part'; export * from './language-model-v2-text'; export * from './language-model-v2-tool-call'; export * from './language-model-v2-tool-choice'; export * from './language-model-v2-usage'; --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-call-options.ts --- import { JSONSchema7 } from 'json-schema'; import { SharedV2ProviderOptions } from '../../shared/v2/shared-v2-provider-options'; import { LanguageModelV2FunctionTool } from './language-model-v2-function-tool'; import { LanguageModelV2Prompt } from './language-model-v2-prompt'; import { LanguageModelV2ProviderDefinedTool } from './language-model-v2-provider-defined-tool'; import { LanguageModelV2ToolChoice } from './language-model-v2-tool-choice'; export type LanguageModelV2CallOptions = { /** A language mode prompt is a standardized prompt type. Note: This is **not** the user-facing prompt. The AI SDK methods will map the user-facing prompt types such as chat or instruction prompts to this format. That approach allows us to evolve the user facing prompts without breaking the language model interface. */ prompt: LanguageModelV2Prompt; /** Maximum number of tokens to generate. */ maxOutputTokens?: number; /** Temperature setting. The range depends on the provider and model. */ temperature?: number; /** Stop sequences. If set, the model will stop generating text when one of the stop sequences is generated. Providers may have limits on the number of stop sequences. */ stopSequences?: string[]; /** Nucleus sampling. */ topP?: number; /** Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature. */ topK?: number; /** Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. */ presencePenalty?: number; /** Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. */ frequencyPenalty?: number; /** Response format. The output can either be text or JSON. Default is text. If JSON is selected, a schema can optionally be provided to guide the LLM. */ responseFormat?: | { type: 'text' } | { type: 'json'; /** * JSON schema that the generated output should conform to. */ schema?: JSONSchema7; /** * Name of output that should be generated. Used by some providers for additional LLM guidance. */ name?: string; /** * Description of the output that should be generated. Used by some providers for additional LLM guidance. */ description?: string; }; /** The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results. */ seed?: number; /** The tools that are available for the model. */ tools?: Array< LanguageModelV2FunctionTool | LanguageModelV2ProviderDefinedTool >; /** Specifies how the tool should be selected. Defaults to 'auto'. */ toolChoice?: LanguageModelV2ToolChoice; /** Include raw chunks in the stream. Only applicable for streaming calls. */ includeRawChunks?: boolean; /** Abort signal for cancelling the operation. */ abortSignal?: AbortSignal; /** Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. */ headers?: Record<string, string | undefined>; /** * Additional provider-specific options. They are passed through * to the provider from the AI SDK and enable provider-specific * functionality that can be fully encapsulated in the provider. */ providerOptions?: SharedV2ProviderOptions; }; --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-call-warning.ts --- import { LanguageModelV2CallOptions } from './language-model-v2-call-options'; import { LanguageModelV2FunctionTool } from './language-model-v2-function-tool'; import { LanguageModelV2ProviderDefinedTool } from './language-model-v2-provider-defined-tool'; /** Warning from the model provider for this call. The call will proceed, but e.g. some settings might not be supported, which can lead to suboptimal results. */ export type LanguageModelV2CallWarning = | { type: 'unsupported-setting'; setting: Omit<keyof LanguageModelV2CallOptions, 'prompt'>; details?: string; } | { type: 'unsupported-tool'; tool: LanguageModelV2FunctionTool | LanguageModelV2ProviderDefinedTool; details?: string; } | { type: 'other'; message: string; }; --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-content.ts --- import { LanguageModelV2File } from './language-model-v2-file'; import { LanguageModelV2Reasoning } from './language-model-v2-reasoning'; import { LanguageModelV2Source } from './language-model-v2-source'; import { LanguageModelV2Text } from './language-model-v2-text'; import { LanguageModelV2ToolCall } from './language-model-v2-tool-call'; import { LanguageModelV2ToolResult } from './language-model-v2-tool-result'; export type LanguageModelV2Content = | LanguageModelV2Text | LanguageModelV2Reasoning | LanguageModelV2File | LanguageModelV2Source | LanguageModelV2ToolCall | LanguageModelV2ToolResult; --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-data-content.ts --- /** Data content. Can be a Uint8Array, base64 encoded data as a string or a URL. */ export type LanguageModelV2DataContent = Uint8Array | string | URL; --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-file.ts --- /** A file that has been generated by the model. Generated files as base64 encoded strings or binary data. The files should be returned without any unnecessary conversion. */ export type LanguageModelV2File = { type: 'file'; /** The IANA media type of the file, e.g. `image/png` or `audio/mp3`. @see https://www.iana.org/assignments/media-types/media-types.xhtml */ mediaType: string; /** Generated file data as base64 encoded strings or binary data. The file data should be returned without any unnecessary conversion. If the API returns base64 encoded strings, the file data should be returned as base64 encoded strings. If the API returns binary data, the file data should be returned as binary data. */ data: string | Uint8Array; }; --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-finish-reason.ts --- /** Reason why a language model finished generating a response. Can be one of the following: - `stop`: model generated stop sequence - `length`: model generated maximum number of tokens - `content-filter`: content filter violation stopped the model - `tool-calls`: model triggered tool calls - `error`: model stopped because of an error - `other`: model stopped for other reasons - `unknown`: the model has not transmitted a finish reason */ export type LanguageModelV2FinishReason = | 'stop' // model generated stop sequence | 'length' // model generated maximum number of tokens | 'content-filter' // content filter violation stopped the model | 'tool-calls' // model triggered tool calls | 'error' // model stopped because of an error | 'other' // model stopped for other reasons | 'unknown'; // the model has not transmitted a finish reason --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-function-tool.ts --- import { JSONSchema7 } from 'json-schema'; import { SharedV2ProviderOptions } from '../../shared'; /** A tool has a name, a description, and a set of parameters. Note: this is **not** the user-facing tool definition. The AI SDK methods will map the user-facing tool definitions to this format. */ export type LanguageModelV2FunctionTool = { /** The type of the tool (always 'function'). */ type: 'function'; /** The name of the tool. Unique within this model call. */ name: string; /** A description of the tool. The language model uses this to understand the tool's purpose and to provide better completion suggestions. */ description?: string; /** The parameters that the tool expects. The language model uses this to understand the tool's input requirements and to provide matching suggestions. */ inputSchema: JSONSchema7; /** The provider-specific options for the tool. */ providerOptions?: SharedV2ProviderOptions; }; --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-prompt.ts --- import { JSONValue } from '../../json-value/json-value'; import { SharedV2ProviderOptions } from '../../shared/v2/shared-v2-provider-options'; import { LanguageModelV2DataContent } from './language-model-v2-data-content'; /** A prompt is a list of messages. Note: Not all models and prompt formats support multi-modal inputs and tool calls. The validation happens at runtime. Note: This is not a user-facing prompt. The AI SDK methods will map the user-facing prompt types such as chat or instruction prompts to this format. */ export type LanguageModelV2Prompt = Array<LanguageModelV2Message>; export type LanguageModelV2Message = // Note: there could be additional parts for each role in the future, // e.g. when the assistant can return images or the user can share files // such as PDFs. ( | { role: 'system'; content: string; } | { role: 'user'; content: Array<LanguageModelV2TextPart | LanguageModelV2FilePart>; } | { role: 'assistant'; content: Array< | LanguageModelV2TextPart | LanguageModelV2FilePart | LanguageModelV2ReasoningPart | LanguageModelV2ToolCallPart | LanguageModelV2ToolResultPart >; } | { role: 'tool'; content: Array<LanguageModelV2ToolResultPart>; } ) & { /** * Additional provider-specific options. They are passed through * to the provider from the AI SDK and enable provider-specific * functionality that can be fully encapsulated in the provider. */ providerOptions?: SharedV2ProviderOptions; }; /** Text content part of a prompt. It contains a string of text. */ export interface LanguageModelV2TextPart { type: 'text'; /** The text content. */ text: string; /** * Additional provider-specific options. They are passed through * to the provider from the AI SDK and enable provider-specific * functionality that can be fully encapsulated in the provider. */ providerOptions?: SharedV2ProviderOptions; } /** Reasoning content part of a prompt. It contains a string of reasoning text. */ export interface LanguageModelV2ReasoningPart { type: 'reasoning'; /** The reasoning text. */ text: string; /** * Additional provider-specific options. They are passed through * to the provider from the AI SDK and enable provider-specific * functionality that can be fully encapsulated in the provider. */ providerOptions?: SharedV2ProviderOptions; } /** File content part of a prompt. It contains a file. */ export interface LanguageModelV2FilePart { type: 'file'; /** * Optional filename of the file. */ filename?: string; /** File data. Can be a Uint8Array, base64 encoded data as a string or a URL. */ data: LanguageModelV2DataContent; /** IANA media type of the file. Can support wildcards, e.g. `image/*` (in which case the provider needs to take appropriate action). @see https://www.iana.org/assignments/media-types/media-types.xhtml */ mediaType: string; /** * Additional provider-specific options. They are passed through * to the provider from the AI SDK and enable provider-specific * functionality that can be fully encapsulated in the provider. */ providerOptions?: SharedV2ProviderOptions; } /** Tool call content part of a prompt. It contains a tool call (usually generated by the AI model). */ export interface LanguageModelV2ToolCallPart { type: 'tool-call'; /** ID of the tool call. This ID is used to match the tool call with the tool result. */ toolCallId: string; /** Name of the tool that is being called. */ toolName: string; /** Arguments of the tool call. This is a JSON-serializable object that matches the tool's input schema. */ input: unknown; /** * Whether the tool call will be executed by the provider. * If this flag is not set or is false, the tool call will be executed by the client. */ providerExecuted?: boolean; /** * Additional provider-specific options. They are passed through * to the provider from the AI SDK and enable provider-specific * functionality that can be fully encapsulated in the provider. */ providerOptions?: SharedV2ProviderOptions; } /** Tool result content part of a prompt. It contains the result of the tool call with the matching ID. */ export interface LanguageModelV2ToolResultPart { type: 'tool-result'; /** ID of the tool call that this result is associated with. */ toolCallId: string; /** Name of the tool that generated this result. */ toolName: string; /** Result of the tool call. */ output: LanguageModelV2ToolResultOutput; /** * Additional provider-specific options. They are passed through * to the provider from the AI SDK and enable provider-specific * functionality that can be fully encapsulated in the provider. */ providerOptions?: SharedV2ProviderOptions; } export type LanguageModelV2ToolResultOutput = | { type: 'text'; value: string } | { type: 'json'; value: JSONValue } | { type: 'error-text'; value: string } | { type: 'error-json'; value: JSONValue } | { type: 'content'; value: Array< | { type: 'text'; /** Text content. */ text: string; } | { type: 'media'; /** Base-64 encoded media data. */ data: string; /** IANA media type. @see https://www.iana.org/assignments/media-types/media-types.xhtml */ mediaType: string; } >; }; --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-provider-defined-tool.ts --- /** The configuration of a tool that is defined by the provider. */ export type LanguageModelV2ProviderDefinedTool = { /** The type of the tool (always 'provider-defined'). */ type: 'provider-defined'; /** The ID of the tool. Should follow the format `<provider-name>.<unique-tool-name>`. */ id: `${string}.${string}`; /** The name of the tool that the user must use in the tool set. */ name: string; /** The arguments for configuring the tool. Must match the expected arguments defined by the provider for this tool. */ args: Record<string, unknown>; }; --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-reasoning.ts --- import { SharedV2ProviderMetadata } from '../../shared'; /** Reasoning that the model has generated. */ export type LanguageModelV2Reasoning = { type: 'reasoning'; text: string; /** * Optional provider-specific metadata for the reasoning part. */ providerMetadata?: SharedV2ProviderMetadata; }; --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-response-metadata.ts --- export interface LanguageModelV2ResponseMetadata { /** ID for the generated response, if the provider sends one. */ id?: string; /** Timestamp for the start of the generated response, if the provider sends one. */ timestamp?: Date; /** The ID of the response model that was used to generate the response, if the provider sends one. */ modelId?: string; } --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-source.ts --- import { SharedV2ProviderMetadata } from '../../shared/v2/shared-v2-provider-metadata'; /** A source that has been used as input to generate the response. */ export type LanguageModelV2Source = | { type: 'source'; /** * The type of source - URL sources reference web content. */ sourceType: 'url'; /** * The ID of the source. */ id: string; /** * The URL of the source. */ url: string; /** * The title of the source. */ title?: string; /** * Additional provider metadata for the source. */ providerMetadata?: SharedV2ProviderMetadata; } | { type: 'source'; /** * The type of source - document sources reference files/documents. */ sourceType: 'document'; /** * The ID of the source. */ id: string; /** * IANA media type of the document (e.g., 'application/pdf'). */ mediaType: string; /** * The title of the document. */ title: string; /** * Optional filename of the document. */ filename?: string; /** * Additional provider metadata for the source. */ providerMetadata?: SharedV2ProviderMetadata; }; --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-stream-part.ts --- import { SharedV2ProviderMetadata } from '../../shared/v2/shared-v2-provider-metadata'; import { LanguageModelV2CallWarning } from './language-model-v2-call-warning'; import { LanguageModelV2File } from './language-model-v2-file'; import { LanguageModelV2FinishReason } from './language-model-v2-finish-reason'; import { LanguageModelV2ResponseMetadata } from './language-model-v2-response-metadata'; import { LanguageModelV2Source } from './language-model-v2-source'; import { LanguageModelV2ToolCall } from './language-model-v2-tool-call'; import { LanguageModelV2ToolResult } from './language-model-v2-tool-result'; import { LanguageModelV2Usage } from './language-model-v2-usage'; export type LanguageModelV2StreamPart = // Text blocks: | { type: 'text-start'; providerMetadata?: SharedV2ProviderMetadata; id: string; } | { type: 'text-delta'; id: string; providerMetadata?: SharedV2ProviderMetadata; delta: string; } | { type: 'text-end'; providerMetadata?: SharedV2ProviderMetadata; id: string; } // Reasoning blocks: | { type: 'reasoning-start'; providerMetadata?: SharedV2ProviderMetadata; id: string; } | { type: 'reasoning-delta'; id: string; providerMetadata?: SharedV2ProviderMetadata; delta: string; } | { type: 'reasoning-end'; id: string; providerMetadata?: SharedV2ProviderMetadata; } // Tool calls and results: | { type: 'tool-input-start'; id: string; toolName: string; providerMetadata?: SharedV2ProviderMetadata; providerExecuted?: boolean; } | { type: 'tool-input-delta'; id: string; delta: string; providerMetadata?: SharedV2ProviderMetadata; } | { type: 'tool-input-end'; id: string; providerMetadata?: SharedV2ProviderMetadata; } | LanguageModelV2ToolCall | LanguageModelV2ToolResult // Files and sources: | LanguageModelV2File | LanguageModelV2Source // stream start event with warnings for the call, e.g. unsupported settings: | { type: 'stream-start'; warnings: Array<LanguageModelV2CallWarning>; } // metadata for the response. // separate stream part so it can be sent once it is available. | ({ type: 'response-metadata' } & LanguageModelV2ResponseMetadata) // metadata that is available after the stream is finished: | { type: 'finish'; usage: LanguageModelV2Usage; finishReason: LanguageModelV2FinishReason; providerMetadata?: SharedV2ProviderMetadata; } // raw chunks if enabled | { type: 'raw'; rawValue: unknown; } // error parts are streamed, allowing for multiple errors | { type: 'error'; error: unknown; }; --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-text.ts --- import { SharedV2ProviderMetadata } from '../../shared/v2/shared-v2-provider-metadata'; /** Text that the model has generated. */ export type LanguageModelV2Text = { type: 'text'; /** The text content. */ text: string; providerMetadata?: SharedV2ProviderMetadata; }; --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-tool-call.ts --- import { SharedV2ProviderMetadata } from '../../shared/v2/shared-v2-provider-metadata'; /** Tool calls that the model has generated. */ export type LanguageModelV2ToolCall = { type: 'tool-call'; toolCallId: string; toolName: string; /** Stringified JSON object with the tool call arguments. Must match the parameters schema of the tool. */ input: string; /** * Whether the tool call will be executed by the provider. * If this flag is not set or is false, the tool call will be executed by the client. */ providerExecuted?: boolean; /** * Additional provider-specific metadata for the tool call. */ providerMetadata?: SharedV2ProviderMetadata; }; --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-tool-choice.ts --- export type LanguageModelV2ToolChoice = | { type: 'auto' } // the tool selection is automatic (can be no tool) | { type: 'none' } // no tool must be selected | { type: 'required' } // one of the available tools must be selected | { type: 'tool'; toolName: string }; // a specific tool must be selected: --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-tool-result.ts --- import { SharedV2ProviderMetadata } from '../../shared/v2/shared-v2-provider-metadata'; /** Result of a tool call that has been executed by the provider. */ export type LanguageModelV2ToolResult = { type: 'tool-result'; /** * The ID of the tool call that this result is associated with. */ toolCallId: string; /** * Name of the tool that generated this result. */ toolName: string; /** * Result of the tool call. This is a JSON-serializable object. */ result: unknown; /** * Optional flag if the result is an error or an error message. */ isError?: boolean; /** * Whether the tool result was generated by the provider. * If this flag is set to true, the tool result was generated by the provider. * If this flag is not set or is false, the tool result was generated by the client. */ providerExecuted?: boolean; /** * Additional provider-specific metadata for the tool result. */ providerMetadata?: SharedV2ProviderMetadata; }; --- File: /ai/packages/provider/src/language-model/v2/language-model-v2-usage.ts --- /** Usage information for a language model call. If your API return additional usage information, you can add it to the provider metadata under your provider's key. */ export type LanguageModelV2Usage = { /** The number of input (prompt) tokens used. */ inputTokens: number | undefined; /** The number of output (completion) tokens used. */ outputTokens: number | undefined; /** The total number of tokens as reported by the provider. This number might be different from the sum of `inputTokens` and `outputTokens` and e.g. include reasoning tokens or other overhead. */ totalTokens: number | undefined; /** The number of reasoning tokens used. */ reasoningTokens?: number | undefined; /** The number of cached input tokens. */ cachedInputTokens?: number | undefined; }; --- File: /ai/packages/provider/src/language-model/v2/language-model-v2.ts --- import { SharedV2Headers } from '../../shared'; import { SharedV2ProviderMetadata } from '../../shared/v2/shared-v2-provider-metadata'; import { LanguageModelV2CallOptions } from './language-model-v2-call-options'; import { LanguageModelV2CallWarning } from './language-model-v2-call-warning'; import { LanguageModelV2Content } from './language-model-v2-content'; import { LanguageModelV2FinishReason } from './language-model-v2-finish-reason'; import { LanguageModelV2ResponseMetadata } from './language-model-v2-response-metadata'; import { LanguageModelV2StreamPart } from './language-model-v2-stream-part'; import { LanguageModelV2Usage } from './language-model-v2-usage'; /** Specification for a language model that implements the language model interface version 2. */ export type LanguageModelV2 = { /** The language model must specify which language model interface version it implements. */ readonly specificationVersion: 'v2'; /** Name of the provider for logging purposes. */ readonly provider: string; /** Provider-specific model ID for logging purposes. */ readonly modelId: string; /** Supported URL patterns by media type for the provider. The keys are media type patterns or full media types (e.g. `*\/*` for everything, `audio/*`, `video/*`, or `application/pdf`). and the values are arrays of regular expressions that match the URL paths. The matching should be against lower-case URLs. Matched URLs are supported natively by the model and are not downloaded. @returns A map of supported URL patterns by media type (as a promise or a plain object). */ supportedUrls: | PromiseLike<Record<string, RegExp[]>> | Record<string, RegExp[]>; /** Generates a language model output (non-streaming). Naming: "do" prefix to prevent accidental direct usage of the method by the user. */ doGenerate(options: LanguageModelV2CallOptions): PromiseLike<{ /** Ordered content that the model has generated. */ content: Array<LanguageModelV2Content>; /** Finish reason. */ finishReason: LanguageModelV2FinishReason; /** Usage information. */ usage: LanguageModelV2Usage; /** Additional provider-specific metadata. They are passed through from the provider to the AI SDK and enable provider-specific results that can be fully encapsulated in the provider. */ providerMetadata?: SharedV2ProviderMetadata; /** Optional request information for telemetry and debugging purposes. */ request?: { /** Request HTTP body that was sent to the provider API. */ body?: unknown; }; /** Optional response information for telemetry and debugging purposes. */ response?: LanguageModelV2ResponseMetadata & { /** Response headers. */ headers?: SharedV2Headers; /** Response HTTP body. */ body?: unknown; }; /** Warnings for the call, e.g. unsupported settings. */ warnings: Array<LanguageModelV2CallWarning>; }>; /** Generates a language model output (streaming). Naming: "do" prefix to prevent accidental direct usage of the method by the user. * @return A stream of higher-level language model output parts. */ doStream(options: LanguageModelV2CallOptions): PromiseLike<{ stream: ReadableStream<LanguageModelV2StreamPart>; /** Optional request information for telemetry and debugging purposes. */ request?: { /** Request HTTP body that was sent to the provider API. */ body?: unknown; }; /** Optional response data. */ response?: { /** Response headers. */ headers?: SharedV2Headers; }; }>; }; --- File: /ai/packages/provider/src/language-model/index.ts --- export * from './v2/index'; --- File: /ai/packages/provider/src/language-model-middleware/v2/index.ts --- export * from './language-model-v2-middleware'; --- File: /ai/packages/provider/src/language-model-middleware/v2/language-model-v2-middleware.ts --- import { LanguageModelV2 } from '../../language-model/v2/language-model-v2'; import { LanguageModelV2CallOptions } from '../../language-model/v2/language-model-v2-call-options'; /** * Experimental middleware for LanguageModelV2. * This type defines the structure for middleware that can be used to modify * the behavior of LanguageModelV2 operations. */ export type LanguageModelV2Middleware = { /** * Middleware specification version. Use `v2` for the current version. */ middlewareVersion?: 'v2' | undefined; // backwards compatibility /** * Override the provider name if desired. * @param options.model - The language model instance. */ overrideProvider?: (options: { model: LanguageModelV2 }) => string; /** * Override the model ID if desired. * @param options.model - The language model instance. */ overrideModelId?: (options: { model: LanguageModelV2 }) => string; /** * Override the supported URLs if desired. * @param options.model - The language model instance. */ overrideSupportedUrls?: (options: { model: LanguageModelV2; }) => PromiseLike<Record<string, RegExp[]>> | Record<string, RegExp[]>; /** * Transforms the parameters before they are passed to the language model. * @param options - Object containing the type of operation and the parameters. * @param options.type - The type of operation ('generate' or 'stream'). * @param options.params - The original parameters for the language model call. * @returns A promise that resolves to the transformed parameters. */ transformParams?: (options: { type: 'generate' | 'stream'; params: LanguageModelV2CallOptions; model: LanguageModelV2; }) => PromiseLike<LanguageModelV2CallOptions>; /** * Wraps the generate operation of the language model. * @param options - Object containing the generate function, parameters, and model. * @param options.doGenerate - The original generate function. * @param options.doStream - The original stream function. * @param options.params - The parameters for the generate call. If the * `transformParams` middleware is used, this will be the transformed parameters. * @param options.model - The language model instance. * @returns A promise that resolves to the result of the generate operation. */ wrapGenerate?: (options: { doGenerate: () => ReturnType<LanguageModelV2['doGenerate']>; doStream: () => ReturnType<LanguageModelV2['doStream']>; params: LanguageModelV2CallOptions; model: LanguageModelV2; }) => Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>>; /** * Wraps the stream operation of the language model. * * @param options - Object containing the stream function, parameters, and model. * @param options.doGenerate - The original generate function. * @param options.doStream - The original stream function. * @param options.params - The parameters for the stream call. If the * `transformParams` middleware is used, this will be the transformed parameters. * @param options.model - The language model instance. * @returns A promise that resolves to the result of the stream operation. */ wrapStream?: (options: { doGenerate: () => ReturnType<LanguageModelV2['doGenerate']>; doStream: () => ReturnType<LanguageModelV2['doStream']>; params: LanguageModelV2CallOptions; model: LanguageModelV2; }) => PromiseLike<Awaited<ReturnType<LanguageModelV2['doStream']>>>; }; --- File: /ai/packages/provider/src/language-model-middleware/index.ts --- export * from './v2/index'; --- File: /ai/packages/provider/src/provider/v2/index.ts --- export type { ProviderV2 } from './provider-v2'; --- File: /ai/packages/provider/src/provider/v2/provider-v2.ts --- import { EmbeddingModelV2 } from '../../embedding-model/v2/embedding-model-v2'; import { ImageModelV2 } from '../../image-model/v2/image-model-v2'; import { LanguageModelV2 } from '../../language-model/v2/language-model-v2'; import { SpeechModelV2 } from '../../speech-model/v2/speech-model-v2'; import { TranscriptionModelV2 } from '../../transcription-model/v2/transcription-model-v2'; /** * Provider for language, text embedding, and image generation models. */ export interface ProviderV2 { /** Returns the language model with the given id. The model id is then passed to the provider function to get the model. @param {string} modelId - The id of the model to return. @returns {LanguageModel} The language model associated with the id @throws {NoSuchModelError} If no such model exists. */ languageModel(modelId: string): LanguageModelV2; /** Returns the text embedding model with the given id. The model id is then passed to the provider function to get the model. @param {string} modelId - The id of the model to return. @returns {LanguageModel} The language model associated with the id @throws {NoSuchModelError} If no such model exists. */ textEmbeddingModel(modelId: string): EmbeddingModelV2<string>; /** Returns the image model with the given id. The model id is then passed to the provider function to get the model. @param {string} modelId - The id of the model to return. @returns {ImageModel} The image model associated with the id */ imageModel(modelId: string): ImageModelV2; /** Returns the transcription model with the given id. The model id is then passed to the provider function to get the model. @param {string} modelId - The id of the model to return. @returns {TranscriptionModel} The transcription model associated with the id */ transcriptionModel?(modelId: string): TranscriptionModelV2; /** Returns the speech model with the given id. The model id is then passed to the provider function to get the model. @param {string} modelId - The id of the model to return. @returns {SpeechModel} The speech model associated with the id */ speechModel?(modelId: string): SpeechModelV2; } --- File: /ai/packages/provider/src/provider/index.ts --- export * from './v2/index'; --- File: /ai/packages/provider/src/shared/v2/index.ts --- export * from './shared-v2-headers'; export * from './shared-v2-provider-metadata'; export * from './shared-v2-provider-options'; --- File: /ai/packages/provider/src/shared/v2/shared-v2-headers.ts --- export type SharedV2Headers = Record<string, string>; --- File: /ai/packages/provider/src/shared/v2/shared-v2-provider-metadata.ts --- import { JSONValue } from '../../json-value/json-value'; /** * Additional provider-specific metadata. * Metadata are additional outputs from the provider. * They are passed through to the provider from the AI SDK * and enable provider-specific functionality * that can be fully encapsulated in the provider. * * This enables us to quickly ship provider-specific functionality * without affecting the core AI SDK. * * The outer record is keyed by the provider name, and the inner * record is keyed by the provider-specific metadata key. * * ```ts * { * "anthropic": { * "cacheControl": { "type": "ephemeral" } * } * } * ``` */ export type SharedV2ProviderMetadata = Record< string, Record<string, JSONValue> >; --- File: /ai/packages/provider/src/shared/v2/shared-v2-provider-options.ts --- import { JSONValue } from '../../json-value/json-value'; /** * Additional provider-specific options. * Options are additional input to the provider. * They are passed through to the provider from the AI SDK * and enable provider-specific functionality * that can be fully encapsulated in the provider. * * This enables us to quickly ship provider-specific functionality * without affecting the core AI SDK. * * The outer record is keyed by the provider name, and the inner * record is keyed by the provider-specific metadata key. * * ```ts * { * "anthropic": { * "cacheControl": { "type": "ephemeral" } * } * } * ``` */ export type SharedV2ProviderOptions = Record<string, Record<string, JSONValue>>; --- File: /ai/packages/provider/src/shared/index.ts --- export * from './v2/index'; --- File: /ai/packages/provider/src/speech-model/v2/index.ts --- export type { SpeechModelV2 } from './speech-model-v2'; export type { SpeechModelV2CallOptions } from './speech-model-v2-call-options'; export type { SpeechModelV2CallWarning } from './speech-model-v2-call-warning'; --- File: /ai/packages/provider/src/speech-model/v2/speech-model-v2-call-options.ts --- import { JSONValue } from '../../json-value/json-value'; type SpeechModelV2ProviderOptions = Record<string, Record<string, JSONValue>>; export type SpeechModelV2CallOptions = { /** * Text to convert to speech. */ text: string; /** * The voice to use for speech synthesis. * This is provider-specific and may be a voice ID, name, or other identifier. */ voice?: string; /** * The desired output format for the audio e.g. "mp3", "wav", etc. */ outputFormat?: string; /** * Instructions for the speech generation e.g. "Speak in a slow and steady tone". */ instructions?: string; /** * The speed of the speech generation. */ speed?: number; /** * The language for speech generation. This should be an ISO 639-1 language code (e.g. "en", "es", "fr") * or "auto" for automatic language detection. Provider support varies. */ language?: string; /** * Additional provider-specific options that are passed through to the provider * as body parameters. * * The outer record is keyed by the provider name, and the inner * record is keyed by the provider-specific metadata key. * ```ts * { * "openai": {} * } * ``` */ providerOptions?: SpeechModelV2ProviderOptions; /** * Abort signal for cancelling the operation. */ abortSignal?: AbortSignal; /** * Additional HTTP headers to be sent with the request. * Only applicable for HTTP-based providers. */ headers?: Record<string, string | undefined>; }; --- File: /ai/packages/provider/src/speech-model/v2/speech-model-v2-call-warning.ts --- import { SpeechModelV2CallOptions } from './speech-model-v2-call-options'; /** * Warning from the model provider for this call. The call will proceed, but e.g. * some settings might not be supported, which can lead to suboptimal results. */ export type SpeechModelV2CallWarning = | { type: 'unsupported-setting'; setting: keyof SpeechModelV2CallOptions; details?: string; } | { type: 'other'; message: string; }; --- File: /ai/packages/provider/src/speech-model/v2/speech-model-v2.ts --- import { JSONValue } from '../../json-value'; import { SharedV2Headers } from '../../shared'; import { SpeechModelV2CallOptions } from './speech-model-v2-call-options'; import { SpeechModelV2CallWarning } from './speech-model-v2-call-warning'; /** * Speech model specification version 2. */ export type SpeechModelV2 = { /** * The speech model must specify which speech model interface * version it implements. This will allow us to evolve the speech * model interface and retain backwards compatibility. The different * implementation versions can be handled as a discriminated union * on our side. */ readonly specificationVersion: 'v2'; /** * Name of the provider for logging purposes. */ readonly provider: string; /** * Provider-specific model ID for logging purposes. */ readonly modelId: string; /** * Generates speech audio from text. */ doGenerate(options: SpeechModelV2CallOptions): PromiseLike<{ /** * Generated audio as an ArrayBuffer. * The audio should be returned without any unnecessary conversion. * If the API returns base64 encoded strings, the audio should be returned * as base64 encoded strings. If the API returns binary data, the audio * should be returned as binary data. */ audio: string | Uint8Array; /** * Warnings for the call, e.g. unsupported settings. */ warnings: Array<SpeechModelV2CallWarning>; /** * Optional request information for telemetry and debugging purposes. */ request?: { /** * Response body (available only for providers that use HTTP requests). */ body?: unknown; }; /** * Response information for telemetry and debugging purposes. */ response: { /** * Timestamp for the start of the generated response. */ timestamp: Date; /** * The ID of the response model that was used to generate the response. */ modelId: string; /** * Response headers. */ headers?: SharedV2Headers; /** * Response body. */ body?: unknown; }; /** * Additional provider-specific metadata. They are passed through * from the provider to the AI SDK and enable provider-specific * results that can be fully encapsulated in the provider. */ providerMetadata?: Record<string, Record<string, JSONValue>>; }>; }; --- File: /ai/packages/provider/src/speech-model/index.ts --- export * from './v2/index'; --- File: /ai/packages/provider/src/transcription-model/v2/index.ts --- export type { TranscriptionModelV2 } from './transcription-model-v2'; export type { TranscriptionModelV2CallOptions } from './transcription-model-v2-call-options'; export type { TranscriptionModelV2CallWarning } from './transcription-model-v2-call-warning'; --- File: /ai/packages/provider/src/transcription-model/v2/transcription-model-v2-call-options.ts --- import { JSONValue } from '../../json-value/json-value'; type TranscriptionModelV2ProviderOptions = Record< string, Record<string, JSONValue> >; export type TranscriptionModelV2CallOptions = { /** Audio data to transcribe. Accepts a `Uint8Array` or `string`, where `string` is a base64 encoded audio file. */ audio: Uint8Array | string; /** The IANA media type of the audio data. @see https://www.iana.org/assignments/media-types/media-types.xhtml */ mediaType: string; /** Additional provider-specific options that are passed through to the provider as body parameters. The outer record is keyed by the provider name, and the inner record is keyed by the provider-specific metadata key. ```ts { "openai": { "timestampGranularities": ["word"] } } ``` */ providerOptions?: TranscriptionModelV2ProviderOptions; /** Abort signal for cancelling the operation. */ abortSignal?: AbortSignal; /** Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. */ headers?: Record<string, string | undefined>; }; --- File: /ai/packages/provider/src/transcription-model/v2/transcription-model-v2-call-warning.ts --- import { TranscriptionModelV2CallOptions } from './transcription-model-v2-call-options'; /** Warning from the model provider for this call. The call will proceed, but e.g. some settings might not be supported, which can lead to suboptimal results. */ export type TranscriptionModelV2CallWarning = | { type: 'unsupported-setting'; setting: keyof TranscriptionModelV2CallOptions; details?: string; } | { type: 'other'; message: string; }; --- File: /ai/packages/provider/src/transcription-model/v2/transcription-model-v2.ts --- import { JSONValue } from '../../json-value'; import { SharedV2Headers } from '../../shared'; import { TranscriptionModelV2CallOptions } from './transcription-model-v2-call-options'; import { TranscriptionModelV2CallWarning } from './transcription-model-v2-call-warning'; /** Transcription model specification version 2. */ export type TranscriptionModelV2 = { /** The transcription model must specify which transcription model interface version it implements. This will allow us to evolve the transcription model interface and retain backwards compatibility. The different implementation versions can be handled as a discriminated union on our side. */ readonly specificationVersion: 'v2'; /** Name of the provider for logging purposes. */ readonly provider: string; /** Provider-specific model ID for logging purposes. */ readonly modelId: string; /** Generates a transcript. */ doGenerate(options: TranscriptionModelV2CallOptions): PromiseLike<{ /** * The complete transcribed text from the audio. */ text: string; /** * Array of transcript segments with timing information. * Each segment represents a portion of the transcribed text with start and end times. */ segments: Array<{ /** * The text content of this segment. */ text: string; /** * The start time of this segment in seconds. */ startSecond: number; /** * The end time of this segment in seconds. */ endSecond: number; }>; /** * The detected language of the audio content, as an ISO-639-1 code (e.g., 'en' for English). * May be undefined if the language couldn't be detected. */ language: string | undefined; /** * The total duration of the audio file in seconds. * May be undefined if the duration couldn't be determined. */ durationInSeconds: number | undefined; /** Warnings for the call, e.g. unsupported settings. */ warnings: Array<TranscriptionModelV2CallWarning>; /** Optional request information for telemetry and debugging purposes. */ request?: { /** Raw request HTTP body that was sent to the provider API as a string (JSON should be stringified). Non-HTTP(s) providers should not set this. */ body?: string; }; /** Response information for telemetry and debugging purposes. */ response: { /** Timestamp for the start of the generated response. */ timestamp: Date; /** The ID of the response model that was used to generate the response. */ modelId: string; /** Response headers. */ headers?: SharedV2Headers; /** Response body. */ body?: unknown; }; /** Additional provider-specific metadata. They are passed through from the provider to the AI SDK and enable provider-specific results that can be fully encapsulated in the provider. */ providerMetadata?: Record<string, Record<string, JSONValue>>; }>; }; --- File: /ai/packages/provider/src/transcription-model/index.ts --- export * from './v2/index'; --- File: /ai/packages/provider/src/index.ts --- export * from './embedding-model/index'; export * from './errors/index'; export * from './image-model/index'; export * from './json-value/index'; export * from './language-model-middleware/index'; export * from './language-model/index'; export * from './provider/index'; export * from './shared/index'; export * from './speech-model/index'; export * from './transcription-model/index'; export type { JSONSchema7, JSONSchema7Definition } from 'json-schema'; --- File: /ai/packages/provider/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/provider-utils/src/test/convert-array-to-async-iterable.ts --- export function convertArrayToAsyncIterable<T>(values: T[]): AsyncIterable<T> { return { async *[Symbol.asyncIterator]() { for (const value of values) { yield value; } }, }; } --- File: /ai/packages/provider-utils/src/test/convert-array-to-readable-stream.ts --- export function convertArrayToReadableStream<T>( values: T[], ): ReadableStream<T> { return new ReadableStream({ start(controller) { try { for (const value of values) { controller.enqueue(value); } } finally { controller.close(); } }, }); } --- File: /ai/packages/provider-utils/src/test/convert-async-iterable-to-array.ts --- export async function convertAsyncIterableToArray<T>( iterable: AsyncIterable<T>, ): Promise<T[]> { const result: T[] = []; for await (const item of iterable) { result.push(item); } return result; } --- File: /ai/packages/provider-utils/src/test/convert-readable-stream-to-array.ts --- export async function convertReadableStreamToArray<T>( stream: ReadableStream<T>, ): Promise<T[]> { const reader = stream.getReader(); const result: T[] = []; while (true) { const { done, value } = await reader.read(); if (done) break; result.push(value); } return result; } --- File: /ai/packages/provider-utils/src/test/convert-response-stream-to-array.ts --- import { convertReadableStreamToArray } from './convert-readable-stream-to-array'; export async function convertResponseStreamToArray( response: Response, ): Promise<string[]> { return convertReadableStreamToArray( response.body!.pipeThrough(new TextDecoderStream()), ); } --- File: /ai/packages/provider-utils/src/test/index.ts --- export * from './convert-array-to-async-iterable'; export * from './convert-array-to-readable-stream'; export * from './convert-async-iterable-to-array'; export * from './convert-readable-stream-to-array'; export * from './convert-response-stream-to-array'; export * from './is-node-version'; export * from './mock-id'; export * from './test-server'; --- File: /ai/packages/provider-utils/src/test/is-node-version.ts --- export function isNodeVersion(version: number) { const nodeMajorVersion = parseInt(process.version.slice(1).split('.')[0], 10); return nodeMajorVersion === version; } --- File: /ai/packages/provider-utils/src/test/mock-id.ts --- export function mockId({ prefix = 'id', }: { prefix?: string; } = {}): () => string { let counter = 0; return () => `${prefix}-${counter++}`; } --- File: /ai/packages/provider-utils/src/test/test-server.ts --- import { http, HttpResponse, JsonBodyType } from 'msw'; import { setupServer } from 'msw/node'; import { convertArrayToReadableStream } from './convert-array-to-readable-stream'; export type UrlResponse = | { type: 'json-value'; headers?: Record<string, string>; body: JsonBodyType; } | { type: 'stream-chunks'; headers?: Record<string, string>; chunks: Array<string>; } | { type: 'binary'; headers?: Record<string, string>; body: Buffer; } | { type: 'empty'; headers?: Record<string, string>; status?: number; } | { type: 'error'; headers?: Record<string, string>; status?: number; body?: string; } | { type: 'controlled-stream'; headers?: Record<string, string>; controller: TestResponseController; } | undefined; type UrlResponseParameter = | UrlResponse | UrlResponse[] | ((options: { callNumber: number }) => UrlResponse); export type UrlHandler = { response: UrlResponseParameter; }; export type UrlHandlers< URLS extends { [url: string]: { response?: UrlResponseParameter; }; }, > = { [url in keyof URLS]: UrlHandler; }; class TestServerCall { constructor(private request: Request) {} get requestBodyJson() { return this.request!.text().then(JSON.parse); } get requestBodyMultipart() { return this.request!.headers.get('content-type')?.startsWith( 'multipart/form-data', ) ? // For multipart/form-data, return the form data entries as an object this.request!.formData().then(formData => { const entries: Record<string, any> = {}; formData.forEach((value, key) => { entries[key] = value; }); return entries; }) : null; } get requestCredentials() { return this.request!.credentials; } get requestHeaders() { const requestHeaders = this.request!.headers; // convert headers to object for easier comparison const headersObject: Record<string, string> = {}; requestHeaders.forEach((value, key) => { headersObject[key] = value; }); return headersObject; } get requestUrlSearchParams() { return new URL(this.request!.url).searchParams; } get requestUrl() { return this.request!.url; } get requestMethod() { return this.request!.method; } } export function createTestServer< URLS extends { [url: string]: { response?: UrlResponseParameter; }; }, >( routes: URLS, ): { urls: UrlHandlers<URLS>; calls: TestServerCall[]; } { const originalRoutes = structuredClone(routes); // deep copy const mswServer = setupServer( ...Object.entries(routes).map(([url, handler]) => { return http.all(url, ({ request }) => { const callNumber = calls.length; calls.push(new TestServerCall(request)); const response = typeof handler.response === 'function' ? handler.response({ callNumber }) : Array.isArray(handler.response) ? handler.response[callNumber] : handler.response; if (response === undefined) { return HttpResponse.json({ error: 'Not Found' }, { status: 404 }); } const handlerType = response.type; switch (handlerType) { case 'json-value': return HttpResponse.json(response.body, { status: 200, headers: { 'Content-Type': 'application/json', ...response.headers, }, }); case 'stream-chunks': return new HttpResponse( convertArrayToReadableStream(response.chunks).pipeThrough( new TextEncoderStream(), ), { status: 200, headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', ...response.headers, }, }, ); case 'controlled-stream': { if (request.signal) { request.signal.addEventListener('abort', () => { response.controller.error( new DOMException('Aborted', 'AbortError'), ); }); } return new HttpResponse( response.controller.stream.pipeThrough(new TextEncoderStream()), { status: 200, headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', ...response.headers, }, }, ); } case 'binary': { return HttpResponse.arrayBuffer(response.body, { status: 200, headers: response.headers, }); } case 'error': return HttpResponse.text(response.body ?? 'Error', { status: response.status ?? 500, headers: response.headers, }); case 'empty': return new HttpResponse(null, { status: response.status ?? 200, }); default: { const _exhaustiveCheck: never = handlerType; throw new Error(`Unknown response type: ${_exhaustiveCheck}`); } } }); }), ); let calls: TestServerCall[] = []; beforeAll(() => { mswServer.listen(); }); beforeEach(() => { mswServer.resetHandlers(); // set the responses back to the original values Object.entries(originalRoutes).forEach(([url, handler]) => { routes[url].response = handler.response; }); calls = []; }); afterAll(() => { mswServer.close(); }); return { urls: routes as UrlHandlers<URLS>, get calls() { return calls; }, }; } export class TestResponseController { private readonly transformStream: TransformStream; private readonly writer: WritableStreamDefaultWriter; constructor() { this.transformStream = new TransformStream(); this.writer = this.transformStream.writable.getWriter(); } get stream(): ReadableStream { return this.transformStream.readable; } async write(chunk: string): Promise<void> { await this.writer.write(chunk); } async error(error: Error): Promise<void> { await this.writer.abort(error); } async close(): Promise<void> { await this.writer.close(); } } --- File: /ai/packages/provider-utils/src/types/assistant-model-message.ts --- import { FilePart, ReasoningPart, TextPart, ToolCallPart, ToolResultPart, } from './content-part'; import { ProviderOptions } from './provider-options'; /** An assistant message. It can contain text, tool calls, or a combination of text and tool calls. */ export type AssistantModelMessage = { role: 'assistant'; content: AssistantContent; /** Additional provider-specific metadata. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; }; /** Content of an assistant message. It can be a string or an array of text, image, reasoning, redacted reasoning, and tool call parts. */ export type AssistantContent = | string | Array<TextPart | FilePart | ReasoningPart | ToolCallPart | ToolResultPart>; --- File: /ai/packages/provider-utils/src/types/content-part.ts --- import { LanguageModelV2ToolResultOutput } from '@ai-sdk/provider'; import { ProviderOptions } from './provider-options'; import { DataContent } from './data-content'; /** Text content part of a prompt. It contains a string of text. */ export interface TextPart { type: 'text'; /** The text content. */ text: string; /** Additional provider-specific metadata. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; } /** Image content part of a prompt. It contains an image. */ export interface ImagePart { type: 'image'; /** Image data. Can either be: - data: a base64-encoded string, a Uint8Array, an ArrayBuffer, or a Buffer - URL: a URL that points to the image */ image: DataContent | URL; /** Optional IANA media type of the image. @see https://www.iana.org/assignments/media-types/media-types.xhtml */ mediaType?: string; /** Additional provider-specific metadata. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; } /** File content part of a prompt. It contains a file. */ export interface FilePart { type: 'file'; /** File data. Can either be: - data: a base64-encoded string, a Uint8Array, an ArrayBuffer, or a Buffer - URL: a URL that points to the image */ data: DataContent | URL; /** Optional filename of the file. */ filename?: string; /** IANA media type of the file. @see https://www.iana.org/assignments/media-types/media-types.xhtml */ mediaType: string; /** Additional provider-specific metadata. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; } /** * Reasoning content part of a prompt. It contains a reasoning. */ export interface ReasoningPart { type: 'reasoning'; /** The reasoning text. */ text: string; /** Additional provider-specific metadata. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; } /** Tool call content part of a prompt. It contains a tool call (usually generated by the AI model). */ export interface ToolCallPart { type: 'tool-call'; /** ID of the tool call. This ID is used to match the tool call with the tool result. */ toolCallId: string; /** Name of the tool that is being called. */ toolName: string; /** Arguments of the tool call. This is a JSON-serializable object that matches the tool's input schema. */ input: unknown; /** Additional provider-specific metadata. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; /** Whether the tool call was executed by the provider. */ providerExecuted?: boolean; } /** Tool result content part of a prompt. It contains the result of the tool call with the matching ID. */ export interface ToolResultPart { type: 'tool-result'; /** ID of the tool call that this result is associated with. */ toolCallId: string; /** Name of the tool that generated this result. */ toolName: string; /** Result of the tool call. This is a JSON-serializable object. */ output: LanguageModelV2ToolResultOutput; /** Additional provider-specific metadata. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; } --- File: /ai/packages/provider-utils/src/types/data-content.ts --- /** Data content. Can either be a base64-encoded string, a Uint8Array, an ArrayBuffer, or a Buffer. */ export type DataContent = string | Uint8Array | ArrayBuffer | Buffer; --- File: /ai/packages/provider-utils/src/types/index.ts --- export type { AssistantContent, AssistantModelMessage, } from './assistant-model-message'; export type { FilePart, ImagePart, ReasoningPart, TextPart, ToolCallPart, ToolResultPart, } from './content-part'; export type { DataContent } from './data-content'; export type { ModelMessage } from './model-message'; export type { ProviderOptions } from './provider-options'; export type { SystemModelMessage } from './system-model-message'; export { dynamicTool, tool, type InferToolInput, type InferToolOutput, type Tool, type ToolCallOptions, type ToolExecuteFunction, } from './tool'; export type { ToolCall } from './tool-call'; export type { ToolContent, ToolModelMessage } from './tool-model-message'; export type { ToolResult } from './tool-result'; export type { UserContent, UserModelMessage } from './user-model-message'; --- File: /ai/packages/provider-utils/src/types/model-message.ts --- import { AssistantModelMessage } from './assistant-model-message'; import { SystemModelMessage } from './system-model-message'; import { ToolModelMessage } from './tool-model-message'; import { UserModelMessage } from './user-model-message'; /** A message that can be used in the `messages` field of a prompt. It can be a user message, an assistant message, or a tool message. */ export type ModelMessage = | SystemModelMessage | UserModelMessage | AssistantModelMessage | ToolModelMessage; --- File: /ai/packages/provider-utils/src/types/provider-options.ts --- import { SharedV2ProviderOptions } from '@ai-sdk/provider'; /** Additional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ export type ProviderOptions = SharedV2ProviderOptions; --- File: /ai/packages/provider-utils/src/types/system-model-message.ts --- import { ProviderOptions } from './provider-options'; /** A system message. It can contain system information. Note: using the "system" part of the prompt is strongly preferred to increase the resilience against prompt injection attacks, and because not all providers support several system messages. */ export type SystemModelMessage = { role: 'system'; content: string; /** Additional provider-specific metadata. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; }; --- File: /ai/packages/provider-utils/src/types/tool-call.ts --- /** Typed tool call that is returned by generateText and streamText. It contains the tool call ID, the tool name, and the tool arguments. */ export interface ToolCall<NAME extends string, INPUT> { /** ID of the tool call. This ID is used to match the tool call with the tool result. */ toolCallId: string; /** Name of the tool that is being called. */ toolName: NAME; /** Arguments of the tool call. This is a JSON-serializable object that matches the tool's input schema. */ input: INPUT; /** * Whether the tool call will be executed by the provider. * If this flag is not set or is false, the tool call will be executed by the client. */ providerExecuted?: boolean; /** * Whether the tool is dynamic. */ dynamic?: boolean; } --- File: /ai/packages/provider-utils/src/types/tool-model-message.ts --- import { ToolResultPart } from './content-part'; import { ProviderOptions } from './provider-options'; /** A tool message. It contains the result of one or more tool calls. */ export type ToolModelMessage = { role: 'tool'; content: ToolContent; /** Additional provider-specific metadata. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; }; /** Content of a tool message. It is an array of tool result parts. */ export type ToolContent = Array<ToolResultPart>; --- File: /ai/packages/provider-utils/src/types/tool-result.ts --- /** Typed tool result that is returned by `generateText` and `streamText`. It contains the tool call ID, the tool name, the tool arguments, and the tool result. */ export interface ToolResult<NAME extends string, INPUT, OUTPUT> { /** ID of the tool call. This ID is used to match the tool call with the tool result. */ toolCallId: string; /** Name of the tool that was called. */ toolName: NAME; /** Arguments of the tool call. This is a JSON-serializable object that matches the tool's input schema. */ input: INPUT; /** Result of the tool call. This is the result of the tool's execution. */ output: OUTPUT; /** * Whether the tool result has been executed by the provider. */ providerExecuted?: boolean; /** * Whether the tool is dynamic. */ dynamic?: boolean; } --- File: /ai/packages/provider-utils/src/types/tool.test-d.ts --- import { z } from 'zod/v4'; import { Tool, ToolExecuteFunction, FlexibleSchema, } from '@ai-sdk/provider-utils'; import { tool } from './tool'; describe('tool helper', () => { it('should work with no parameters and no output', () => { const toolType = tool({}); expectTypeOf(toolType).toEqualTypeOf<Tool<never, never>>(); expectTypeOf(toolType.execute).toEqualTypeOf<undefined>(); expectTypeOf(toolType.execute).not.toEqualTypeOf<Function>(); expectTypeOf(toolType.inputSchema).toEqualTypeOf<undefined>(); }); it('should work with only parameters', () => { const toolType = tool({ inputSchema: z.object({ number: z.number() }), }); expectTypeOf(toolType).toEqualTypeOf<Tool<{ number: number }, never>>(); expectTypeOf(toolType.execute).toEqualTypeOf<undefined>(); expectTypeOf(toolType.execute).not.toEqualTypeOf<Function>(); expectTypeOf(toolType.inputSchema).toEqualTypeOf< FlexibleSchema<{ number: number }> >(); }); it('should work with only output', () => { const toolType = tool({ execute: async () => 'test' as const, }); expectTypeOf(toolType).toEqualTypeOf<Tool<never, 'test'>>(); expectTypeOf(toolType.execute).toMatchTypeOf< ToolExecuteFunction<never, 'test'> | undefined >(); expectTypeOf(toolType.execute).not.toEqualTypeOf<undefined>(); expectTypeOf(toolType.inputSchema).toEqualTypeOf<undefined>(); }); it('should work with both inputSchema and output', () => { const toolType = tool({ inputSchema: z.object({ number: z.number() }), execute: async input => { expectTypeOf(input).toEqualTypeOf<{ number: number }>(); return 'test' as const; }, }); expectTypeOf(toolType).toEqualTypeOf<Tool<{ number: number }, 'test'>>(); expectTypeOf(toolType.execute).toMatchTypeOf< ToolExecuteFunction<{ number: number }, 'test'> | undefined >(); expectTypeOf(toolType.execute).not.toEqualTypeOf<undefined>(); expectTypeOf(toolType.inputSchema).toEqualTypeOf< FlexibleSchema<{ number: number }> >(); }); }); --- File: /ai/packages/provider-utils/src/types/tool.ts --- import { JSONValue, LanguageModelV2ToolResultPart } from '@ai-sdk/provider'; import { FlexibleSchema } from '../schema'; import { ModelMessage } from './model-message'; import { ProviderOptions } from './provider-options'; /** * Additional options that are sent into each tool call. */ // TODO AI SDK 6: rename to ToolExecutionOptions export interface ToolCallOptions { /** * The ID of the tool call. You can use it e.g. when sending tool-call related information with stream data. */ toolCallId: string; /** * Messages that were sent to the language model to initiate the response that contained the tool call. * The messages **do not** include the system prompt nor the assistant response that contained the tool call. */ messages: ModelMessage[]; /** * An optional abort signal that indicates that the overall operation should be aborted. */ abortSignal?: AbortSignal; /** * Additional context. * * Experimental (can break in patch releases). */ experimental_context?: unknown; } export type ToolExecuteFunction<INPUT, OUTPUT> = ( input: INPUT, options: ToolCallOptions, ) => PromiseLike<OUTPUT> | OUTPUT; // 0 extends 1 & N checks for any // [N] extends [never] checks for never type NeverOptional<N, T> = 0 extends 1 & N ? Partial<T> : [N] extends [never] ? Partial<Record<keyof T, undefined>> : T; /** A tool contains the description and the schema of the input that the tool expects. This enables the language model to generate the input. The tool can also contain an optional execute function for the actual execution function of the tool. */ export type Tool< INPUT extends JSONValue | unknown | never = any, OUTPUT extends JSONValue | unknown | never = any, > = { /** An optional description of what the tool does. Will be used by the language model to decide whether to use the tool. Not used for provider-defined tools. */ description?: string; /** Additional provider-specific metadata. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; } & NeverOptional< INPUT, { /** The schema of the input that the tool expects. The language model will use this to generate the input. It is also used to validate the output of the language model. Use descriptions to make the input understandable for the language model. */ inputSchema: FlexibleSchema<INPUT>; /** * Optional function that is called when the argument streaming starts. * Only called when the tool is used in a streaming context. */ onInputStart?: (options: ToolCallOptions) => void | PromiseLike<void>; /** * Optional function that is called when an argument streaming delta is available. * Only called when the tool is used in a streaming context. */ onInputDelta?: ( options: { inputTextDelta: string } & ToolCallOptions, ) => void | PromiseLike<void>; /** * Optional function that is called when a tool call can be started, * even if the execute function is not provided. */ onInputAvailable?: ( options: { input: [INPUT] extends [never] ? undefined : INPUT; } & ToolCallOptions, ) => void | PromiseLike<void>; } > & NeverOptional< OUTPUT, { /** Optional conversion function that maps the tool result to an output that can be used by the language model. If not provided, the tool result will be sent as a JSON object. */ toModelOutput?: ( output: OUTPUT, ) => LanguageModelV2ToolResultPart['output']; } & ( | { /** An async function that is called with the arguments from the tool call and produces a result. If not provided, the tool will not be executed automatically. @args is the input of the tool call. @options.abortSignal is a signal that can be used to abort the tool call. */ execute: ToolExecuteFunction<INPUT, OUTPUT>; outputSchema?: FlexibleSchema<OUTPUT>; } | { outputSchema: FlexibleSchema<OUTPUT>; execute?: never; } ) > & ( | { /** Tool with user-defined input and output schemas. */ type?: undefined | 'function'; } | { /** Tool that is defined at runtime (e.g. an MCP tool). The types of input and output are not known at development time. */ type: 'dynamic'; } | { /** Tool with provider-defined input and output schemas. */ type: 'provider-defined'; /** The ID of the tool. Should follow the format `<provider-name>.<unique-tool-name>`. */ id: `${string}.${string}`; /** The name of the tool that the user must use in the tool set. */ name: string; /** The arguments for configuring the tool. Must match the expected arguments defined by the provider for this tool. */ args: Record<string, unknown>; } ); /** * Infer the input type of a tool. */ export type InferToolInput<TOOL extends Tool> = TOOL extends Tool<infer INPUT, any> ? INPUT : never; /** * Infer the output type of a tool. */ export type InferToolOutput<TOOL extends Tool> = TOOL extends Tool<any, infer OUTPUT> ? OUTPUT : never; /** Helper function for inferring the execute args of a tool. */ // Note: overload order is important for auto-completion export function tool<INPUT, OUTPUT>( tool: Tool<INPUT, OUTPUT>, ): Tool<INPUT, OUTPUT>; export function tool<INPUT>(tool: Tool<INPUT, never>): Tool<INPUT, never>; export function tool<OUTPUT>(tool: Tool<never, OUTPUT>): Tool<never, OUTPUT>; export function tool(tool: Tool<never, never>): Tool<never, never>; export function tool(tool: any): any { return tool; } /** Helper function for defining a dynamic tool. */ export function dynamicTool(tool: { description?: string; providerOptions?: ProviderOptions; inputSchema: FlexibleSchema<unknown>; execute: ToolExecuteFunction<unknown, unknown>; toModelOutput?: (output: unknown) => LanguageModelV2ToolResultPart['output']; }): Tool<unknown, unknown> & { type: 'dynamic'; } { return { ...tool, type: 'dynamic' }; } --- File: /ai/packages/provider-utils/src/types/user-model-message.ts --- import { FilePart, ImagePart, TextPart } from './content-part'; import { ProviderOptions } from './provider-options'; /** A user message. It can contain text or a combination of text and images. */ export type UserModelMessage = { role: 'user'; content: UserContent; /** Additional provider-specific metadata. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; }; /** Content of a user message. It can be a string or an array of text and image parts. */ export type UserContent = string | Array<TextPart | ImagePart | FilePart>; --- File: /ai/packages/provider-utils/src/combine-headers.ts --- export function combineHeaders( ...headers: Array<Record<string, string | undefined> | undefined> ): Record<string, string | undefined> { return headers.reduce( (combinedHeaders, currentHeaders) => ({ ...combinedHeaders, ...(currentHeaders ?? {}), }), {}, ) as Record<string, string | undefined>; } --- File: /ai/packages/provider-utils/src/convert-async-iterator-to-readable-stream.ts --- /** * Converts an AsyncIterator to a ReadableStream. * * @template T - The type of elements produced by the AsyncIterator. * @param { <T>} iterator - The AsyncIterator to convert. * @returns {ReadableStream<T>} - A ReadableStream that provides the same data as the AsyncIterator. */ export function convertAsyncIteratorToReadableStream<T>( iterator: AsyncIterator<T>, ): ReadableStream<T> { return new ReadableStream<T>({ /** * Called when the consumer wants to pull more data from the stream. * * @param {ReadableStreamDefaultController<T>} controller - The controller to enqueue data into the stream. * @returns {Promise<void>} */ async pull(controller) { try { const { value, done } = await iterator.next(); if (done) { controller.close(); } else { controller.enqueue(value); } } catch (error) { controller.error(error); } }, /** * Called when the consumer cancels the stream. */ cancel() {}, }); } --- File: /ai/packages/provider-utils/src/delay.test.ts --- import { delay } from './delay'; describe('delay', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); describe('basic delay functionality', () => { it('should resolve after the specified delay', async () => { const delayPromise = delay(1000); // Promise should not be resolved immediately let resolved = false; delayPromise.then(() => { resolved = true; }); expect(resolved).toBe(false); // Advance timers by less than the delay await vi.advanceTimersByTimeAsync(500); expect(resolved).toBe(false); // Advance timers to complete the delay await vi.advanceTimersByTimeAsync(500); expect(resolved).toBe(true); // Verify the promise resolves await expect(delayPromise).resolves.toBeUndefined(); }); it('should resolve immediately when delayInMs is null', async () => { const delayPromise = delay(null); await expect(delayPromise).resolves.toBeUndefined(); }); it('should resolve immediately when delayInMs is undefined', async () => { const delayPromise = delay(undefined); await expect(delayPromise).resolves.toBeUndefined(); }); it('should resolve immediately when delayInMs is 0', async () => { const delayPromise = delay(0); // Even with 0 delay, setTimeout is used, so we need to advance timers await vi.advanceTimersByTimeAsync(0); await expect(delayPromise).resolves.toBeUndefined(); }); }); describe('abort signal functionality', () => { it('should reject immediately if signal is already aborted', async () => { const controller = new AbortController(); controller.abort(); const delayPromise = delay(1000, { abortSignal: controller.signal }); await expect(delayPromise).rejects.toThrow('Delay was aborted'); expect(vi.getTimerCount()).toBe(0); // No timer should be set }); it('should reject when signal is aborted during delay', async () => { const controller = new AbortController(); const delayPromise = delay(1000, { abortSignal: controller.signal }); // Advance time partially await vi.advanceTimersByTimeAsync(500); // Abort the signal controller.abort(); await expect(delayPromise).rejects.toThrow('Delay was aborted'); }); it('should clean up timeout when aborted', async () => { const controller = new AbortController(); const delayPromise = delay(1000, { abortSignal: controller.signal }); expect(vi.getTimerCount()).toBe(1); controller.abort(); try { await delayPromise; } catch { // Expected to throw } expect(vi.getTimerCount()).toBe(0); }); it('should clean up event listener when delay completes normally', async () => { const controller = new AbortController(); const addEventListenerSpy = vi.spyOn( controller.signal, 'addEventListener', ); const removeEventListenerSpy = vi.spyOn( controller.signal, 'removeEventListener', ); const delayPromise = delay(1000, { abortSignal: controller.signal }); expect(addEventListenerSpy).toHaveBeenCalledWith( 'abort', expect.any(Function), ); await vi.advanceTimersByTimeAsync(1000); await delayPromise; expect(removeEventListenerSpy).toHaveBeenCalledWith( 'abort', expect.any(Function), ); }); it('should work without signal option', async () => { const delayPromise = delay(1000); await vi.advanceTimersByTimeAsync(1000); await expect(delayPromise).resolves.toBeUndefined(); }); }); describe('error handling', () => { it('should create proper DOMException for abort', async () => { const controller = new AbortController(); controller.abort(); const delayPromise = delay(1000, { abortSignal: controller.signal }); try { await delayPromise; expect.fail('Should have thrown'); } catch (error) { expect(error).toBeInstanceOf(DOMException); expect((error as DOMException).message).toBe('Delay was aborted'); expect((error as DOMException).name).toBe('AbortError'); } }); }); describe('edge cases', () => { it('should handle very large delays', async () => { const delayPromise = delay(Number.MAX_SAFE_INTEGER); await vi.advanceTimersByTimeAsync(1000); let resolved = false; delayPromise.then(() => { resolved = true; }); expect(resolved).toBe(false); // Fast forward to complete await vi.advanceTimersByTimeAsync(1000); await expect(delayPromise).resolves.toBeUndefined(); }); it('should handle negative delays (treated as 0)', async () => { const delayPromise = delay(-100); vi.advanceTimersByTime(0); await expect(delayPromise).resolves.toBeUndefined(); }); it('should handle multiple delays simultaneously', async () => { const delay1 = delay(100); const delay2 = delay(200); const delay3 = delay(300); let resolved1 = false; let resolved2 = false; let resolved3 = false; delay1.then(() => { resolved1 = true; }); delay2.then(() => { resolved2 = true; }); delay3.then(() => { resolved3 = true; }); // After 100ms, only first should resolve await vi.advanceTimersByTimeAsync(100); expect(resolved1).toBe(true); expect(resolved2).toBe(false); expect(resolved3).toBe(false); // After 200ms, first two should resolve await vi.advanceTimersByTimeAsync(100); expect(resolved1).toBe(true); expect(resolved2).toBe(true); expect(resolved3).toBe(false); // After 300ms, all should resolve await vi.advanceTimersByTimeAsync(100); expect(resolved1).toBe(true); expect(resolved2).toBe(true); expect(resolved3).toBe(true); }); }); }); --- File: /ai/packages/provider-utils/src/delay.ts --- /** * Creates a Promise that resolves after a specified delay * @param delayInMs - The delay duration in milliseconds. If null or undefined, resolves immediately. * @param signal - Optional AbortSignal to cancel the delay * @returns A Promise that resolves after the specified delay * @throws {DOMException} When the signal is aborted */ export async function delay( delayInMs?: number | null, options?: { abortSignal?: AbortSignal; }, ): Promise<void> { if (delayInMs == null) { return Promise.resolve(); } const signal = options?.abortSignal; return new Promise<void>((resolve, reject) => { if (signal?.aborted) { reject(createAbortError()); return; } const timeoutId = setTimeout(() => { cleanup(); resolve(); }, delayInMs); const cleanup = () => { clearTimeout(timeoutId); signal?.removeEventListener('abort', onAbort); }; const onAbort = () => { cleanup(); reject(createAbortError()); }; signal?.addEventListener('abort', onAbort); }); } function createAbortError(): DOMException { return new DOMException('Delay was aborted', 'AbortError'); } --- File: /ai/packages/provider-utils/src/extract-response-headers.ts --- /** Extracts the headers from a response object and returns them as a key-value object. @param response - The response object to extract headers from. @returns The headers as a key-value object. */ export function extractResponseHeaders(response: Response) { return Object.fromEntries<string>([...response.headers]); } --- File: /ai/packages/provider-utils/src/fetch-function.ts --- /** * Fetch function type (standardizes the version of fetch used). */ export type FetchFunction = typeof globalThis.fetch; --- File: /ai/packages/provider-utils/src/generate-id.test.ts --- import { InvalidArgumentError } from '@ai-sdk/provider'; import { expect, it } from 'vitest'; import { createIdGenerator, generateId } from './generate-id'; describe('createIdGenerator', () => { it('should generate an ID with the correct length', () => { const idGenerator = createIdGenerator({ size: 10 }); expect(idGenerator()).toHaveLength(10); }); it('should generate an ID with the correct default length', () => { const idGenerator = createIdGenerator(); expect(idGenerator()).toHaveLength(16); }); it('should throw an error if the separator is part of the alphabet', () => { expect(() => createIdGenerator({ separator: 'a', prefix: 'b' })).toThrow( InvalidArgumentError, ); }); }); describe('generateId', () => { it('should generate unique IDs', () => { const id1 = generateId(); const id2 = generateId(); expect(id1).not.toBe(id2); }); }); --- File: /ai/packages/provider-utils/src/generate-id.ts --- import { InvalidArgumentError } from '@ai-sdk/provider'; /** Creates an ID generator. The total length of the ID is the sum of the prefix, separator, and random part length. Not cryptographically secure. @param alphabet - The alphabet to use for the ID. Default: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'. @param prefix - The prefix of the ID to generate. Optional. @param separator - The separator between the prefix and the random part of the ID. Default: '-'. @param size - The size of the random part of the ID to generate. Default: 16. */ export const createIdGenerator = ({ prefix, size = 16, alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', separator = '-', }: { prefix?: string; separator?: string; size?: number; alphabet?: string; } = {}): IdGenerator => { const generator = () => { const alphabetLength = alphabet.length; const chars = new Array(size); for (let i = 0; i < size; i++) { chars[i] = alphabet[(Math.random() * alphabetLength) | 0]; } return chars.join(''); }; if (prefix == null) { return generator; } // check that the prefix is not part of the alphabet (otherwise prefix checking can fail randomly) if (alphabet.includes(separator)) { throw new InvalidArgumentError({ argument: 'separator', message: `The separator "${separator}" must not be part of the alphabet "${alphabet}".`, }); } return () => `${prefix}${separator}${generator()}`; }; /** A function that generates an ID. */ export type IdGenerator = () => string; /** Generates a 16-character random string to use for IDs. Not cryptographically secure. */ export const generateId = createIdGenerator(); --- File: /ai/packages/provider-utils/src/get-error-message.ts --- export function getErrorMessage(error: unknown | undefined) { if (error == null) { return 'unknown error'; } if (typeof error === 'string') { return error; } if (error instanceof Error) { return error.message; } return JSON.stringify(error); } --- File: /ai/packages/provider-utils/src/get-from-api.test.ts --- import { APICallError } from '@ai-sdk/provider'; import { describe, it, expect, vi } from 'vitest'; import { getFromApi } from './get-from-api'; import { createJsonResponseHandler, createStatusCodeErrorResponseHandler, } from './response-handler'; import { z } from 'zod/v4'; describe('getFromApi', () => { const mockSuccessResponse = { name: 'test', value: 123, }; const mockResponseSchema = z.object({ name: z.string(), value: z.number(), }); const mockHeaders = { 'Content-Type': 'application/json', Authorization: 'Bearer test', }; it('should successfully fetch and parse data', async () => { const mockFetch = vi.fn().mockResolvedValue( new Response(JSON.stringify(mockSuccessResponse), { status: 200, headers: mockHeaders, }), ); const result = await getFromApi({ url: 'https://api.test.com/data', headers: { Authorization: 'Bearer test' }, successfulResponseHandler: createJsonResponseHandler(mockResponseSchema), failedResponseHandler: createStatusCodeErrorResponseHandler(), fetch: mockFetch, }); expect(result.value).toEqual(mockSuccessResponse); expect(mockFetch).toHaveBeenCalledWith( 'https://api.test.com/data', expect.objectContaining({ method: 'GET', headers: { Authorization: 'Bearer test' }, }), ); }); it('should handle API errors', async () => { const errorResponse = { error: 'Not Found' }; const mockFetch = vi.fn().mockResolvedValue( new Response(JSON.stringify(errorResponse), { status: 404, statusText: 'Not Found', headers: mockHeaders, }), ); await expect( getFromApi({ url: 'https://api.test.com/data', successfulResponseHandler: createJsonResponseHandler(mockResponseSchema), failedResponseHandler: createStatusCodeErrorResponseHandler(), fetch: mockFetch, }), ).rejects.toThrow(APICallError); }); it('should handle network errors', async () => { const mockFetch = vi.fn().mockRejectedValue( Object.assign(new TypeError('fetch failed'), { cause: new Error('Failed to connect'), }), ); await expect( getFromApi({ url: 'https://api.test.com/data', successfulResponseHandler: createJsonResponseHandler(mockResponseSchema), failedResponseHandler: createStatusCodeErrorResponseHandler(), fetch: mockFetch, }), ).rejects.toThrow('Cannot connect to API: Failed to connect'); }); it('should handle abort signals', async () => { const abortController = new AbortController(); const mockFetch = vi.fn().mockImplementation(() => { abortController.abort(); return Promise.reject(new DOMException('Aborted', 'AbortError')); }); await expect( getFromApi({ url: 'https://api.test.com/data', successfulResponseHandler: createJsonResponseHandler(mockResponseSchema), failedResponseHandler: createStatusCodeErrorResponseHandler(), fetch: mockFetch, abortSignal: abortController.signal, }), ).rejects.toThrow('Aborted'); }); it('should remove undefined header entries', async () => { const mockFetch = vi.fn().mockResolvedValue( new Response(JSON.stringify(mockSuccessResponse), { status: 200, headers: mockHeaders, }), ); await getFromApi({ url: 'https://api.test.com/data', headers: { Authorization: 'Bearer test', 'X-Custom-Header': undefined, }, successfulResponseHandler: createJsonResponseHandler(mockResponseSchema), failedResponseHandler: createStatusCodeErrorResponseHandler(), fetch: mockFetch, }); expect(mockFetch).toHaveBeenCalledWith( 'https://api.test.com/data', expect.objectContaining({ headers: { Authorization: 'Bearer test', }, }), ); }); it('should handle errors in response handlers', async () => { const mockFetch = vi.fn().mockResolvedValue( new Response('invalid json', { status: 200, headers: mockHeaders, }), ); await expect( getFromApi({ url: 'https://api.test.com/data', successfulResponseHandler: createJsonResponseHandler(mockResponseSchema), failedResponseHandler: createStatusCodeErrorResponseHandler(), fetch: mockFetch, }), ).rejects.toThrow(APICallError); }); it('should use default fetch when not provided', async () => { const originalFetch = global.fetch; const mockFetch = vi.fn().mockResolvedValue( new Response(JSON.stringify(mockSuccessResponse), { status: 200, headers: mockHeaders, }), ); global.fetch = mockFetch; try { await getFromApi({ url: 'https://api.test.com/data', successfulResponseHandler: createJsonResponseHandler(mockResponseSchema), failedResponseHandler: createStatusCodeErrorResponseHandler(), }); expect(mockFetch).toHaveBeenCalled(); } finally { global.fetch = originalFetch; } }); }); --- File: /ai/packages/provider-utils/src/get-from-api.ts --- import { APICallError } from '@ai-sdk/provider'; import { extractResponseHeaders } from './extract-response-headers'; import { FetchFunction } from './fetch-function'; import { handleFetchError } from './handle-fetch-error'; import { isAbortError } from './is-abort-error'; import { removeUndefinedEntries } from './remove-undefined-entries'; import { ResponseHandler } from './response-handler'; // use function to allow for mocking in tests: const getOriginalFetch = () => globalThis.fetch; export const getFromApi = async <T>({ url, headers = {}, successfulResponseHandler, failedResponseHandler, abortSignal, fetch = getOriginalFetch(), }: { url: string; headers?: Record<string, string | undefined>; failedResponseHandler: ResponseHandler<Error>; successfulResponseHandler: ResponseHandler<T>; abortSignal?: AbortSignal; fetch?: FetchFunction; }) => { try { const response = await fetch(url, { method: 'GET', headers: removeUndefinedEntries(headers), signal: abortSignal, }); const responseHeaders = extractResponseHeaders(response); if (!response.ok) { let errorInformation: { value: Error; responseHeaders?: Record<string, string> | undefined; }; try { errorInformation = await failedResponseHandler({ response, url, requestBodyValues: {}, }); } catch (error) { if (isAbortError(error) || APICallError.isInstance(error)) { throw error; } throw new APICallError({ message: 'Failed to process error response', cause: error, statusCode: response.status, url, responseHeaders, requestBodyValues: {}, }); } throw errorInformation.value; } try { return await successfulResponseHandler({ response, url, requestBodyValues: {}, }); } catch (error) { if (error instanceof Error) { if (isAbortError(error) || APICallError.isInstance(error)) { throw error; } } throw new APICallError({ message: 'Failed to process successful response', cause: error, statusCode: response.status, url, responseHeaders, requestBodyValues: {}, }); } } catch (error) { throw handleFetchError({ error, url, requestBodyValues: {} }); } }; --- File: /ai/packages/provider-utils/src/handle-fetch-error.ts --- import { APICallError } from '@ai-sdk/provider'; import { isAbortError } from './is-abort-error'; const FETCH_FAILED_ERROR_MESSAGES = ['fetch failed', 'failed to fetch']; export function handleFetchError({ error, url, requestBodyValues, }: { error: unknown; url: string; requestBodyValues: unknown; }) { if (isAbortError(error)) { return error; } // unwrap original error when fetch failed (for easier debugging): if ( error instanceof TypeError && FETCH_FAILED_ERROR_MESSAGES.includes(error.message.toLowerCase()) ) { const cause = (error as any).cause; if (cause != null) { // Failed to connect to server: return new APICallError({ message: `Cannot connect to API: ${cause.message}`, cause, url, requestBodyValues, isRetryable: true, // retry when network error }); } } return error; } --- File: /ai/packages/provider-utils/src/index.ts --- export * from './combine-headers'; export { convertAsyncIteratorToReadableStream } from './convert-async-iterator-to-readable-stream'; export * from './delay'; export * from './extract-response-headers'; export * from './fetch-function'; export { createIdGenerator, generateId, type IdGenerator } from './generate-id'; export * from './get-error-message'; export * from './get-from-api'; export * from './is-abort-error'; export { isUrlSupported } from './is-url-supported'; export * from './load-api-key'; export { loadOptionalSetting } from './load-optional-setting'; export { loadSetting } from './load-setting'; export * from './parse-json'; export { parseJsonEventStream } from './parse-json-event-stream'; export { parseProviderOptions } from './parse-provider-options'; export * from './post-to-api'; export { createProviderDefinedToolFactory, type ProviderDefinedToolFactory, createProviderDefinedToolFactoryWithOutputSchema, type ProviderDefinedToolFactoryWithOutputSchema, } from './provider-defined-tool-factory'; export * from './remove-undefined-entries'; export * from './resolve'; export * from './response-handler'; export { asSchema, jsonSchema, type FlexibleSchema, type InferSchema, type Schema, } from './schema'; export * from './uint8-utils'; export * from './validate-types'; export * from './validator'; export * from './without-trailing-slash'; export { zodSchema } from './zod-schema'; // folder re-exports export * from './types'; // external re-exports export * from '@standard-schema/spec'; export { EventSourceParserStream, type EventSourceMessage, } from 'eventsource-parser/stream'; --- File: /ai/packages/provider-utils/src/is-abort-error.ts --- export function isAbortError(error: unknown): error is Error { return ( (error instanceof Error || error instanceof DOMException) && (error.name === 'AbortError' || error.name === 'ResponseAborted' || // Next.js error.name === 'TimeoutError') ); } --- File: /ai/packages/provider-utils/src/is-url-supported.test.ts --- import { isUrlSupported } from './is-url-supported'; describe('isUrlSupported', () => { describe('when the model does not support any URLs', () => { it('should return false', async () => { expect( isUrlSupported({ mediaType: 'text/plain', url: 'https://example.com', supportedUrls: {}, }), ).toBe(false); }); }); describe('when the model supports specific media types and URLs', () => { it('should return true for exact media type and exact URL match', async () => { expect( isUrlSupported({ mediaType: 'text/plain', url: 'https://example.com', supportedUrls: { 'text/plain': [/https:\/\/example\.com/] }, }), ).toBe(true); }); it('should return true for exact media type and regex URL match', async () => { expect( isUrlSupported({ mediaType: 'image/png', url: 'https://images.example.com/cat.png', supportedUrls: { 'image/png': [/https:\/\/images\.example\.com\/.+/], }, }), ).toBe(true); }); it('should return true for exact media type and one of multiple regex URLs match', async () => { expect( isUrlSupported({ mediaType: 'image/png', url: 'https://another.com/img.png', supportedUrls: { 'image/png': [ /https:\/\/images\.example\.com\/.+/, /https:\/\/another\.com\/img\.png/, ], }, }), ).toBe(true); }); it('should return false for exact media type but URL mismatch', async () => { expect( isUrlSupported({ mediaType: 'text/plain', url: 'https://another.com', supportedUrls: { 'text/plain': [/https:\/\/example\.com/] }, }), ).toBe(false); }); it('should return false for URL match but media type mismatch', async () => { expect( isUrlSupported({ mediaType: 'image/png', // Different media type url: 'https://example.com', supportedUrls: { 'text/plain': [/https:\/\/example\.com/] }, }), ).toBe(false); }); }); describe('when the model supports URLs via wildcard media type (*)', () => { it('should return true for wildcard media type and exact URL match', async () => { expect( isUrlSupported({ mediaType: 'text/plain', url: 'https://example.com', supportedUrls: { '*': [/https:\/\/example\.com/] }, }), ).toBe(true); }); it('should return true for wildcard media type and regex URL match', async () => { expect( isUrlSupported({ mediaType: 'image/jpeg', url: 'https://images.example.com/dog.jpg', supportedUrls: { '*': [/https:\/\/images\.example\.com\/.+/] }, }), ).toBe(true); }); it('should return false for wildcard media type but URL mismatch', async () => { expect( isUrlSupported({ mediaType: 'video/mp4', url: 'https://another.com', supportedUrls: { '*': [/https:\/\/example\.com/] }, }), ).toBe(false); }); }); describe('when both specific and wildcard media types are defined', () => { const supportedUrls = { 'text/plain': [/https:\/\/text\.com/], '*': [/https:\/\/any\.com/], }; it('should return true if URL matches under specific media type', async () => { expect( isUrlSupported({ mediaType: 'text/plain', url: 'https://text.com', supportedUrls, }), ).toBe(true); }); it('should return true if URL matches under wildcard media type even if specific exists', async () => { // Assumes the logic checks specific first, then falls back to wildcard expect( isUrlSupported({ mediaType: 'text/plain', // Specific type exists url: 'https://any.com', // Matches wildcard supportedUrls, }), ).toBe(true); }); it('should return true if URL matches under wildcard for a non-specified media type', async () => { expect( isUrlSupported({ mediaType: 'image/png', // No specific entry for this type url: 'https://any.com', // Matches wildcard supportedUrls, }), ).toBe(true); }); it('should return false if URL matches neither specific nor wildcard', async () => { expect( isUrlSupported({ mediaType: 'text/plain', url: 'https://other.com', supportedUrls, }), ).toBe(false); }); it('should return false if URL does not match wildcard for a non-specified media type', async () => { expect( isUrlSupported({ mediaType: 'image/png', url: 'https://other.com', supportedUrls, }), ).toBe(false); }); }); describe('edge cases', () => { it('should return true if an empty URL matches a pattern', async () => { expect( isUrlSupported({ mediaType: 'text/plain', url: '', supportedUrls: { 'text/plain': [/.*/] }, // Matches any string, including empty }), ).toBe(true); }); it('should return false if an empty URL does not match a pattern', async () => { expect( isUrlSupported({ mediaType: 'text/plain', url: '', supportedUrls: { 'text/plain': [/https:\/\/.+/] }, // Requires non-empty string }), ).toBe(false); }); }); describe('case sensitivity', () => { it('should be case-insensitive for media types', async () => { expect( isUrlSupported({ mediaType: 'TEXT/PLAIN', // Uppercase url: 'https://example.com', supportedUrls: { 'text/plain': [/https:\/\/example\.com/] }, // Lowercase }), ).toBe(true); }); it('should handle case-insensitive regex for URLs if specified', async () => { expect( isUrlSupported({ mediaType: 'text/plain', url: 'https://EXAMPLE.com/path', // Uppercase domain supportedUrls: { 'text/plain': [/https:\/\/example\.com\/path/] }, }), ).toBe(true); }); it('should be case-insensitive for URL paths by default regex', async () => { expect( isUrlSupported({ mediaType: 'text/plain', url: 'https://example.com/PATH', // Uppercase path supportedUrls: { 'text/plain': [/https:\/\/example\.com\/path/] }, // Lowercase path in regex }), ).toBe(true); }); }); describe('wildcard subtypes in media types', () => { it('should return true for wildcard subtype match', async () => { expect( isUrlSupported({ mediaType: 'image/png', url: 'https://example.com', supportedUrls: { 'image/*': [/https:\/\/example\.com/] }, }), ).toBe(true); }); it('should use full wildcard "*" if subtype wildcard is not matched or supported', async () => { expect( isUrlSupported({ mediaType: 'image/png', url: 'https://any.com', supportedUrls: { 'image/*': [/https:\/\/images\.com/], // Doesn't match URL '*': [/https:\/\/any\.com/], // Matches URL }, }), ).toBe(true); }); }); describe('empty URL arrays for a media type', () => { it('should return false if the specific media type has an empty URL array', async () => { expect( isUrlSupported({ mediaType: 'text/plain', url: 'https://example.com', supportedUrls: { 'text/plain': [] }, }), ).toBe(false); }); it('should fall back to wildcard "*" if specific media type has empty array but wildcard matches', async () => { expect( isUrlSupported({ mediaType: 'text/plain', url: 'https://any.com', supportedUrls: { 'text/plain': [], '*': [/https:\/\/any\.com/], }, }), ).toBe(true); }); it('should return false if specific media type has empty array and wildcard does not match', async () => { expect( isUrlSupported({ mediaType: 'text/plain', url: 'https://another.com', supportedUrls: { 'text/plain': [], '*': [/https:\/\/any\.com/], }, }), ).toBe(false); }); }); }); --- File: /ai/packages/provider-utils/src/is-url-supported.ts --- /** * Checks if the given URL is supported natively by the model. * * @param mediaType - The media type of the URL. Case-sensitive. * @param url - The URL to check. * @param supportedUrls - A record where keys are case-sensitive media types (or '*') * and values are arrays of RegExp patterns for URLs. * * @returns `true` if the URL matches a pattern under the specific media type * or the wildcard '*', `false` otherwise. */ export function isUrlSupported({ mediaType, url, supportedUrls, }: { mediaType: string; url: string; supportedUrls: Record<string, RegExp[]>; }): boolean { // standardize media type and url to lower case url = url.toLowerCase(); mediaType = mediaType.toLowerCase(); return ( Object.entries(supportedUrls) // standardize supported url map into lowercase prefixes: .map(([key, value]) => { const mediaType = key.toLowerCase(); return mediaType === '*' || mediaType === '*/*' ? { mediaTypePrefix: '', regexes: value } : { mediaTypePrefix: mediaType.replace(/\*/, ''), regexes: value }; }) // gather all regexp pattern from matched media type prefixes: .filter(({ mediaTypePrefix }) => mediaType.startsWith(mediaTypePrefix)) .flatMap(({ regexes }) => regexes) // check if any pattern matches the url: .some(pattern => pattern.test(url)) ); } --- File: /ai/packages/provider-utils/src/load-api-key.ts --- import { LoadAPIKeyError } from '@ai-sdk/provider'; export function loadApiKey({ apiKey, environmentVariableName, apiKeyParameterName = 'apiKey', description, }: { apiKey: string | undefined; environmentVariableName: string; apiKeyParameterName?: string; description: string; }): string { if (typeof apiKey === 'string') { return apiKey; } if (apiKey != null) { throw new LoadAPIKeyError({ message: `${description} API key must be a string.`, }); } if (typeof process === 'undefined') { throw new LoadAPIKeyError({ message: `${description} API key is missing. Pass it using the '${apiKeyParameterName}' parameter. Environment variables is not supported in this environment.`, }); } apiKey = process.env[environmentVariableName]; if (apiKey == null) { throw new LoadAPIKeyError({ message: `${description} API key is missing. Pass it using the '${apiKeyParameterName}' parameter or the ${environmentVariableName} environment variable.`, }); } if (typeof apiKey !== 'string') { throw new LoadAPIKeyError({ message: `${description} API key must be a string. The value of the ${environmentVariableName} environment variable is not a string.`, }); } return apiKey; } --- File: /ai/packages/provider-utils/src/load-optional-setting.ts --- /** * Loads an optional `string` setting from the environment or a parameter. * * @param settingValue - The setting value. * @param environmentVariableName - The environment variable name. * @returns The setting value. */ export function loadOptionalSetting({ settingValue, environmentVariableName, }: { settingValue: string | undefined; environmentVariableName: string; }): string | undefined { if (typeof settingValue === 'string') { return settingValue; } if (settingValue != null || typeof process === 'undefined') { return undefined; } settingValue = process.env[environmentVariableName]; if (settingValue == null || typeof settingValue !== 'string') { return undefined; } return settingValue; } --- File: /ai/packages/provider-utils/src/load-setting.ts --- import { LoadSettingError } from '@ai-sdk/provider'; /** * Loads a `string` setting from the environment or a parameter. * * @param settingValue - The setting value. * @param environmentVariableName - The environment variable name. * @param settingName - The setting name. * @param description - The description of the setting. * @returns The setting value. */ export function loadSetting({ settingValue, environmentVariableName, settingName, description, }: { settingValue: string | undefined; environmentVariableName: string; settingName: string; description: string; }): string { if (typeof settingValue === 'string') { return settingValue; } if (settingValue != null) { throw new LoadSettingError({ message: `${description} setting must be a string.`, }); } if (typeof process === 'undefined') { throw new LoadSettingError({ message: `${description} setting is missing. ` + `Pass it using the '${settingName}' parameter. ` + `Environment variables is not supported in this environment.`, }); } settingValue = process.env[environmentVariableName]; if (settingValue == null) { throw new LoadSettingError({ message: `${description} setting is missing. ` + `Pass it using the '${settingName}' parameter ` + `or the ${environmentVariableName} environment variable.`, }); } if (typeof settingValue !== 'string') { throw new LoadSettingError({ message: `${description} setting must be a string. ` + `The value of the ${environmentVariableName} environment variable is not a string.`, }); } return settingValue; } --- File: /ai/packages/provider-utils/src/parse-json-event-stream.ts --- import { EventSourceMessage, EventSourceParserStream, } from 'eventsource-parser/stream'; import { ZodType } from 'zod/v4'; import { ParseResult, safeParseJSON } from './parse-json'; /** * Parses a JSON event stream into a stream of parsed JSON objects. */ export function parseJsonEventStream<T>({ stream, schema, }: { stream: ReadableStream<Uint8Array>; schema: ZodType<T>; }): ReadableStream<ParseResult<T>> { return stream .pipeThrough(new TextDecoderStream()) .pipeThrough(new EventSourceParserStream()) .pipeThrough( new TransformStream<EventSourceMessage, ParseResult<T>>({ async transform({ data }, controller) { // ignore the 'DONE' event that e.g. OpenAI sends: if (data === '[DONE]') { return; } controller.enqueue(await safeParseJSON({ text: data, schema })); }, }), ); } --- File: /ai/packages/provider-utils/src/parse-json.test.ts --- import { describe, it, expect } from 'vitest'; import { parseJSON, safeParseJSON, isParsableJson } from './parse-json'; import { z } from 'zod/v4'; import { JSONParseError, TypeValidationError } from '@ai-sdk/provider'; describe('parseJSON', () => { it('should parse basic JSON without schema', async () => { const result = await parseJSON({ text: '{"foo": "bar"}' }); expect(result).toEqual({ foo: 'bar' }); }); it('should parse JSON with schema validation', async () => { const schema = z.object({ foo: z.string() }); const result = await parseJSON({ text: '{"foo": "bar"}', schema }); expect(result).toEqual({ foo: 'bar' }); }); it('should throw JSONParseError for invalid JSON', async () => { await expect(() => parseJSON({ text: 'invalid json' })).rejects.toThrow( JSONParseError, ); }); it('should throw TypeValidationError for schema validation failures', async () => { const schema = z.object({ foo: z.number() }); await expect(() => parseJSON({ text: '{"foo": "bar"}', schema }), ).rejects.toThrow(TypeValidationError); }); }); describe('safeParseJSON', () => { it('should safely parse basic JSON without schema and include rawValue', async () => { const result = await safeParseJSON({ text: '{"foo": "bar"}' }); expect(result).toEqual({ success: true, value: { foo: 'bar' }, rawValue: { foo: 'bar' }, }); }); it('should preserve rawValue even after schema transformation', async () => { const schema = z.object({ count: z.coerce.number(), }); const result = await safeParseJSON({ text: '{"count": "42"}', schema, }); expect(result).toEqual({ success: true, value: { count: 42 }, rawValue: { count: '42' }, }); }); it('should handle failed parsing with error details', async () => { const result = await safeParseJSON({ text: 'invalid json' }); expect(result).toEqual({ success: false, error: expect.any(JSONParseError), }); }); it('should handle schema validation failures', async () => { const schema = z.object({ age: z.number() }); const result = await safeParseJSON({ text: '{"age": "twenty"}', schema, }); expect(result).toEqual({ success: false, error: expect.any(TypeValidationError), rawValue: { age: 'twenty' }, }); }); it('should handle nested objects and preserve raw values', async () => { const schema = z.object({ user: z.object({ id: z.string().transform(val => parseInt(val, 10)), name: z.string(), }), }); const result = await safeParseJSON({ text: '{"user": {"id": "123", "name": "John"}}', schema: schema as any, }); expect(result).toEqual({ success: true, value: { user: { id: 123, name: 'John' } }, rawValue: { user: { id: '123', name: 'John' } }, }); }); it('should handle arrays and preserve raw values', async () => { const schema = z.array(z.string().transform(val => val.toUpperCase())); const result = await safeParseJSON({ text: '["hello", "world"]', schema, }); expect(result).toEqual({ success: true, value: ['HELLO', 'WORLD'], rawValue: ['hello', 'world'], }); }); it('should handle discriminated unions in schema', async () => { const schema = z.discriminatedUnion('type', [ z.object({ type: z.literal('text'), content: z.string() }), z.object({ type: z.literal('number'), value: z.number() }), ]); const result = await safeParseJSON({ text: '{"type": "text", "content": "hello"}', schema, }); expect(result).toEqual({ success: true, value: { type: 'text', content: 'hello' }, rawValue: { type: 'text', content: 'hello' }, }); }); it('should handle nullable fields in schema', async () => { const schema = z.object({ id: z.string().nullish(), data: z.string(), }); const result = await safeParseJSON({ text: '{"id": null, "data": "test"}', schema, }); expect(result).toEqual({ success: true, value: { id: null, data: 'test' }, rawValue: { id: null, data: 'test' }, }); }); it('should handle union types in schema', async () => { const schema = z.object({ value: z.union([z.string(), z.number()]), }); const result1 = await safeParseJSON({ text: '{"value": "test"}', schema, }); const result2 = await safeParseJSON({ text: '{"value": 123}', schema, }); expect(result1).toEqual({ success: true, value: { value: 'test' }, rawValue: { value: 'test' }, }); expect(result2).toEqual({ success: true, value: { value: 123 }, rawValue: { value: 123 }, }); }); }); describe('isParsableJson', () => { it('should return true for valid JSON', () => { expect(isParsableJson('{"foo": "bar"}')).toBe(true); expect(isParsableJson('[1, 2, 3]')).toBe(true); expect(isParsableJson('"hello"')).toBe(true); }); it('should return false for invalid JSON', () => { expect(isParsableJson('invalid')).toBe(false); expect(isParsableJson('{foo: "bar"}')).toBe(false); expect(isParsableJson('{"foo": }')).toBe(false); }); }); --- File: /ai/packages/provider-utils/src/parse-json.ts --- import { JSONParseError, JSONValue, TypeValidationError, } from '@ai-sdk/provider'; import * as z3 from 'zod/v3'; import * as z4 from 'zod/v4'; import { secureJsonParse } from './secure-json-parse'; import { safeValidateTypes, validateTypes } from './validate-types'; import { Validator } from './validator'; /** * Parses a JSON string into an unknown object. * * @param text - The JSON string to parse. * @returns {JSONValue} - The parsed JSON object. */ export async function parseJSON(options: { text: string; schema?: undefined; }): Promise<JSONValue>; /** * Parses a JSON string into a strongly-typed object using the provided schema. * * @template T - The type of the object to parse the JSON into. * @param {string} text - The JSON string to parse. * @param {Validator<T>} schema - The schema to use for parsing the JSON. * @returns {Promise<T>} - The parsed object. */ export async function parseJSON<T>(options: { text: string; schema: z4.core.$ZodType<T> | z3.Schema<T> | Validator<T>; }): Promise<T>; export async function parseJSON<T>({ text, schema, }: { text: string; schema?: z4.core.$ZodType<T> | z3.Schema<T> | Validator<T>; }): Promise<T> { try { const value = secureJsonParse(text); if (schema == null) { return value; } return validateTypes<T>({ value, schema }); } catch (error) { if ( JSONParseError.isInstance(error) || TypeValidationError.isInstance(error) ) { throw error; } throw new JSONParseError({ text, cause: error }); } } export type ParseResult<T> = | { success: true; value: T; rawValue: unknown } | { success: false; error: JSONParseError | TypeValidationError; rawValue: unknown; }; /** * Safely parses a JSON string and returns the result as an object of type `unknown`. * * @param text - The JSON string to parse. * @returns {Promise<object>} Either an object with `success: true` and the parsed data, or an object with `success: false` and the error that occurred. */ export async function safeParseJSON(options: { text: string; schema?: undefined; }): Promise<ParseResult<JSONValue>>; /** * Safely parses a JSON string into a strongly-typed object, using a provided schema to validate the object. * * @template T - The type of the object to parse the JSON into. * @param {string} text - The JSON string to parse. * @param {Validator<T>} schema - The schema to use for parsing the JSON. * @returns An object with either a `success` flag and the parsed and typed data, or a `success` flag and an error object. */ export async function safeParseJSON<T>(options: { text: string; schema: z4.core.$ZodType<T> | z3.Schema<T> | Validator<T>; }): Promise<ParseResult<T>>; export async function safeParseJSON<T>({ text, schema, }: { text: string; schema?: z4.core.$ZodType<T> | z3.Schema<T> | Validator<T>; }): Promise<ParseResult<T>> { try { const value = secureJsonParse(text); if (schema == null) { return { success: true, value: value as T, rawValue: value }; } return await safeValidateTypes<T>({ value, schema }); } catch (error) { return { success: false, error: JSONParseError.isInstance(error) ? error : new JSONParseError({ text, cause: error }), rawValue: undefined, }; } } export function isParsableJson(input: string): boolean { try { secureJsonParse(input); return true; } catch { return false; } } --- File: /ai/packages/provider-utils/src/parse-provider-options.ts --- import { InvalidArgumentError } from '@ai-sdk/provider'; import { safeValidateTypes } from './validate-types'; import { z } from 'zod/v4'; export async function parseProviderOptions<T>({ provider, providerOptions, schema, }: { provider: string; providerOptions: Record<string, unknown> | undefined; schema: z.core.$ZodType<T, any>; }): Promise<T | undefined> { if (providerOptions?.[provider] == null) { return undefined; } const parsedProviderOptions = await safeValidateTypes<T | undefined>({ value: providerOptions[provider], schema, }); if (!parsedProviderOptions.success) { throw new InvalidArgumentError({ argument: 'providerOptions', message: `invalid ${provider} provider options`, cause: parsedProviderOptions.error, }); } return parsedProviderOptions.value; } --- File: /ai/packages/provider-utils/src/post-to-api.ts --- import { APICallError } from '@ai-sdk/provider'; import { extractResponseHeaders } from './extract-response-headers'; import { FetchFunction } from './fetch-function'; import { handleFetchError } from './handle-fetch-error'; import { isAbortError } from './is-abort-error'; import { removeUndefinedEntries } from './remove-undefined-entries'; import { ResponseHandler } from './response-handler'; // use function to allow for mocking in tests: const getOriginalFetch = () => globalThis.fetch; export const postJsonToApi = async <T>({ url, headers, body, failedResponseHandler, successfulResponseHandler, abortSignal, fetch, }: { url: string; headers?: Record<string, string | undefined>; body: unknown; failedResponseHandler: ResponseHandler<APICallError>; successfulResponseHandler: ResponseHandler<T>; abortSignal?: AbortSignal; fetch?: FetchFunction; }) => postToApi({ url, headers: { 'Content-Type': 'application/json', ...headers, }, body: { content: JSON.stringify(body), values: body, }, failedResponseHandler, successfulResponseHandler, abortSignal, fetch, }); export const postFormDataToApi = async <T>({ url, headers, formData, failedResponseHandler, successfulResponseHandler, abortSignal, fetch, }: { url: string; headers?: Record<string, string | undefined>; formData: FormData; failedResponseHandler: ResponseHandler<APICallError>; successfulResponseHandler: ResponseHandler<T>; abortSignal?: AbortSignal; fetch?: FetchFunction; }) => postToApi({ url, headers, body: { content: formData, values: Object.fromEntries((formData as any).entries()), }, failedResponseHandler, successfulResponseHandler, abortSignal, fetch, }); export const postToApi = async <T>({ url, headers = {}, body, successfulResponseHandler, failedResponseHandler, abortSignal, fetch = getOriginalFetch(), }: { url: string; headers?: Record<string, string | undefined>; body: { content: string | FormData | Uint8Array; values: unknown; }; failedResponseHandler: ResponseHandler<Error>; successfulResponseHandler: ResponseHandler<T>; abortSignal?: AbortSignal; fetch?: FetchFunction; }) => { try { const response = await fetch(url, { method: 'POST', headers: removeUndefinedEntries(headers), body: body.content, signal: abortSignal, }); const responseHeaders = extractResponseHeaders(response); if (!response.ok) { let errorInformation: { value: Error; responseHeaders?: Record<string, string> | undefined; }; try { errorInformation = await failedResponseHandler({ response, url, requestBodyValues: body.values, }); } catch (error) { if (isAbortError(error) || APICallError.isInstance(error)) { throw error; } throw new APICallError({ message: 'Failed to process error response', cause: error, statusCode: response.status, url, responseHeaders, requestBodyValues: body.values, }); } throw errorInformation.value; } try { return await successfulResponseHandler({ response, url, requestBodyValues: body.values, }); } catch (error) { if (error instanceof Error) { if (isAbortError(error) || APICallError.isInstance(error)) { throw error; } } throw new APICallError({ message: 'Failed to process successful response', cause: error, statusCode: response.status, url, responseHeaders, requestBodyValues: body.values, }); } } catch (error) { throw handleFetchError({ error, url, requestBodyValues: body.values }); } }; --- File: /ai/packages/provider-utils/src/provider-defined-tool-factory.ts --- import { tool, Tool, ToolExecuteFunction } from './types/tool'; import { FlexibleSchema } from './schema'; export type ProviderDefinedToolFactory<INPUT, ARGS extends object> = <OUTPUT>( options: ARGS & { execute?: ToolExecuteFunction<INPUT, OUTPUT>; toModelOutput?: Tool<INPUT, OUTPUT>['toModelOutput']; onInputStart?: Tool<INPUT, OUTPUT>['onInputStart']; onInputDelta?: Tool<INPUT, OUTPUT>['onInputDelta']; onInputAvailable?: Tool<INPUT, OUTPUT>['onInputAvailable']; }, ) => Tool<INPUT, OUTPUT>; export function createProviderDefinedToolFactory<INPUT, ARGS extends object>({ id, name, inputSchema, }: { id: `${string}.${string}`; name: string; inputSchema: FlexibleSchema<INPUT>; }): ProviderDefinedToolFactory<INPUT, ARGS> { return <OUTPUT>({ execute, outputSchema, toModelOutput, onInputStart, onInputDelta, onInputAvailable, ...args }: ARGS & { execute?: ToolExecuteFunction<INPUT, OUTPUT>; outputSchema?: FlexibleSchema<OUTPUT>; toModelOutput?: Tool<INPUT, OUTPUT>['toModelOutput']; onInputStart?: Tool<INPUT, OUTPUT>['onInputStart']; onInputDelta?: Tool<INPUT, OUTPUT>['onInputDelta']; onInputAvailable?: Tool<INPUT, OUTPUT>['onInputAvailable']; }): Tool<INPUT, OUTPUT> => tool({ type: 'provider-defined', id, name, args, inputSchema, outputSchema, execute, toModelOutput, onInputStart, onInputDelta, onInputAvailable, }); } export type ProviderDefinedToolFactoryWithOutputSchema< INPUT, OUTPUT, ARGS extends object, > = ( options: ARGS & { execute?: ToolExecuteFunction<INPUT, OUTPUT>; toModelOutput?: Tool<INPUT, OUTPUT>['toModelOutput']; onInputStart?: Tool<INPUT, OUTPUT>['onInputStart']; onInputDelta?: Tool<INPUT, OUTPUT>['onInputDelta']; onInputAvailable?: Tool<INPUT, OUTPUT>['onInputAvailable']; }, ) => Tool<INPUT, OUTPUT>; export function createProviderDefinedToolFactoryWithOutputSchema< INPUT, OUTPUT, ARGS extends object, >({ id, name, inputSchema, outputSchema, }: { id: `${string}.${string}`; name: string; inputSchema: FlexibleSchema<INPUT>; outputSchema: FlexibleSchema<OUTPUT>; }): ProviderDefinedToolFactoryWithOutputSchema<INPUT, OUTPUT, ARGS> { return ({ execute, toModelOutput, onInputStart, onInputDelta, onInputAvailable, ...args }: ARGS & { execute?: ToolExecuteFunction<INPUT, OUTPUT>; toModelOutput?: Tool<INPUT, OUTPUT>['toModelOutput']; onInputStart?: Tool<INPUT, OUTPUT>['onInputStart']; onInputDelta?: Tool<INPUT, OUTPUT>['onInputDelta']; onInputAvailable?: Tool<INPUT, OUTPUT>['onInputAvailable']; }): Tool<INPUT, OUTPUT> => tool({ type: 'provider-defined', id, name, args, inputSchema, outputSchema, execute, toModelOutput, onInputStart, onInputDelta, onInputAvailable, }); } --- File: /ai/packages/provider-utils/src/remove-undefined-entries.test.ts --- import { expect, it } from 'vitest'; import { removeUndefinedEntries } from './remove-undefined-entries'; it('should remove undefined entries from record', () => { const input = { a: 1, b: undefined, c: 'test', d: undefined, }; expect(removeUndefinedEntries(input)).toEqual({ a: 1, c: 'test', }); }); it('should handle empty object', () => { const input = {}; expect(removeUndefinedEntries(input)).toEqual({}); }); it('should handle object with all undefined values', () => { const input = { a: undefined, b: undefined, }; expect(removeUndefinedEntries(input)).toEqual({}); }); it('should remove null values', () => { // Both null and undefined will be removed. const input = { a: null, b: undefined, c: 'test', }; expect(removeUndefinedEntries(input)).toEqual({ c: 'test', }); }); it('should preserve falsy values except null and undefined', () => { // Only false, 0, and '' are preserved. const input = { a: false, b: 0, c: '', d: undefined, e: null, }; expect(removeUndefinedEntries(input)).toEqual({ a: false, b: 0, c: '', }); }); --- File: /ai/packages/provider-utils/src/remove-undefined-entries.ts --- /** * Removes entries from a record where the value is null or undefined. * @param record - The input object whose entries may be null or undefined. * @returns A new object containing only entries with non-null and non-undefined values. */ export function removeUndefinedEntries<T>( record: Record<string, T | undefined>, ): Record<string, T> { return Object.fromEntries( Object.entries(record).filter(([_key, value]) => value != null), ) as Record<string, T>; } --- File: /ai/packages/provider-utils/src/resolve.test.ts --- import { describe, it, expect } from 'vitest'; import { resolve, Resolvable } from './resolve'; describe('resolve', () => { // Test raw values it('should resolve raw values', async () => { const value: Resolvable<number> = 42; expect(await resolve(value)).toBe(42); }); it('should resolve raw objects', async () => { const value: Resolvable<object> = { foo: 'bar' }; expect(await resolve(value)).toEqual({ foo: 'bar' }); }); // Test promises it('should resolve promises', async () => { const value: Resolvable<string> = Promise.resolve('hello'); expect(await resolve(value)).toBe('hello'); }); it('should resolve rejected promises', async () => { const value: Resolvable<string> = Promise.reject(new Error('test error')); await expect(resolve(value)).rejects.toThrow('test error'); }); // Test synchronous functions it('should resolve synchronous functions', async () => { const value: Resolvable<number> = () => 42; expect(await resolve(value)).toBe(42); }); it('should resolve synchronous functions returning objects', async () => { const value: Resolvable<object> = () => ({ foo: 'bar' }); expect(await resolve(value)).toEqual({ foo: 'bar' }); }); // Test async functions it('should resolve async functions', async () => { const value: Resolvable<string> = async () => 'hello'; expect(await resolve(value)).toBe('hello'); }); it('should resolve async functions returning promises', async () => { const value: Resolvable<number> = () => Promise.resolve(42); expect(await resolve(value)).toBe(42); }); it('should handle async function rejections', async () => { const value: Resolvable<string> = async () => { throw new Error('async error'); }; await expect(resolve(value)).rejects.toThrow('async error'); }); // Test edge cases it('should handle null', async () => { const value: Resolvable<null> = null; expect(await resolve(value)).toBe(null); }); it('should handle undefined', async () => { const value: Resolvable<undefined> = undefined; expect(await resolve(value)).toBe(undefined); }); // Test with complex objects it('should resolve nested objects', async () => { const value: Resolvable<{ nested: { value: number } }> = { nested: { value: 42 }, }; expect(await resolve(value)).toEqual({ nested: { value: 42 } }); }); // Test resolving objects as frequently used in headers as a common example describe('resolve headers', () => { it('should resolve header objects', async () => { const headers = { 'Content-Type': 'application/json' }; expect(await resolve(headers)).toEqual(headers); }); it('should resolve header functions', async () => { const headers = () => ({ Authorization: 'Bearer token' }); expect(await resolve(headers)).toEqual({ Authorization: 'Bearer token' }); }); it('should resolve async header functions', async () => { const headers = async () => ({ 'X-Custom': 'value' }); expect(await resolve(headers)).toEqual({ 'X-Custom': 'value' }); }); it('should resolve header promises', async () => { const headers = Promise.resolve({ Accept: 'application/json' }); expect(await resolve(headers)).toEqual({ Accept: 'application/json' }); }); it('should call async header functions each time when resolved multiple times', async () => { let counter = 0; const headers = async () => ({ 'X-Request-Number': String(++counter) }); // Resolve the same headers function multiple times expect(await resolve(headers)).toEqual({ 'X-Request-Number': '1' }); expect(await resolve(headers)).toEqual({ 'X-Request-Number': '2' }); expect(await resolve(headers)).toEqual({ 'X-Request-Number': '3' }); }); }); // Test type inference it('should maintain type information', async () => { interface User { id: number; name: string; } const userPromise: Resolvable<User> = Promise.resolve({ id: 1, name: 'Test User', }); const result = await resolve(userPromise); // TypeScript should recognize result as User type expect(result.id).toBe(1); expect(result.name).toBe('Test User'); }); }); --- File: /ai/packages/provider-utils/src/resolve.ts --- export type Resolvable<T> = | T // Raw value | Promise<T> // Promise of value | (() => T) // Function returning value | (() => Promise<T>); // Function returning promise of value /** * Resolves a value that could be a raw value, a Promise, a function returning a value, * or a function returning a Promise. */ export async function resolve<T>(value: Resolvable<T>): Promise<T> { // If it's a function, call it to get the value/promise if (typeof value === 'function') { value = (value as Function)(); } // Otherwise just resolve whatever we got (value or promise) return Promise.resolve(value as T); } --- File: /ai/packages/provider-utils/src/response-handler.test.ts --- import { z } from 'zod/v4'; import { convertArrayToReadableStream, convertReadableStreamToArray, } from './test'; import { createJsonResponseHandler, createJsonStreamResponseHandler, createBinaryResponseHandler, createStatusCodeErrorResponseHandler, } from './response-handler'; describe('createJsonStreamResponseHandler', () => { it('should return a stream of complete json chunks', async () => { const handler = createJsonStreamResponseHandler( z.object({ a: z.number() }), ); const { value: stream } = await handler({ url: 'some url', requestBodyValues: {}, response: new Response( convertArrayToReadableStream([ JSON.stringify({ a: 1 }) + '\n', JSON.stringify({ a: 2 }) + '\n', ]).pipeThrough(new TextEncoderStream()), ), }); expect(await convertReadableStreamToArray(stream)).toStrictEqual([ { success: true, value: { a: 1 }, rawValue: { a: 1 } }, { success: true, value: { a: 2 }, rawValue: { a: 2 } }, ]); }); it('should return a stream of partial json chunks', async () => { const handler = createJsonStreamResponseHandler( z.object({ a: z.number() }), ); const { value: stream } = await handler({ url: 'some url', requestBodyValues: {}, response: new Response( convertArrayToReadableStream([ '{ "a":', // start '1 }\n', // end ]).pipeThrough(new TextEncoderStream()), ), }); expect(await convertReadableStreamToArray(stream)).toStrictEqual([ { success: true, value: { a: 1 }, rawValue: { a: 1 } }, ]); }); }); describe('createJsonResponseHandler', () => { it('should return both parsed value and rawValue', async () => { const responseSchema = z.object({ name: z.string(), age: z.number(), }); const rawData = { name: 'John', age: 30, extraField: 'ignored', }; const response = new Response(JSON.stringify(rawData)); const handler = createJsonResponseHandler(responseSchema); const result = await handler({ url: 'test-url', requestBodyValues: {}, response, }); expect(result.value).toEqual({ name: 'John', age: 30, }); expect(result.rawValue).toEqual(rawData); }); }); describe('createBinaryResponseHandler', () => { it('should handle binary response successfully', async () => { const binaryData = new Uint8Array([1, 2, 3, 4]); const response = new Response(binaryData); const handler = createBinaryResponseHandler(); const result = await handler({ url: 'test-url', requestBodyValues: {}, response, }); expect(result.value).toBeInstanceOf(Uint8Array); expect(result.value).toEqual(binaryData); }); it('should throw APICallError when response body is null', async () => { const response = new Response(null); const handler = createBinaryResponseHandler(); await expect( handler({ url: 'test-url', requestBodyValues: {}, response, }), ).rejects.toThrow('Response body is empty'); }); }); describe('createStatusCodeErrorResponseHandler', () => { it('should create error with status text and response body', async () => { const response = new Response('Error message', { status: 404, statusText: 'Not Found', }); const handler = createStatusCodeErrorResponseHandler(); const result = await handler({ url: 'test-url', requestBodyValues: { some: 'data' }, response, }); expect(result.value.message).toBe('Not Found'); expect(result.value.statusCode).toBe(404); expect(result.value.responseBody).toBe('Error message'); expect(result.value.url).toBe('test-url'); expect(result.value.requestBodyValues).toEqual({ some: 'data' }); }); }); --- File: /ai/packages/provider-utils/src/response-handler.ts --- import { APICallError, EmptyResponseBodyError } from '@ai-sdk/provider'; import { ZodType } from 'zod/v4'; import { extractResponseHeaders } from './extract-response-headers'; import { parseJSON, ParseResult, safeParseJSON } from './parse-json'; import { parseJsonEventStream } from './parse-json-event-stream'; export type ResponseHandler<RETURN_TYPE> = (options: { url: string; requestBodyValues: unknown; response: Response; }) => PromiseLike<{ value: RETURN_TYPE; rawValue?: unknown; responseHeaders?: Record<string, string>; }>; export const createJsonErrorResponseHandler = <T>({ errorSchema, errorToMessage, isRetryable, }: { errorSchema: ZodType<T>; errorToMessage: (error: T) => string; isRetryable?: (response: Response, error?: T) => boolean; }): ResponseHandler<APICallError> => async ({ response, url, requestBodyValues }) => { const responseBody = await response.text(); const responseHeaders = extractResponseHeaders(response); // Some providers return an empty response body for some errors: if (responseBody.trim() === '') { return { responseHeaders, value: new APICallError({ message: response.statusText, url, requestBodyValues, statusCode: response.status, responseHeaders, responseBody, isRetryable: isRetryable?.(response), }), }; } // resilient parsing in case the response is not JSON or does not match the schema: try { const parsedError = await parseJSON({ text: responseBody, schema: errorSchema, }); return { responseHeaders, value: new APICallError({ message: errorToMessage(parsedError), url, requestBodyValues, statusCode: response.status, responseHeaders, responseBody, data: parsedError, isRetryable: isRetryable?.(response, parsedError), }), }; } catch (parseError) { return { responseHeaders, value: new APICallError({ message: response.statusText, url, requestBodyValues, statusCode: response.status, responseHeaders, responseBody, isRetryable: isRetryable?.(response), }), }; } }; export const createEventSourceResponseHandler = <T>( chunkSchema: ZodType<T>, ): ResponseHandler<ReadableStream<ParseResult<T>>> => async ({ response }: { response: Response }) => { const responseHeaders = extractResponseHeaders(response); if (response.body == null) { throw new EmptyResponseBodyError({}); } return { responseHeaders, value: parseJsonEventStream({ stream: response.body, schema: chunkSchema, }), }; }; export const createJsonStreamResponseHandler = <T>( chunkSchema: ZodType<T>, ): ResponseHandler<ReadableStream<ParseResult<T>>> => async ({ response }: { response: Response }) => { const responseHeaders = extractResponseHeaders(response); if (response.body == null) { throw new EmptyResponseBodyError({}); } let buffer = ''; return { responseHeaders, value: response.body.pipeThrough(new TextDecoderStream()).pipeThrough( new TransformStream<string, ParseResult<T>>({ async transform(chunkText, controller) { if (chunkText.endsWith('\n')) { controller.enqueue( await safeParseJSON({ text: buffer + chunkText, schema: chunkSchema, }), ); buffer = ''; } else { buffer += chunkText; } }, }), ), }; }; export const createJsonResponseHandler = <T>(responseSchema: ZodType<T>): ResponseHandler<T> => async ({ response, url, requestBodyValues }) => { const responseBody = await response.text(); const parsedResult = await safeParseJSON({ text: responseBody, schema: responseSchema, }); const responseHeaders = extractResponseHeaders(response); if (!parsedResult.success) { throw new APICallError({ message: 'Invalid JSON response', cause: parsedResult.error, statusCode: response.status, responseHeaders, responseBody, url, requestBodyValues, }); } return { responseHeaders, value: parsedResult.value, rawValue: parsedResult.rawValue, }; }; export const createBinaryResponseHandler = (): ResponseHandler<Uint8Array> => async ({ response, url, requestBodyValues }) => { const responseHeaders = extractResponseHeaders(response); if (!response.body) { throw new APICallError({ message: 'Response body is empty', url, requestBodyValues, statusCode: response.status, responseHeaders, responseBody: undefined, }); } try { const buffer = await response.arrayBuffer(); return { responseHeaders, value: new Uint8Array(buffer), }; } catch (error) { throw new APICallError({ message: 'Failed to read response as array buffer', url, requestBodyValues, statusCode: response.status, responseHeaders, responseBody: undefined, cause: error, }); } }; export const createStatusCodeErrorResponseHandler = (): ResponseHandler<APICallError> => async ({ response, url, requestBodyValues }) => { const responseHeaders = extractResponseHeaders(response); const responseBody = await response.text(); return { responseHeaders, value: new APICallError({ message: response.statusText, url, requestBodyValues: requestBodyValues as Record<string, unknown>, statusCode: response.status, responseHeaders, responseBody, }), }; }; --- File: /ai/packages/provider-utils/src/schema.ts --- import { Validator, validatorSymbol, type ValidationResult } from './validator'; import { JSONSchema7 } from '@ai-sdk/provider'; import * as z3 from 'zod/v3'; import * as z4 from 'zod/v4'; import { zodSchema } from './zod-schema'; /** * Used to mark schemas so we can support both Zod and custom schemas. */ const schemaSymbol = Symbol.for('vercel.ai.schema'); export type Schema<OBJECT = unknown> = Validator<OBJECT> & { /** * Used to mark schemas so we can support both Zod and custom schemas. */ [schemaSymbol]: true; /** * Schema type for inference. */ _type: OBJECT; /** * The JSON Schema for the schema. It is passed to the providers. */ readonly jsonSchema: JSONSchema7; }; export type FlexibleSchema<T> = z4.core.$ZodType<T> | z3.Schema<T> | Schema<T>; export type InferSchema<SCHEMA> = SCHEMA extends z3.Schema ? z3.infer<SCHEMA> : SCHEMA extends z4.core.$ZodType ? z4.infer<SCHEMA> : SCHEMA extends Schema<infer T> ? T : never; /** * Create a schema using a JSON Schema. * * @param jsonSchema The JSON Schema for the schema. * @param options.validate Optional. A validation function for the schema. */ export function jsonSchema<OBJECT = unknown>( jsonSchema: JSONSchema7, { validate, }: { validate?: ( value: unknown, ) => ValidationResult<OBJECT> | PromiseLike<ValidationResult<OBJECT>>; } = {}, ): Schema<OBJECT> { return { [schemaSymbol]: true, _type: undefined as OBJECT, // should never be used directly [validatorSymbol]: true, jsonSchema, validate, }; } function isSchema(value: unknown): value is Schema { return ( typeof value === 'object' && value !== null && schemaSymbol in value && value[schemaSymbol] === true && 'jsonSchema' in value && 'validate' in value ); } export function asSchema<OBJECT>( schema: | z4.core.$ZodType<OBJECT, any> | z3.Schema<OBJECT, z3.ZodTypeDef, any> | Schema<OBJECT> | undefined, ): Schema<OBJECT> { return schema == null ? jsonSchema({ properties: {}, additionalProperties: false, }) : isSchema(schema) ? schema : zodSchema(schema); } --- File: /ai/packages/provider-utils/src/secure-json-parse.test.ts --- // Licensed under BSD-3-Clause (this file only) // Code adapted from https://github.com/fastify/secure-json-parse/blob/783fcb1b5434709466759847cec974381939673a/test/index.test.js // // Copyright (c) Vercel, Inc. (https://vercel.com) // Copyright (c) 2019 The Fastify Team // Copyright (c) 2019, Sideway Inc, and project contributors // All rights reserved. // // The complete list of contributors can be found at: // - https://github.com/hapijs/bourne/graphs/contributors // - https://github.com/fastify/secure-json-parse/graphs/contributors // - https://github.com/vercel/ai/commits/main/packages/provider-utils/src/secure-parse-json.test.ts // // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. // // 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. // // 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import { describe, it, expect } from 'vitest'; import { secureJsonParse } from './secure-json-parse'; describe('secureJsonParse', () => { it('parses object string', () => { expect(secureJsonParse('{"a": 5, "b": 6}')).toStrictEqual( JSON.parse('{"a": 5, "b": 6}'), ); }); it('parses null string', () => { expect(secureJsonParse('null')).toStrictEqual(JSON.parse('null')); }); it('parses 0 string', () => { expect(secureJsonParse('0')).toStrictEqual(JSON.parse('0')); }); it('parses string string', () => { expect(secureJsonParse('"X"')).toStrictEqual(JSON.parse('"X"')); }); it('errors on constructor property', () => { const text = '{ "a": 5, "b": 6, "constructor": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }'; expect(() => secureJsonParse(text)).toThrow(SyntaxError); }); it('errors on proto property', () => { const text = '{ "a": 5, "b": 6, "__proto__": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }'; expect(() => secureJsonParse(text)).toThrow(SyntaxError); }); }); --- File: /ai/packages/provider-utils/src/secure-json-parse.ts --- // Licensed under BSD-3-Clause (this file only) // Code adapted from https://github.com/fastify/secure-json-parse/blob/783fcb1b5434709466759847cec974381939673a/index.js // // Copyright (c) Vercel, Inc. (https://vercel.com) // Copyright (c) 2019 The Fastify Team // Copyright (c) 2019, Sideway Inc, and project contributors // All rights reserved. // // The complete list of contributors can be found at: // - https://github.com/hapijs/bourne/graphs/contributors // - https://github.com/fastify/secure-json-parse/graphs/contributors // - https://github.com/vercel/ai/commits/main/packages/provider-utils/src/secure-parse-json.ts // // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. // // 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. // // 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. const suspectProtoRx = /"__proto__"\s*:/; const suspectConstructorRx = /"constructor"\s*:/; function _parse(text: string) { // Parse normally const obj = JSON.parse(text); // Ignore null and non-objects if (obj === null || typeof obj !== 'object') { return obj; } if ( suspectProtoRx.test(text) === false && suspectConstructorRx.test(text) === false ) { return obj; } // Scan result for proto keys return filter(obj); } function filter(obj: any) { let next = [obj]; while (next.length) { const nodes = next; next = []; for (const node of nodes) { if (Object.prototype.hasOwnProperty.call(node, '__proto__')) { throw new SyntaxError('Object contains forbidden prototype property'); } if ( Object.prototype.hasOwnProperty.call(node, 'constructor') && Object.prototype.hasOwnProperty.call(node.constructor, 'prototype') ) { throw new SyntaxError('Object contains forbidden prototype property'); } for (const key in node) { const value = node[key]; if (value && typeof value === 'object') { next.push(value); } } } } return obj; } export function secureJsonParse(text: string) { // Performance optimization, see https://github.com/fastify/secure-json-parse/pull/90 const { stackTraceLimit } = Error; Error.stackTraceLimit = 0; try { return _parse(text); } finally { Error.stackTraceLimit = stackTraceLimit; } } --- File: /ai/packages/provider-utils/src/uint8-utils.ts --- // btoa and atob need to be invoked as a function call, not as a method call. // Otherwise CloudFlare will throw a // "TypeError: Illegal invocation: function called with incorrect this reference" const { btoa, atob } = globalThis; export function convertBase64ToUint8Array(base64String: string) { const base64Url = base64String.replace(/-/g, '+').replace(/_/g, '/'); const latin1string = atob(base64Url); return Uint8Array.from(latin1string, byte => byte.codePointAt(0)!); } export function convertUint8ArrayToBase64(array: Uint8Array): string { let latin1string = ''; // Note: regular for loop to support older JavaScript versions that // do not support for..of on Uint8Array for (let i = 0; i < array.length; i++) { latin1string += String.fromCodePoint(array[i]); } return btoa(latin1string); } export function convertToBase64(value: string | Uint8Array): string { return value instanceof Uint8Array ? convertUint8ArrayToBase64(value) : value; } --- File: /ai/packages/provider-utils/src/validate-types.test.ts --- import { TypeValidationError } from '@ai-sdk/provider'; import type { StandardSchemaV1 } from '@standard-schema/spec'; import { validateTypes, safeValidateTypes } from './validate-types'; import { validator } from './validator'; const customSchema: StandardSchemaV1<{ name: string; age: number }> = { '~standard': { version: 1, vendor: 'custom', validate: async (value: any) => { return typeof value === 'object' && value !== null && 'name' in value && typeof value.name === 'string' && 'age' in value && typeof value.age === 'number' ? { value } : { issues: [new Error('Invalid input')] }; }, }, }; const customValidator = validator<{ name: string; age: number }>(async value => typeof value === 'object' && value !== null && 'name' in value && typeof value.name === 'string' && 'age' in value && typeof value.age === 'number' ? { success: true, value: value as { name: string; age: number } } : { success: false, error: new TypeValidationError({ value, cause: [new Error('Invalid input')], }), }, ); describe('validateTypes', () => { describe.each([ ['Custom schema', customSchema], ['Custom validator', customValidator], ])('using %s', (_, schema) => { it('should return validated object for valid input', async () => { const input = { name: 'John', age: 30 }; expect(await validateTypes({ value: input, schema })).toEqual(input); }); it('should throw TypeValidationError for invalid input', async () => { const input = { name: 'John', age: '30' }; try { await validateTypes({ value: input, schema, }); expect.fail('Expected TypeValidationError to be thrown'); } catch (error) { expect(error).toBeInstanceOf(TypeValidationError); const typedError = error as TypeValidationError; expect({ name: typedError.name, value: typedError.value, cause: typedError.cause, message: typedError.message, }).toStrictEqual({ name: 'AI_TypeValidationError', value: input, cause: [expect.any(Error)], message: expect.stringContaining('Type validation failed'), }); } }); }); }); describe('safeValidateTypes', () => { describe.each([ ['Custom schema', customSchema], ['Custom validator', customValidator], ])('using %s', (_, schema) => { it('should return validated object for valid input', async () => { const input = { name: 'John', age: 30 }; const result = await safeValidateTypes({ value: input, schema }); expect(result).toEqual({ success: true, value: input, rawValue: input }); }); it('should return error object for invalid input', async () => { const input = { name: 'John', age: '30' }; const result = await safeValidateTypes({ value: input, schema, }); expect(result).toEqual({ success: false, error: expect.any(TypeValidationError), rawValue: input, }); if (!result.success) { expect(result.error).toBeInstanceOf(TypeValidationError); expect(result.error.value).toEqual(input); expect(result.error.message).toContain('Type validation failed'); } }); }); }); --- File: /ai/packages/provider-utils/src/validate-types.ts --- import { TypeValidationError } from '@ai-sdk/provider'; import type { StandardSchemaV1 } from '@standard-schema/spec'; import { Validator, asValidator } from './validator'; /** * Validates the types of an unknown object using a schema and * return a strongly-typed object. * * @template T - The type of the object to validate. * @param {string} options.value - The object to validate. * @param {Validator<T>} options.schema - The schema to use for validating the JSON. * @returns {Promise<T>} - The typed object. */ export async function validateTypes<OBJECT>({ value, schema, }: { value: unknown; schema: StandardSchemaV1<unknown, OBJECT> | Validator<OBJECT>; }): Promise<OBJECT> { const result = await safeValidateTypes({ value, schema }); if (!result.success) { throw TypeValidationError.wrap({ value, cause: result.error }); } return result.value; } /** * Safely validates the types of an unknown object using a schema and * return a strongly-typed object. * * @template T - The type of the object to validate. * @param {string} options.value - The JSON object to validate. * @param {Validator<T>} options.schema - The schema to use for validating the JSON. * @returns An object with either a `success` flag and the parsed and typed data, or a `success` flag and an error object. */ export async function safeValidateTypes<OBJECT>({ value, schema, }: { value: unknown; schema: StandardSchemaV1<unknown, OBJECT> | Validator<OBJECT>; }): Promise< | { success: true; value: OBJECT; rawValue: unknown; } | { success: false; error: TypeValidationError; rawValue: unknown; } > { const validator = asValidator(schema); try { if (validator.validate == null) { return { success: true, value: value as OBJECT, rawValue: value }; } const result = await validator.validate(value); if (result.success) { return { success: true, value: result.value, rawValue: value }; } return { success: false, error: TypeValidationError.wrap({ value, cause: result.error }), rawValue: value, }; } catch (error) { return { success: false, error: TypeValidationError.wrap({ value, cause: error }), rawValue: value, }; } } --- File: /ai/packages/provider-utils/src/validator.ts --- import { TypeValidationError } from '@ai-sdk/provider'; import { StandardSchemaV1 } from '@standard-schema/spec'; /** * Used to mark validator functions so we can support both Zod and custom schemas. */ export const validatorSymbol = Symbol.for('vercel.ai.validator'); export type ValidationResult<OBJECT> = | { success: true; value: OBJECT } | { success: false; error: Error }; export type Validator<OBJECT = unknown> = { /** * Used to mark validator functions so we can support both Zod and custom schemas. */ [validatorSymbol]: true; /** * Optional. Validates that the structure of a value matches this schema, * and returns a typed version of the value if it does. */ readonly validate?: ( value: unknown, ) => ValidationResult<OBJECT> | PromiseLike<ValidationResult<OBJECT>>; }; /** * Create a validator. * * @param validate A validation function for the schema. */ export function validator<OBJECT>( validate?: | undefined | (( value: unknown, ) => ValidationResult<OBJECT> | PromiseLike<ValidationResult<OBJECT>>), ): Validator<OBJECT> { return { [validatorSymbol]: true, validate }; } export function isValidator(value: unknown): value is Validator { return ( typeof value === 'object' && value !== null && validatorSymbol in value && value[validatorSymbol] === true && 'validate' in value ); } export function asValidator<OBJECT>( value: Validator<OBJECT> | StandardSchemaV1<unknown, OBJECT>, ): Validator<OBJECT> { return isValidator(value) ? value : standardSchemaValidator(value); } export function standardSchemaValidator<OBJECT>( standardSchema: StandardSchemaV1<unknown, OBJECT>, ): Validator<OBJECT> { return validator(async value => { const result = await standardSchema['~standard'].validate(value); return result.issues == null ? { success: true, value: result.value } : { success: false, error: new TypeValidationError({ value, cause: result.issues, }), }; }); } --- File: /ai/packages/provider-utils/src/without-trailing-slash.ts --- export function withoutTrailingSlash(url: string | undefined) { return url?.replace(/\/$/, ''); } --- File: /ai/packages/provider-utils/src/zod-schema.test.ts --- import { z } from 'zod/v4'; import { zodSchema } from './zod-schema'; import { safeParseJSON } from './parse-json'; describe('zodSchema', () => { describe('json schema conversion', () => { it('should create a schema with simple types', () => { const schema = zodSchema( z.object({ text: z.string(), number: z.number(), }), ); expect(schema.jsonSchema).toMatchSnapshot(); }); it('should support optional fields in object', () => { const schema = zodSchema( z.object({ required: z.string(), optional: z.string().optional(), }), ); expect(schema.jsonSchema).toMatchSnapshot(); }); it('should support optional fields with descriptions in object', () => { const schema = zodSchema( z.object({ required: z.string().describe('Required description'), optional: z.string().optional().describe('Optional description'), }), ); expect(schema.jsonSchema).toMatchSnapshot(); }); it('should support arrays', () => { const schema = zodSchema( z.object({ items: z.array(z.string()), }), ); expect(schema.jsonSchema).toMatchSnapshot(); }); it('should support optional arrays', () => { const schema = zodSchema( z.object({ items: z.array(z.string()).optional(), }), ); expect(schema.jsonSchema).toMatchSnapshot(); }); it('should support required enums', () => { const schema = zodSchema( z.object({ type: z.enum(['a', 'b', 'c']), }), ); expect(schema.jsonSchema).toMatchSnapshot(); }); it('should support optional enums', () => { const schema = zodSchema( z.object({ type: z.enum(['a', 'b', 'c']).optional(), }), ); expect(schema.jsonSchema).toMatchSnapshot(); }); it('should duplicate referenced schemas (and not use references) by default', () => { const Inner = z.object({ text: z.string(), number: z.number(), }); const schema = zodSchema( z.object({ group1: z.array(Inner), group2: z.array(Inner), }), ); expect(schema.jsonSchema).toMatchSnapshot(); }); it('should use references when useReferences is true', () => { const Inner = z.object({ text: z.string(), number: z.number(), }); const schema = zodSchema( z.object({ group1: z.array(Inner), group2: z.array(Inner), }), { useReferences: true }, ); expect(schema.jsonSchema).toMatchSnapshot(); }); it('should use recursive references with z.lazy when useReferences is true', () => { const baseCategorySchema = z.object({ name: z.string(), }); type Category = z.infer<typeof baseCategorySchema> & { subcategories: Category[]; }; const categorySchema: z.ZodType<Category> = baseCategorySchema.extend({ subcategories: z.lazy(() => categorySchema.array()), }); const schema = zodSchema( z.object({ category: categorySchema, }), { useReferences: true }, ); expect(schema.jsonSchema).toMatchSnapshot(); }); describe('nullable', () => { it('should support nullable', () => { const schema = zodSchema( z.object({ location: z.string().nullable(), }), ); expect(schema.jsonSchema).toMatchSnapshot(); }); }); describe('z4 schema', () => { it('generates correct JSON SChema for z4 and .literal and .enum', () => { const schema = zodSchema( z.object({ text: z.literal('hello'), number: z.enum(['one', 'two', 'three']), }), ); expect(schema.jsonSchema).toMatchSnapshot(); }); }); }); describe('output validation', () => { it('should validate output with transform', async () => { const schema = zodSchema( z.object({ user: z.object({ id: z .string() .transform(val => parseInt(val, 10)) .pipe(z.number()), name: z.string(), }), }), ); const result = await safeParseJSON({ text: '{"user": {"id": "123", "name": "John"}}', schema, }); expect(result).toStrictEqual({ success: true, value: { user: { id: 123, name: 'John' } }, rawValue: { user: { id: '123', name: 'John' } }, }); }); }); }); --- File: /ai/packages/provider-utils/src/zod-schema.ts --- import { JSONSchema7 } from '@ai-sdk/provider'; import * as z3 from 'zod/v3'; import * as z4 from 'zod/v4'; import zodToJsonSchema from 'zod-to-json-schema'; import { jsonSchema, Schema } from './schema'; export function zod3Schema<OBJECT>( zodSchema: z3.Schema<OBJECT, z3.ZodTypeDef, any>, options?: { /** * Enables support for references in the schema. * This is required for recursive schemas, e.g. with `z.lazy`. * However, not all language models and providers support such references. * Defaults to `false`. */ useReferences?: boolean; }, ): Schema<OBJECT> { // default to no references (to support openapi conversion for google) const useReferences = options?.useReferences ?? false; return jsonSchema( zodToJsonSchema(zodSchema, { $refStrategy: useReferences ? 'root' : 'none', target: 'jsonSchema7', // note: openai mode breaks various gemini conversions }) as JSONSchema7, { validate: async value => { const result = await zodSchema.safeParseAsync(value); return result.success ? { success: true, value: result.data } : { success: false, error: result.error }; }, }, ); } export function zod4Schema<OBJECT>( zodSchema: z4.core.$ZodType<OBJECT, any>, options?: { /** * Enables support for references in the schema. * This is required for recursive schemas, e.g. with `z.lazy`. * However, not all language models and providers support such references. * Defaults to `false`. */ useReferences?: boolean; }, ): Schema<OBJECT> { // default to no references (to support openapi conversion for google) const useReferences = options?.useReferences ?? false; const z4JSONSchema = z4.toJSONSchema(zodSchema, { target: 'draft-7', io: 'output', reused: useReferences ? 'ref' : 'inline', }) as JSONSchema7; return jsonSchema(z4JSONSchema, { validate: async value => { const result = await z4.safeParseAsync(zodSchema, value); return result.success ? { success: true, value: result.data } : { success: false, error: result.error }; }, }); } export function isZod4Schema( zodSchema: z4.core.$ZodType<any, any> | z3.Schema<any, z3.ZodTypeDef, any>, ): zodSchema is z4.core.$ZodType<any, any> { // https://zod.dev/library-authors?id=how-to-support-zod-3-and-zod-4-simultaneously return '_zod' in zodSchema; } export function zodSchema<OBJECT>( zodSchema: | z4.core.$ZodType<OBJECT, any> | z3.Schema<OBJECT, z3.ZodTypeDef, any>, options?: { /** * Enables support for references in the schema. * This is required for recursive schemas, e.g. with `z.lazy`. * However, not all language models and providers support such references. * Defaults to `false`. */ useReferences?: boolean; }, ): Schema<OBJECT> { if (isZod4Schema(zodSchema)) { return zod4Schema(zodSchema, options); } else { return zod3Schema(zodSchema, options); } } --- File: /ai/packages/provider-utils/test.d.ts --- export * from './dist/test'; --- File: /ai/packages/provider-utils/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, { entry: ['src/test/index.ts'], outDir: 'dist/test', format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/provider-utils/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/provider-utils/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/react/src/util/use-stable-value.ts --- import { isDeepEqualData } from 'ai'; import { useEffect, useState } from 'react'; /** * Returns a stable value that only updates the stored value (and triggers a re-render) * when the value's contents differ by deep-compare. */ export function useStableValue<T>(latestValue: T): T { const [value, setValue] = useState<T>(latestValue); useEffect(() => { if (!isDeepEqualData(latestValue, value)) { setValue(latestValue); } }, [latestValue, value]); return value; } --- File: /ai/packages/react/src/chat.react.ts --- import { AbstractChat, ChatInit, ChatState, ChatStatus, UIMessage } from 'ai'; import { throttle } from './throttle'; class ReactChatState<UI_MESSAGE extends UIMessage> implements ChatState<UI_MESSAGE> { #messages: UI_MESSAGE[]; #status: ChatStatus = 'ready'; #error: Error | undefined = undefined; #messagesCallbacks = new Set<() => void>(); #statusCallbacks = new Set<() => void>(); #errorCallbacks = new Set<() => void>(); constructor(initialMessages: UI_MESSAGE[] = []) { this.#messages = initialMessages; } get status(): ChatStatus { return this.#status; } set status(newStatus: ChatStatus) { this.#status = newStatus; this.#callStatusCallbacks(); } get error(): Error | undefined { return this.#error; } set error(newError: Error | undefined) { this.#error = newError; this.#callErrorCallbacks(); } get messages(): UI_MESSAGE[] { return this.#messages; } set messages(newMessages: UI_MESSAGE[]) { this.#messages = [...newMessages]; this.#callMessagesCallbacks(); } pushMessage = (message: UI_MESSAGE) => { this.#messages = this.#messages.concat(message); this.#callMessagesCallbacks(); }; popMessage = () => { this.#messages = this.#messages.slice(0, -1); this.#callMessagesCallbacks(); }; replaceMessage = (index: number, message: UI_MESSAGE) => { this.#messages = [ ...this.#messages.slice(0, index), // We deep clone the message here to ensure the new React Compiler (currently in RC) detects deeply nested parts/metadata changes: this.snapshot(message), ...this.#messages.slice(index + 1), ]; this.#callMessagesCallbacks(); }; snapshot = <T>(value: T): T => structuredClone(value); '~registerMessagesCallback' = ( onChange: () => void, throttleWaitMs?: number, ): (() => void) => { const callback = throttleWaitMs ? throttle(onChange, throttleWaitMs) : onChange; this.#messagesCallbacks.add(callback); return () => { this.#messagesCallbacks.delete(callback); }; }; '~registerStatusCallback' = (onChange: () => void): (() => void) => { this.#statusCallbacks.add(onChange); return () => { this.#statusCallbacks.delete(onChange); }; }; '~registerErrorCallback' = (onChange: () => void): (() => void) => { this.#errorCallbacks.add(onChange); return () => { this.#errorCallbacks.delete(onChange); }; }; #callMessagesCallbacks = () => { this.#messagesCallbacks.forEach(callback => callback()); }; #callStatusCallbacks = () => { this.#statusCallbacks.forEach(callback => callback()); }; #callErrorCallbacks = () => { this.#errorCallbacks.forEach(callback => callback()); }; } export class Chat< UI_MESSAGE extends UIMessage, > extends AbstractChat<UI_MESSAGE> { #state: ReactChatState<UI_MESSAGE>; constructor({ messages, ...init }: ChatInit<UI_MESSAGE>) { const state = new ReactChatState(messages); super({ ...init, state }); this.#state = state; } '~registerMessagesCallback' = ( onChange: () => void, throttleWaitMs?: number, ): (() => void) => this.#state['~registerMessagesCallback'](onChange, throttleWaitMs); '~registerStatusCallback' = (onChange: () => void): (() => void) => this.#state['~registerStatusCallback'](onChange); '~registerErrorCallback' = (onChange: () => void): (() => void) => this.#state['~registerErrorCallback'](onChange); } --- File: /ai/packages/react/src/index.ts --- export * from './use-chat'; export { Chat } from './chat.react'; export * from './use-completion'; export * from './use-object'; --- File: /ai/packages/react/src/setup-test-component.tsx --- import { cleanup, render } from '@testing-library/react'; import { SWRConfig } from 'swr'; export const setupTestComponent = ( TestComponent: React.ComponentType<any>, { init, }: { init?: (TestComponent: React.ComponentType<any>) => React.ReactNode; } = {}, ) => { beforeEach(() => { // reset SWR cache to isolate tests: render( <SWRConfig value={{ provider: () => new Map() }}> {init?.(TestComponent) ?? <TestComponent />} </SWRConfig>, ); }); afterEach(() => { vi.restoreAllMocks(); cleanup(); }); }; --- File: /ai/packages/react/src/throttle.ts --- import throttleFunction from 'throttleit'; export function throttle<T extends (...args: any[]) => any>( fn: T, waitMs: number | undefined, ): T { return waitMs != null ? throttleFunction(fn, waitMs) : fn; } --- File: /ai/packages/react/src/use-chat.ts --- import { AbstractChat, ChatInit, type CreateUIMessage, type UIMessage, } from 'ai'; import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react'; import { Chat } from './chat.react'; export type { CreateUIMessage, UIMessage }; export type UseChatHelpers<UI_MESSAGE extends UIMessage> = { /** * The id of the chat. */ readonly id: string; /** * Update the `messages` state locally. This is useful when you want to * edit the messages on the client, and then trigger the `reload` method * manually to regenerate the AI response. */ setMessages: ( messages: UI_MESSAGE[] | ((messages: UI_MESSAGE[]) => UI_MESSAGE[]), ) => void; error: Error | undefined; } & Pick< AbstractChat<UI_MESSAGE>, | 'sendMessage' | 'regenerate' | 'stop' | 'resumeStream' | 'addToolResult' | 'status' | 'messages' | 'clearError' >; export type UseChatOptions<UI_MESSAGE extends UIMessage> = ( | { chat: Chat<UI_MESSAGE> } | ChatInit<UI_MESSAGE> ) & { /** Custom throttle wait in ms for the chat messages and data updates. Default is undefined, which disables throttling. */ experimental_throttle?: number; /** * Whether to resume an ongoing chat generation stream. */ resume?: boolean; }; export function useChat<UI_MESSAGE extends UIMessage = UIMessage>({ experimental_throttle: throttleWaitMs, resume = false, ...options }: UseChatOptions<UI_MESSAGE> = {}): UseChatHelpers<UI_MESSAGE> { const chatRef = useRef<Chat<UI_MESSAGE>>( 'chat' in options ? options.chat : new Chat(options), ); const shouldRecreateChat = ('chat' in options && options.chat !== chatRef.current) || ('id' in options && chatRef.current.id !== options.id); if (shouldRecreateChat) { chatRef.current = 'chat' in options ? options.chat : new Chat(options); } const optionsId = 'id' in options ? options.id : null; const subscribeToMessages = useCallback( (update: () => void) => chatRef.current['~registerMessagesCallback'](update, throttleWaitMs), // optionsId is required to trigger re-subscription when the chat ID changes // eslint-disable-next-line react-hooks/exhaustive-deps [throttleWaitMs, optionsId], ); const messages = useSyncExternalStore( subscribeToMessages, () => chatRef.current.messages, () => chatRef.current.messages, ); const status = useSyncExternalStore( chatRef.current['~registerStatusCallback'], () => chatRef.current.status, () => chatRef.current.status, ); const error = useSyncExternalStore( chatRef.current['~registerErrorCallback'], () => chatRef.current.error, () => chatRef.current.error, ); const setMessages = useCallback( ( messagesParam: UI_MESSAGE[] | ((messages: UI_MESSAGE[]) => UI_MESSAGE[]), ) => { if (typeof messagesParam === 'function') { messagesParam = messagesParam(chatRef.current.messages); } chatRef.current.messages = messagesParam; }, [chatRef], ); useEffect(() => { if (resume) { chatRef.current.resumeStream(); } }, [resume, chatRef]); return { id: chatRef.current.id, messages, setMessages, sendMessage: chatRef.current.sendMessage, regenerate: chatRef.current.regenerate, clearError: chatRef.current.clearError, stop: chatRef.current.stop, error, resumeStream: chatRef.current.resumeStream, status, addToolResult: chatRef.current.addToolResult, }; } --- File: /ai/packages/react/src/use-chat.ui.test.tsx --- /* eslint-disable jsx-a11y/alt-text */ /* eslint-disable @next/next/no-img-element */ import { createTestServer, mockId, TestResponseController, } from '@ai-sdk/provider-utils/test'; import '@testing-library/jest-dom/vitest'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { DefaultChatTransport, isToolUIPart, TextStreamChatTransport, UIMessage, UIMessageChunk, } from 'ai'; import React, { act, useRef, useState } from 'react'; import { Chat } from './chat.react'; import { setupTestComponent } from './setup-test-component'; import { useChat } from './use-chat'; function formatChunk(part: UIMessageChunk) { return `data: ${JSON.stringify(part)}\n\n`; } const server = createTestServer({ '/api/chat': {}, '/api/chat/123/stream': {}, }); describe('initial messages', () => { setupTestComponent( ({ id: idParam }: { id: string }) => { const [id, setId] = React.useState<string>(idParam); const { messages, status, id: idKey, } = useChat({ id, messages: [ { id: 'id-0', role: 'user', parts: [{ text: 'hi', type: 'text' }], }, ], }); return ( <div> <div data-testid="id">{idKey}</div> <div data-testid="status">{status.toString()}</div> <div data-testid="messages">{JSON.stringify(messages, null, 2)}</div> </div> ); }, { // use a random id to avoid conflicts: init: TestComponent => <TestComponent id={`first-${mockId()()}`} />, }, ); it('should show initial messages', async () => { await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { role: 'user', parts: [ { text: 'hi', type: 'text', }, ], id: 'id-0', }, ]); }); }); }); describe('data protocol stream', () => { let onFinishCalls: Array<{ message: UIMessage }> = []; setupTestComponent( ({ id: idParam }: { id: string }) => { const [id, setId] = React.useState<string>(idParam); const { messages, sendMessage, error, status, id: idKey, } = useChat({ id, onFinish: options => { onFinishCalls.push(options); }, generateId: mockId(), }); return ( <div> <div data-testid="id">{idKey}</div> <div data-testid="status">{status.toString()}</div> {error && <div data-testid="error">{error.toString()}</div>} <div data-testid="messages">{JSON.stringify(messages, null, 2)}</div> <button data-testid="do-send" onClick={() => { sendMessage({ parts: [{ text: 'hi', type: 'text' }] }); }} /> <button data-testid="do-change-id" onClick={() => { setId('second-id'); }} /> </div> ); }, { // use a random id to avoid conflicts: init: TestComponent => <TestComponent id={`first-${mockId()()}`} />, }, ); beforeEach(() => { onFinishCalls = []; }); it('should show streamed response', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), ], }; await userEvent.click(screen.getByTestId('do-send')); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { role: 'user', parts: [ { text: 'hi', type: 'text', }, ], id: 'id-0', }, { id: 'id-1', role: 'assistant', parts: [ { type: 'text', text: 'Hello, world.', state: 'done', }, ], }, ]); }); }); it('should show user message immediately', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; await userEvent.click(screen.getByTestId('do-send')); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { role: 'user', parts: [ { text: 'hi', type: 'text', }, ], id: 'id-0', }, ]); }); }); it('should show error response when there is a server error', async () => { server.urls['/api/chat'].response = { type: 'error', status: 404, body: 'Not found', }; await userEvent.click(screen.getByTestId('do-send')); await screen.findByTestId('error'); expect(screen.getByTestId('error')).toHaveTextContent('Error: Not found'); }); it('should show error response when there is a streaming error', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'error', errorText: 'custom error message' }), ], }; await userEvent.click(screen.getByTestId('do-send')); await screen.findByTestId('error'); expect(screen.getByTestId('error')).toHaveTextContent( 'Error: custom error message', ); }); describe('status', () => { it('should show status', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; await userEvent.click(screen.getByTestId('do-send')); await waitFor(() => { expect(screen.getByTestId('status')).toHaveTextContent('submitted'); }); controller.write(formatChunk({ type: 'text-start', id: '0' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), ); controller.write(formatChunk({ type: 'text-end', id: '0' })); await waitFor(() => { expect(screen.getByTestId('status')).toHaveTextContent('streaming'); }); controller.close(); await waitFor(() => { expect(screen.getByTestId('status')).toHaveTextContent('ready'); }); }); it('should set status to error when there is a server error', async () => { server.urls['/api/chat'].response = { type: 'error', status: 404, body: 'Not found', }; await userEvent.click(screen.getByTestId('do-send')); await waitFor(() => { expect(screen.getByTestId('status')).toHaveTextContent('error'); }); }); }); it('should invoke onFinish when the stream finishes', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; await userEvent.click(screen.getByTestId('do-send')); controller.write(formatChunk({ type: 'text-start', id: '0' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), ); controller.write(formatChunk({ type: 'text-delta', id: '0', delta: ',' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), ); controller.write(formatChunk({ type: 'text-delta', id: '0', delta: '.' })); controller.write(formatChunk({ type: 'text-end', id: '0' })); controller.write( formatChunk({ type: 'finish', messageMetadata: { example: 'metadata', }, }), ); controller.close(); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { role: 'user', parts: [ { text: 'hi', type: 'text', }, ], id: 'id-0', }, { id: 'id-1', role: 'assistant', metadata: { example: 'metadata', }, parts: [ { type: 'text', text: 'Hello, world.', state: 'done', }, ], }, ]); }); expect(onFinishCalls).toMatchInlineSnapshot(` [ { "message": { "id": "id-1", "metadata": { "example": "metadata", }, "parts": [ { "providerMetadata": undefined, "state": "done", "text": "Hello, world.", "type": "text", }, ], "role": "assistant", }, }, ] `); }); describe('id', () => { it('send the id to the server', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), ], }; await userEvent.click(screen.getByTestId('do-send')); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "first-id-0", "messages": [ { "id": "id-0", "parts": [ { "text": "hi", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); }); }); describe('text stream', () => { let onFinishCalls: Array<{ message: UIMessage }> = []; setupTestComponent(() => { const { messages, sendMessage } = useChat({ onFinish: options => { onFinishCalls.push(options); }, generateId: mockId(), transport: new TextStreamChatTransport({ api: '/api/chat', }), }); return ( <div> {messages.map((m, idx) => ( <div data-testid={`message-${idx}-text-stream`} key={m.id}> <div data-testid={`message-${idx}-id`}>{m.id}</div> <div data-testid={`message-${idx}-role`}> {m.role === 'user' ? 'User: ' : 'AI: '} </div> <div data-testid={`message-${idx}-content`}> {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> </div> ))} <button data-testid="do-send" onClick={() => { sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); }} /> </div> ); }); beforeEach(() => { onFinishCalls = []; }); it('should show streamed response', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: ['Hello', ',', ' world', '.'], }; await userEvent.click(screen.getByTestId('do-send')); await screen.findByTestId('message-0-content'); expect(screen.getByTestId('message-0-content')).toHaveTextContent('hi'); await screen.findByTestId('message-1-content'); expect(screen.getByTestId('message-1-content')).toHaveTextContent( 'Hello, world.', ); }); it('should have stable message ids', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; await userEvent.click(screen.getByTestId('do-send')); controller.write('He'); await screen.findByTestId('message-1-content'); expect(screen.getByTestId('message-1-content')).toHaveTextContent('He'); const id = screen.getByTestId('message-1-id').textContent; controller.write('llo'); controller.close(); await screen.findByTestId('message-1-content'); expect(screen.getByTestId('message-1-content')).toHaveTextContent('Hello'); expect(screen.getByTestId('message-1-id').textContent).toBe(id); }); it('should invoke onFinish when the stream finishes', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: ['Hello', ',', ' world', '.'], }; await userEvent.click(screen.getByTestId('do-send')); await screen.findByTestId('message-1-text-stream'); expect(onFinishCalls).toMatchInlineSnapshot(` [ { "message": { "id": "id-2", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Hello, world.", "type": "text", }, ], "role": "assistant", }, }, ] `); }); }); describe('prepareChatRequest', () => { let options: any; setupTestComponent(() => { const { messages, sendMessage, status } = useChat({ transport: new DefaultChatTransport({ body: { 'body-key': 'body-value' }, headers: { 'header-key': 'header-value' }, prepareSendMessagesRequest(optionsArg) { options = optionsArg; return { body: { 'request-body-key': 'request-body-value' }, headers: { 'header-key': 'header-value' }, }; }, }), generateId: mockId(), }); return ( <div> <div data-testid="status">{status.toString()}</div> {messages.map((m, idx) => ( <div data-testid={`message-${idx}`} key={m.id}> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> ))} <button data-testid="do-send" onClick={() => { sendMessage( { parts: [{ text: 'hi', type: 'text' }], }, { body: { 'request-body-key': 'request-body-value' }, headers: { 'request-header-key': 'request-header-value' }, metadata: { 'request-metadata-key': 'request-metadata-value' }, }, ); }} /> </div> ); }); afterEach(() => { options = undefined; }); it('should show streamed response', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), ], }; await userEvent.click(screen.getByTestId('do-send')); await screen.findByTestId('message-0'); expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi'); expect(options).toMatchInlineSnapshot(` { "api": "/api/chat", "body": { "body-key": "body-value", "request-body-key": "request-body-value", }, "credentials": undefined, "headers": { "header-key": "header-value", "request-header-key": "request-header-value", }, "id": "id-0", "messageId": undefined, "messages": [ { "id": "id-1", "metadata": undefined, "parts": [ { "text": "hi", "type": "text", }, ], "role": "user", }, ], "requestMetadata": { "request-metadata-key": "request-metadata-value", }, "trigger": "submit-message", } `); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "request-body-key": "request-body-value", } `); expect(server.calls[0].requestHeaders).toMatchInlineSnapshot(` { "content-type": "application/json", "header-key": "header-value", } `); await screen.findByTestId('message-1'); expect(screen.getByTestId('message-1')).toHaveTextContent( 'AI: Hello, world.', ); }); }); describe('onToolCall', () => { let resolve: () => void; let toolCallPromise: Promise<void>; setupTestComponent(() => { const { messages, sendMessage, addToolResult } = useChat({ async onToolCall({ toolCall }) { await toolCallPromise; addToolResult({ tool: 'test-tool', toolCallId: toolCall.toolCallId, output: `test-tool-response: ${toolCall.toolName} ${ toolCall.toolCallId } ${JSON.stringify(toolCall.input)}`, }); }, }); return ( <div> {messages.map((m, idx) => ( <div data-testid={`message-${idx}`} key={m.id}> {m.parts.filter(isToolUIPart).map((toolPart, toolIdx) => ( <div key={toolIdx} data-testid={`tool-${toolIdx}`}> {JSON.stringify(toolPart)} </div> ))} </div> ))} <button data-testid="do-send" onClick={() => { sendMessage({ parts: [{ text: 'hi', type: 'text' }], }); }} /> </div> ); }); beforeEach(() => { toolCallPromise = new Promise(resolveArg => { resolve = resolveArg; }); }); it("should invoke onToolCall when a tool call is received from the server's response", async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), ], }; await userEvent.click(screen.getByTestId('do-send')); await screen.findByTestId('message-1'); expect( JSON.parse(screen.getByTestId('message-1').textContent ?? ''), ).toStrictEqual({ state: 'input-available', input: { testArg: 'test-value' }, toolCallId: 'tool-call-0', type: 'tool-test-tool', }); resolve(); await waitFor(() => { expect( JSON.parse(screen.getByTestId('message-1').textContent ?? ''), ).toStrictEqual({ state: 'output-available', input: { testArg: 'test-value' }, toolCallId: 'tool-call-0', type: 'tool-test-tool', output: 'test-tool-response: test-tool tool-call-0 {"testArg":"test-value"}', }); }); }); }); describe('tool invocations', () => { setupTestComponent(() => { const { messages, sendMessage, addToolResult } = useChat({ generateId: mockId(), }); return ( <div> {messages.map((m, idx) => ( <div data-testid={`message-${idx}`} key={m.id}> {m.parts.filter(isToolUIPart).map((toolPart, toolIdx) => { return ( <div key={toolIdx}> <div data-testid={`tool-invocation-${toolIdx}`}> {JSON.stringify(toolPart)} </div> {toolPart.state === 'input-available' && ( <button data-testid={`add-result-${toolIdx}`} onClick={() => { addToolResult({ tool: 'test-tool', toolCallId: toolPart.toolCallId, output: 'test-result', }); }} /> )} </div> ); })} {m.role === 'assistant' && ( <div data-testid={`message-${idx}-text`}> {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> )} </div> ))} <div data-testid="messages">{JSON.stringify(messages, null, 2)}</div> <button data-testid="do-send" onClick={() => { sendMessage({ parts: [{ text: 'hi', type: 'text' }], }); }} /> </div> ); }); it('should display partial tool call, tool call, and tool result', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; await userEvent.click(screen.getByTestId('do-send')); controller.write( formatChunk({ type: 'tool-input-start', toolCallId: 'tool-call-0', toolName: 'test-tool', }), ); await waitFor(() => { expect( JSON.parse(screen.getByTestId('message-1').textContent ?? ''), ).toStrictEqual({ state: 'input-streaming', toolCallId: 'tool-call-0', type: 'tool-test-tool', }); }); controller.write( formatChunk({ type: 'tool-input-delta', toolCallId: 'tool-call-0', inputTextDelta: '{"testArg":"t', }), ); await waitFor(() => { expect( JSON.parse(screen.getByTestId('message-1').textContent ?? ''), ).toStrictEqual({ state: 'input-streaming', toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 't' }, }); }); controller.write( formatChunk({ type: 'tool-input-delta', toolCallId: 'tool-call-0', inputTextDelta: 'est-value"}}', }), ); await waitFor(() => { expect( JSON.parse(screen.getByTestId('message-1').textContent ?? ''), ).toStrictEqual({ state: 'input-streaming', toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, }); }); controller.write( formatChunk({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), ); await waitFor(() => { expect( JSON.parse(screen.getByTestId('message-1').textContent ?? ''), ).toStrictEqual({ state: 'input-available', input: { testArg: 'test-value' }, toolCallId: 'tool-call-0', type: 'tool-test-tool', }); }); controller.write( formatChunk({ type: 'tool-output-available', toolCallId: 'tool-call-0', output: 'test-result', }), ); controller.close(); await waitFor(() => { expect( JSON.parse(screen.getByTestId('message-1').textContent ?? ''), ).toStrictEqual({ state: 'output-available', input: { testArg: 'test-value' }, toolCallId: 'tool-call-0', type: 'tool-test-tool', output: 'test-result', }); }); }); it('should display tool call and tool result (when there is no tool call streaming)', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; await userEvent.click(screen.getByTestId('do-send')); controller.write( formatChunk({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), ); await waitFor(() => { expect( JSON.parse(screen.getByTestId('message-1').textContent ?? ''), ).toStrictEqual({ state: 'input-available', input: { testArg: 'test-value' }, toolCallId: 'tool-call-0', type: 'tool-test-tool', }); }); controller.write( formatChunk({ type: 'tool-output-available', toolCallId: 'tool-call-0', output: 'test-result', }), ); controller.close(); await waitFor(() => { expect( JSON.parse(screen.getByTestId('message-1').textContent ?? ''), ).toStrictEqual({ state: 'output-available', input: { testArg: 'test-value' }, toolCallId: 'tool-call-0', type: 'tool-test-tool', output: 'test-result', }); }); }); it('should update tool call to result when addToolResult is called', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; await userEvent.click(screen.getByTestId('do-send')); controller.write(formatChunk({ type: 'start' })); controller.write(formatChunk({ type: 'start-step' })); controller.write( formatChunk({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), ); await waitFor(() => { expect( JSON.parse(screen.getByTestId('message-1').textContent ?? ''), ).toStrictEqual({ state: 'input-available', input: { testArg: 'test-value' }, toolCallId: 'tool-call-0', type: 'tool-test-tool', }); }); await userEvent.click(screen.getByTestId('add-result-0')); await waitFor(() => { expect( JSON.parse(screen.getByTestId('message-1').textContent ?? ''), ).toStrictEqual({ state: 'output-available', input: { testArg: 'test-value' }, toolCallId: 'tool-call-0', type: 'tool-test-tool', output: 'test-result', }); }); controller.write(formatChunk({ type: 'text-start', id: '0' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: 'more text', }), ); controller.write(formatChunk({ type: 'text-end', id: '0' })); controller.close(); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { id: 'id-1', parts: [ { text: 'hi', type: 'text', }, ], role: 'user', }, { id: 'id-2', parts: [ { type: 'step-start', }, { type: 'tool-test-tool', toolCallId: 'tool-call-0', input: { testArg: 'test-value' }, output: 'test-result', state: 'output-available', }, { text: 'more text', type: 'text', state: 'done', }, ], role: 'assistant', }, ]); }); }); }); describe('file attachments with data url', () => { setupTestComponent(() => { const { messages, status, sendMessage } = useChat({ generateId: mockId(), }); const [files, setFiles] = useState<FileList | undefined>(undefined); const fileInputRef = useRef<HTMLInputElement>(null); const [input, setInput] = useState(''); return ( <div> <div data-testid="messages">{JSON.stringify(messages, null, 2)}</div> <form onSubmit={() => { sendMessage({ text: input, files }); setFiles(undefined); if (fileInputRef.current) { fileInputRef.current.value = ''; } }} data-testid="chat-form" > <input type="file" onChange={event => { if (event.target.files) { setFiles(event.target.files); } }} multiple ref={fileInputRef} data-testid="file-input" /> <input value={input} onChange={e => setInput(e.target.value)} disabled={status !== 'ready'} data-testid="message-input" /> <button type="submit" data-testid="submit-button"> Send </button> </form> </div> ); }); it('should handle text file attachment and submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0', }), formatChunk({ type: 'text-delta', id: '0', delta: 'Response to message with text attachment', }), formatChunk({ type: 'text-end', id: '0' }), ], }; const file = new File(['test file content'], 'test.txt', { type: 'text/plain', }); const fileInput = screen.getByTestId('file-input'); await userEvent.upload(fileInput, file); const messageInput = screen.getByTestId('message-input'); await userEvent.type(messageInput, 'Message with text attachment'); const submitButton = screen.getByTestId('submit-button'); await userEvent.click(submitButton); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { id: 'id-1', role: 'user', parts: [ { type: 'file', mediaType: 'text/plain', filename: 'test.txt', url: 'data:text/plain;base64,dGVzdCBmaWxlIGNvbnRlbnQ=', }, { type: 'text', text: 'Message with text attachment', }, ], }, { id: 'id-2', parts: [ { text: 'Response to message with text attachment', type: 'text', state: 'done', }, ], role: 'assistant', }, ]); }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "filename": "test.txt", "mediaType": "text/plain", "type": "file", "url": "data:text/plain;base64,dGVzdCBmaWxlIGNvbnRlbnQ=", }, { "text": "Message with text attachment", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); it('should handle image file attachment and submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0', }), formatChunk({ type: 'text-delta', id: '0', delta: 'Response to message with image attachment', }), formatChunk({ type: 'text-end', id: '0' }), ], }; const file = new File(['test image content'], 'test.png', { type: 'image/png', }); const fileInput = screen.getByTestId('file-input'); await userEvent.upload(fileInput, file); const messageInput = screen.getByTestId('message-input'); await userEvent.type(messageInput, 'Message with image attachment'); const submitButton = screen.getByTestId('submit-button'); await userEvent.click(submitButton); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { role: 'user', id: 'id-1', parts: [ { type: 'file', mediaType: 'image/png', filename: 'test.png', url: 'data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50', }, { type: 'text', text: 'Message with image attachment', }, ], }, { role: 'assistant', id: 'id-2', parts: [ { type: 'text', text: 'Response to message with image attachment', state: 'done', }, ], }, ]); }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "filename": "test.png", "mediaType": "image/png", "type": "file", "url": "data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50", }, { "text": "Message with image attachment", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); }); describe('file attachments with url', () => { setupTestComponent(() => { const { messages, sendMessage, status } = useChat({ generateId: mockId(), }); const [input, setInput] = useState(''); return ( <div> <div data-testid="messages">{JSON.stringify(messages, null, 2)}</div> <form onSubmit={() => { sendMessage({ text: input, files: [ { type: 'file', mediaType: 'image/png', url: 'https://example.com/image.png', }, ], }); }} data-testid="chat-form" > <input value={input} onChange={e => setInput(e.target.value)} disabled={status !== 'ready'} data-testid="message-input" /> <button type="submit" data-testid="submit-button"> Send </button> </form> </div> ); }); it('should handle image file attachment and submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0', }), formatChunk({ type: 'text-delta', id: '0', delta: 'Response to message with image attachment', }), formatChunk({ type: 'text-end', id: '0' }), ], }; const messageInput = screen.getByTestId('message-input'); await userEvent.type(messageInput, 'Message with image attachment'); const submitButton = screen.getByTestId('submit-button'); await userEvent.click(submitButton); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { role: 'user', id: 'id-1', parts: [ { type: 'file', mediaType: 'image/png', url: 'https://example.com/image.png', }, { type: 'text', text: 'Message with image attachment', }, ], }, { role: 'assistant', id: 'id-2', parts: [ { type: 'text', text: 'Response to message with image attachment', state: 'done', }, ], }, ]); }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "mediaType": "image/png", "type": "file", "url": "https://example.com/image.png", }, { "text": "Message with image attachment", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); }); describe('attachments with empty submit', () => { setupTestComponent(() => { const { messages, sendMessage } = useChat({ generateId: mockId(), }); return ( <div> <div data-testid="messages">{JSON.stringify(messages, null, 2)}</div> <form onSubmit={() => { sendMessage({ files: [ { type: 'file', filename: 'test.png', mediaType: 'image/png', url: 'https://example.com/image.png', }, ], }); }} data-testid="chat-form" > <button type="submit" data-testid="submit-button"> Send </button> </form> </div> ); }); it('should handle image file attachment and submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Response to message with image attachment', }), formatChunk({ type: 'text-end', id: '0' }), ], }; const submitButton = screen.getByTestId('submit-button'); await userEvent.click(submitButton); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { id: 'id-1', role: 'user', parts: [ { type: 'file', mediaType: 'image/png', filename: 'test.png', url: 'https://example.com/image.png', }, ], }, { id: 'id-2', role: 'assistant', parts: [ { type: 'text', text: 'Response to message with image attachment', state: 'done', }, ], }, ]); }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "filename": "test.png", "mediaType": "image/png", "type": "file", "url": "https://example.com/image.png", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); }); describe('should send message with attachments', () => { setupTestComponent(() => { const { messages, sendMessage } = useChat({ generateId: mockId(), }); return ( <div> <div data-testid="messages">{JSON.stringify(messages, null, 2)}</div> <form onSubmit={event => { event.preventDefault(); sendMessage({ parts: [ { type: 'file', mediaType: 'image/png', url: 'https://example.com/image.png', }, { type: 'text', text: 'Message with image attachment', }, ], }); }} data-testid="chat-form" > <button type="submit" data-testid="submit-button"> Send </button> </form> </div> ); }); it('should handle image file attachment and submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Response to message with image attachment', }), formatChunk({ type: 'text-end', id: '0' }), ], }; const submitButton = screen.getByTestId('submit-button'); await userEvent.click(submitButton); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { id: 'id-1', parts: [ { mediaType: 'image/png', type: 'file', url: 'https://example.com/image.png', }, { text: 'Message with image attachment', type: 'text', }, ], role: 'user', }, { id: 'id-2', parts: [ { state: 'done', text: 'Response to message with image attachment', type: 'text', }, ], role: 'assistant', }, ]); }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "mediaType": "image/png", "type": "file", "url": "https://example.com/image.png", }, { "text": "Message with image attachment", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); }); describe('regenerate', () => { setupTestComponent(() => { const { messages, sendMessage, regenerate } = useChat({ generateId: mockId(), }); return ( <div> {messages.map((m, idx) => ( <div data-testid={`message-${idx}`} key={m.id}> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> ))} <button data-testid="do-send" onClick={() => { sendMessage({ parts: [{ text: 'hi', type: 'text' }] }); }} /> <button data-testid="do-regenerate" onClick={() => { regenerate({ body: { 'request-body-key': 'request-body-value' }, headers: { 'header-key': 'header-value' }, }); }} /> </div> ); }); it('should show streamed response', async () => { server.urls['/api/chat'].response = [ { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'first response', }), formatChunk({ type: 'text-end', id: '0' }), ], }, { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'second response', }), formatChunk({ type: 'text-end', id: '0' }), ], }, ]; await userEvent.click(screen.getByTestId('do-send')); await screen.findByTestId('message-0'); expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi'); await screen.findByTestId('message-1'); // setup done, click reload: await userEvent.click(screen.getByTestId('do-regenerate')); expect(await server.calls[1].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "text": "hi", "type": "text", }, ], "role": "user", }, ], "request-body-key": "request-body-value", "trigger": "regenerate-message", } `); expect(server.calls[1].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'header-key': 'header-value', }); await screen.findByTestId('message-1'); expect(screen.getByTestId('message-1')).toHaveTextContent( 'AI: second response', ); }); }); describe('test sending additional fields during message submission', () => { setupTestComponent(() => { type Message = UIMessage<{ test: string }>; const { messages, sendMessage } = useChat<Message>({ generateId: mockId(), }); return ( <div> {messages.map((m, idx) => ( <div data-testid={`message-${idx}`} key={m.id}> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> ))} <button data-testid="do-send" onClick={() => { sendMessage({ role: 'user', metadata: { test: 'example' }, parts: [{ text: 'hi', type: 'text' }], }); }} /> </div> ); }); it('should send metadata with the message', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'first response', }), formatChunk({ type: 'text-end', id: '0' }), ], }; await userEvent.click(screen.getByTestId('do-send')); await screen.findByTestId('message-0'); expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi'); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "metadata": { "test": "example", }, "parts": [ { "text": "hi", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); }); describe('resume ongoing stream and return assistant message', () => { const controller = new TestResponseController(); setupTestComponent( () => { const { messages, status } = useChat({ id: '123', messages: [ { id: 'msg_123', role: 'user', parts: [{ type: 'text', text: 'hi' }], }, ], generateId: mockId(), resume: true, }); return ( <div> {messages.map((m, idx) => ( <div data-testid={`message-${idx}`} key={m.id}> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> ))} <div data-testid="status">{status}</div> </div> ); }, { init: TestComponent => { server.urls['/api/chat/123/stream'].response = { type: 'controlled-stream', controller, }; return <TestComponent />; }, }, ); it('construct messages from resumed stream', async () => { await screen.findByTestId('message-0'); expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi'); await waitFor(() => { expect(screen.getByTestId('status')).toHaveTextContent('submitted'); }); controller.write(formatChunk({ type: 'text-start', id: '0' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), ); await waitFor(() => { expect(screen.getByTestId('status')).toHaveTextContent('streaming'); }); controller.write(formatChunk({ type: 'text-delta', id: '0', delta: ',' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), ); controller.write(formatChunk({ type: 'text-delta', id: '0', delta: '.' })); controller.write(formatChunk({ type: 'text-end', id: '0' })); controller.close(); await screen.findByTestId('message-1'); expect(screen.getByTestId('message-1')).toHaveTextContent( 'AI: Hello, world.', ); await waitFor(() => { expect(screen.getByTestId('status')).toHaveTextContent('ready'); expect(server.calls.length).toBeGreaterThan(0); const mostRecentCall = server.calls[0]; const { requestMethod, requestUrl } = mostRecentCall; expect(requestMethod).toBe('GET'); expect(requestUrl).toBe('http://localhost:3000/api/chat/123/stream'); }); }); }); describe('stop', () => { setupTestComponent(() => { const { messages, sendMessage, stop, status } = useChat({ generateId: mockId(), }); return ( <div> {messages.map((m, idx) => ( <div data-testid={`message-${idx}`} key={m.id}> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> ))} <button data-testid="do-send" onClick={() => { sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); }} /> <button data-testid="do-stop" onClick={stop} /> <p data-testid="status">{status}</p> </div> ); }); it('should show stop response', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; await userEvent.click(screen.getByTestId('do-send')); controller.write(formatChunk({ type: 'text-start', id: '0' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), ); await waitFor(() => { expect(screen.getByTestId('message-1')).toHaveTextContent('AI: Hello'); expect(screen.getByTestId('status')).toHaveTextContent('streaming'); }); await userEvent.click(screen.getByTestId('do-stop')); await waitFor(() => { expect(screen.getByTestId('status')).toHaveTextContent('ready'); }); await expect( controller.write( formatChunk({ type: 'text-delta', id: '0', delta: ', world!' }), ), ).rejects.toThrow(); await expect(controller.close()).rejects.toThrow(); expect(screen.getByTestId('message-1')).toHaveTextContent('AI: Hello'); expect(screen.getByTestId('status')).toHaveTextContent('ready'); }); }); describe('experimental_throttle', () => { const throttleMs = 50; setupTestComponent(() => { const { messages, sendMessage, status } = useChat({ experimental_throttle: throttleMs, generateId: mockId(), }); return ( <div> <div data-testid="status">{status.toString()}</div> {messages.map((m, idx) => ( <div data-testid={`message-${idx}`} key={m.id}> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts .map(part => (part.type === 'text' ? part.text : '')) .join('')} </div> ))} <button data-testid="do-send" onClick={() => { sendMessage({ parts: [{ text: 'hi', type: 'text' }] }); }} /> </div> ); }); it('should throttle UI updates when experimental_throttle is set', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; await userEvent.click(screen.getByTestId('do-send')); expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi'); vi.useFakeTimers(); controller.write(formatChunk({ type: 'text-start', id: '0' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: 'Hel' }), ); await act(async () => { await vi.advanceTimersByTimeAsync(throttleMs + 10); }); expect(screen.getByTestId('message-1')).toHaveTextContent('AI: Hel'); controller.write(formatChunk({ type: 'text-delta', id: '0', delta: 'lo' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: ' Th' }), ); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: 'ere' }), ); controller.write(formatChunk({ type: 'text-end', id: '0' })); expect(screen.getByTestId('message-1')).not.toHaveTextContent( 'AI: Hello There', ); await act(async () => { await vi.advanceTimersByTimeAsync(throttleMs + 10); }); expect(screen.getByTestId('message-1')).toHaveTextContent( 'AI: Hello There', ); vi.useRealTimers(); }); }); describe('id changes', () => { setupTestComponent( () => { const [id, setId] = React.useState<string>('initial-id'); const { messages, sendMessage, error, status, id: idKey, } = useChat({ id, generateId: mockId(), }); return ( <div> <div data-testid="id">{idKey}</div> <div data-testid="status">{status.toString()}</div> {error && <div data-testid="error">{error.toString()}</div>} <div data-testid="messages">{JSON.stringify(messages, null, 2)}</div> <button data-testid="do-send" onClick={() => { sendMessage({ parts: [{ text: 'hi', type: 'text' }] }); }} /> <button data-testid="do-change-id" onClick={() => { setId('second-id'); }} /> </div> ); }, { init: TestComponent => <TestComponent />, }, ); it('should update chat instance when the id changes', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), ], }; await userEvent.click(screen.getByTestId('do-send')); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { id: expect.any(String), parts: [ { text: 'hi', type: 'text', }, ], role: 'user', }, { id: 'id-1', parts: [ { text: 'Hello, world.', type: 'text', state: 'done', }, ], role: 'assistant', }, ]); }); await userEvent.click(screen.getByTestId('do-change-id')); expect(screen.queryByTestId('message-0')).not.toBeInTheDocument(); }); }); describe('chat instance changes', () => { setupTestComponent( () => { const [chat, setChat] = React.useState<Chat<UIMessage>>( new Chat({ id: 'initial-id', generateId: mockId(), }), ); const { messages, sendMessage, error, status, id: idKey, } = useChat({ chat, }); return ( <div> <div data-testid="id">{idKey}</div> <div data-testid="status">{status.toString()}</div> {error && <div data-testid="error">{error.toString()}</div>} <div data-testid="messages">{JSON.stringify(messages, null, 2)}</div> <button data-testid="do-send" onClick={() => { sendMessage({ parts: [{ text: 'hi', type: 'text' }] }); }} /> <button data-testid="do-change-chat" onClick={() => { setChat( new Chat({ id: 'second-id', generateId: mockId(), }), ); }} /> </div> ); }, { init: TestComponent => <TestComponent />, }, ); it('should update chat instance when the id changes', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), ], }; await userEvent.click(screen.getByTestId('do-send')); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { id: expect.any(String), parts: [ { text: 'hi', type: 'text', }, ], role: 'user', }, { id: 'id-1', parts: [ { text: 'Hello, world.', type: 'text', state: 'done', }, ], role: 'assistant', }, ]); }); await userEvent.click(screen.getByTestId('do-change-chat')); expect(screen.queryByTestId('message-0')).not.toBeInTheDocument(); }); }); describe('streaming with id change from undefined to defined', () => { setupTestComponent( () => { const [id, setId] = React.useState<string | undefined>(undefined); const { messages, sendMessage, status } = useChat({ id, generateId: mockId(), }); return ( <div> <div data-testid="status">{status.toString()}</div> <div data-testid="messages">{JSON.stringify(messages, null, 2)}</div> <button data-testid="change-id" onClick={() => { setId('chat-123'); }} /> <button data-testid="send-message" onClick={() => { sendMessage({ parts: [{ text: 'hi', type: 'text' }] }); }} /> </div> ); }, { init: TestComponent => <TestComponent />, }, ); it('should handle streaming correctly when id changes from undefined to defined', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; // First, change the ID from undefined to 'chat-123' await userEvent.click(screen.getByTestId('change-id')); // Then send a message await userEvent.click(screen.getByTestId('send-message')); await waitFor(() => { expect(screen.getByTestId('status')).toHaveTextContent('submitted'); }); controller.write(formatChunk({ type: 'text-start', id: '0' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), ); // Verify streaming is working - text should appear immediately await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toContainEqual( expect.objectContaining({ role: 'assistant', parts: expect.arrayContaining([ expect.objectContaining({ type: 'text', text: 'Hello', }), ]), }), ); }); controller.write(formatChunk({ type: 'text-delta', id: '0', delta: ',' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), ); controller.write(formatChunk({ type: 'text-delta', id: '0', delta: '.' })); controller.write(formatChunk({ type: 'text-end', id: '0' })); controller.close(); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toContainEqual( expect.objectContaining({ role: 'assistant', parts: expect.arrayContaining([ expect.objectContaining({ type: 'text', text: 'Hello, world.', state: 'done', }), ]), }), ); }); }); }); --- File: /ai/packages/react/src/use-completion.ts --- import { CompletionRequestOptions, UseCompletionOptions, callCompletionApi, } from 'ai'; import { useCallback, useEffect, useId, useRef, useState } from 'react'; import useSWR from 'swr'; import { throttle } from './throttle'; export type { UseCompletionOptions }; export type UseCompletionHelpers = { /** The current completion result */ completion: string; /** * Send a new prompt to the API endpoint and update the completion state. */ complete: ( prompt: string, options?: CompletionRequestOptions, ) => Promise<string | null | undefined>; /** The error object of the API request */ error: undefined | Error; /** * Abort the current API request but keep the generated tokens. */ stop: () => void; /** * Update the `completion` state locally. */ setCompletion: (completion: string) => void; /** The current value of the input */ input: string; /** setState-powered method to update the input value */ setInput: React.Dispatch<React.SetStateAction<string>>; /** * An input/textarea-ready onChange handler to control the value of the input * @example * ```jsx * <input onChange={handleInputChange} value={input} /> * ``` */ handleInputChange: ( event: | React.ChangeEvent<HTMLInputElement> | React.ChangeEvent<HTMLTextAreaElement>, ) => void; /** * Form submission handler to automatically reset input and append a user message * @example * ```jsx * <form onSubmit={handleSubmit}> * <input onChange={handleInputChange} value={input} /> * </form> * ``` */ handleSubmit: (event?: { preventDefault?: () => void }) => void; /** Whether the API request is in progress */ isLoading: boolean; }; export function useCompletion({ api = '/api/completion', id, initialCompletion = '', initialInput = '', credentials, headers, body, streamProtocol = 'data', fetch, onFinish, onError, experimental_throttle: throttleWaitMs, }: UseCompletionOptions & { /** * Custom throttle wait in ms for the completion and data updates. * Default is undefined, which disables throttling. */ experimental_throttle?: number; } = {}): UseCompletionHelpers { // Generate an unique id for the completion if not provided. const hookId = useId(); const completionId = id || hookId; // Store the completion state in SWR, using the completionId as the key to share states. const { data, mutate } = useSWR<string>([api, completionId], null, { fallbackData: initialCompletion, }); const { data: isLoading = false, mutate: mutateLoading } = useSWR<boolean>( [completionId, 'loading'], null, ); const [error, setError] = useState<undefined | Error>(undefined); const completion = data!; // Abort controller to cancel the current API call. const [abortController, setAbortController] = useState<AbortController | null>(null); const extraMetadataRef = useRef({ credentials, headers, body, }); useEffect(() => { extraMetadataRef.current = { credentials, headers, body, }; }, [credentials, headers, body]); const triggerRequest = useCallback( async (prompt: string, options?: CompletionRequestOptions) => callCompletionApi({ api, prompt, credentials: extraMetadataRef.current.credentials, headers: { ...extraMetadataRef.current.headers, ...options?.headers }, body: { ...extraMetadataRef.current.body, ...options?.body, }, streamProtocol, fetch, // throttle streamed ui updates: setCompletion: throttle( (completion: string) => mutate(completion, false), throttleWaitMs, ), setLoading: mutateLoading, setError, setAbortController, onFinish, onError, }), [ mutate, mutateLoading, api, extraMetadataRef, setAbortController, onFinish, onError, setError, streamProtocol, fetch, throttleWaitMs, ], ); const stop = useCallback(() => { if (abortController) { abortController.abort(); setAbortController(null); } }, [abortController]); const setCompletion = useCallback( (completion: string) => { mutate(completion, false); }, [mutate], ); const complete = useCallback<UseCompletionHelpers['complete']>( async (prompt, options) => { return triggerRequest(prompt, options); }, [triggerRequest], ); const [input, setInput] = useState(initialInput); const handleSubmit = useCallback( (event?: { preventDefault?: () => void }) => { event?.preventDefault?.(); return input ? complete(input) : undefined; }, [input, complete], ); const handleInputChange = useCallback( (e: any) => { setInput(e.target.value); }, [setInput], ); return { completion, complete, error, setCompletion, stop, input, setInput, handleInputChange, handleSubmit, isLoading, }; } --- File: /ai/packages/react/src/use-completion.ui.test.tsx --- import { createTestServer, TestResponseController, } from '@ai-sdk/provider-utils/test'; import '@testing-library/jest-dom/vitest'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { UIMessageChunk } from 'ai'; import { setupTestComponent } from './setup-test-component'; import { useCompletion } from './use-completion'; function formatChunk(part: UIMessageChunk) { return `data: ${JSON.stringify(part)}\n\n`; } const server = createTestServer({ '/api/completion': {}, }); describe('stream data stream', () => { let onFinishResult: { prompt: string; completion: string } | undefined; setupTestComponent(() => { const { completion, handleSubmit, error, handleInputChange, input, isLoading, } = useCompletion({ onFinish(prompt, completion) { onFinishResult = { prompt, completion }; }, }); return ( <div> <div data-testid="loading">{isLoading.toString()}</div> <div data-testid="error">{error?.toString()}</div> <div data-testid="completion">{completion}</div> <form onSubmit={handleSubmit}> <input data-testid="input" value={input} placeholder="Say something..." onChange={handleInputChange} /> </form> </div> ); }); beforeEach(() => { onFinishResult = undefined; }); describe('render simple stream', () => { beforeEach(async () => { server.urls['/api/completion'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), ], }; await userEvent.type(screen.getByTestId('input'), 'hi{enter}'); }); it('should render stream', async () => { await waitFor(() => { expect(screen.getByTestId('completion')).toHaveTextContent( 'Hello, world.', ); }); }); it("should call 'onFinish' callback", async () => { await waitFor(() => { expect(onFinishResult).toEqual({ prompt: 'hi', completion: 'Hello, world.', }); }); }); }); describe('loading state', () => { it('should show loading state', async () => { const controller = new TestResponseController(); server.urls['/api/completion'].response = { type: 'controlled-stream', controller, }; await userEvent.type(screen.getByTestId('input'), 'hi{enter}'); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), ); await waitFor(() => { expect(screen.getByTestId('loading')).toHaveTextContent('true'); }); await controller.close(); await waitFor(() => { expect(screen.getByTestId('loading')).toHaveTextContent('false'); }); }); it('should reset loading state on error', async () => { server.urls['/api/completion'].response = { type: 'error', status: 404, body: 'Not found', }; await userEvent.type(screen.getByTestId('input'), 'hi{enter}'); await screen.findByTestId('loading'); expect(screen.getByTestId('loading')).toHaveTextContent('false'); }); }); }); describe('text stream', () => { setupTestComponent(() => { const { completion, handleSubmit, handleInputChange, input } = useCompletion({ streamProtocol: 'text' }); return ( <div> <div data-testid="completion-text-stream">{completion}</div> <form onSubmit={handleSubmit}> <input data-testid="input-text-stream" value={input} placeholder="Say something..." onChange={handleInputChange} /> </form> </div> ); }); it('should render stream', async () => { server.urls['/api/completion'].response = { type: 'stream-chunks', chunks: ['Hello', ',', ' world', '.'], }; await userEvent.type(screen.getByTestId('input-text-stream'), 'hi{enter}'); await screen.findByTestId('completion-text-stream'); expect(screen.getByTestId('completion-text-stream')).toHaveTextContent( 'Hello, world.', ); }); }); --- File: /ai/packages/react/src/use-object.ts --- import { FetchFunction, InferSchema, isAbortError, safeValidateTypes, } from '@ai-sdk/provider-utils'; import { asSchema, DeepPartial, isDeepEqualData, parsePartialJson, Schema, } from 'ai'; import { useCallback, useId, useRef, useState } from 'react'; import useSWR from 'swr'; import * as z3 from 'zod/v3'; import * as z4 from 'zod/v4'; // use function to allow for mocking in tests: const getOriginalFetch = () => fetch; export type Experimental_UseObjectOptions< SCHEMA extends z4.core.$ZodType | z3.Schema | Schema, RESULT, > = { /** * The API endpoint. It should stream JSON that matches the schema as chunked text. */ api: string; /** * A Zod schema that defines the shape of the complete object. */ schema: SCHEMA; /** * An unique identifier. If not provided, a random one will be * generated. When provided, the `useObject` hook with the same `id` will * have shared states across components. */ id?: string; /** * An optional value for the initial object. */ initialValue?: DeepPartial<RESULT>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; /** Callback that is called when the stream has finished. */ onFinish?: (event: { /** The generated object (typed according to the schema). Can be undefined if the final object does not match the schema. */ object: RESULT | undefined; /** Optional error object. This is e.g. a TypeValidationError when the final object does not match the schema. */ error: Error | undefined; }) => Promise<void> | void; /** * Callback function to be called when an error is encountered. */ onError?: (error: Error) => void; /** * Additional HTTP headers to be included in the request. */ headers?: Record<string, string> | Headers; /** * The credentials mode to be used for the fetch request. * Possible values are: 'omit', 'same-origin', 'include'. * Defaults to 'same-origin'. */ credentials?: RequestCredentials; }; export type Experimental_UseObjectHelpers<RESULT, INPUT> = { /** * Calls the API with the provided input as JSON body. */ submit: (input: INPUT) => void; /** * The current value for the generated object. Updated as the API streams JSON chunks. */ object: DeepPartial<RESULT> | undefined; /** * The error object of the API request if any. */ error: Error | undefined; /** * Flag that indicates whether an API request is in progress. */ isLoading: boolean; /** * Abort the current request immediately, keep the current partial object if any. */ stop: () => void; /** * Clear the object state. */ clear: () => void; }; function useObject< SCHEMA extends z4.core.$ZodType | z3.Schema | Schema, RESULT = InferSchema<SCHEMA>, INPUT = any, >({ api, id, schema, // required, in the future we will use it for validation initialValue, fetch, onError, onFinish, headers, credentials, }: Experimental_UseObjectOptions< SCHEMA, RESULT >): Experimental_UseObjectHelpers<RESULT, INPUT> { // Generate an unique id if not provided. const hookId = useId(); const completionId = id ?? hookId; // Store the completion state in SWR, using the completionId as the key to share states. const { data, mutate } = useSWR<DeepPartial<RESULT>>( [api, completionId], null, { fallbackData: initialValue }, ); const [error, setError] = useState<undefined | Error>(undefined); const [isLoading, setIsLoading] = useState(false); // Abort controller to cancel the current API call. const abortControllerRef = useRef<AbortController | null>(null); const stop = useCallback(() => { try { abortControllerRef.current?.abort(); } catch (ignored) { } finally { setIsLoading(false); abortControllerRef.current = null; } }, []); const submit = async (input: INPUT) => { try { clearObject(); setIsLoading(true); const abortController = new AbortController(); abortControllerRef.current = abortController; const actualFetch = fetch ?? getOriginalFetch(); const response = await actualFetch(api, { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers, }, credentials, signal: abortController.signal, body: JSON.stringify(input), }); if (!response.ok) { throw new Error( (await response.text()) ?? 'Failed to fetch the response.', ); } if (response.body == null) { throw new Error('The response body is empty.'); } let accumulatedText = ''; let latestObject: DeepPartial<RESULT> | undefined = undefined; await response.body.pipeThrough(new TextDecoderStream()).pipeTo( new WritableStream<string>({ async write(chunk) { accumulatedText += chunk; const { value } = await parsePartialJson(accumulatedText); const currentObject = value as DeepPartial<RESULT>; if (!isDeepEqualData(latestObject, currentObject)) { latestObject = currentObject; mutate(currentObject); } }, async close() { setIsLoading(false); abortControllerRef.current = null; if (onFinish != null) { const validationResult = await safeValidateTypes({ value: latestObject, schema: asSchema(schema), }); onFinish( validationResult.success ? { object: validationResult.value, error: undefined } : { object: undefined, error: validationResult.error }, ); } }, }), ); } catch (error) { if (isAbortError(error)) { return; } if (onError && error instanceof Error) { onError(error); } setIsLoading(false); setError(error instanceof Error ? error : new Error(String(error))); } }; const clear = () => { stop(); clearObject(); }; const clearObject = () => { setError(undefined); setIsLoading(false); mutate(undefined); }; return { submit, object: data, error, isLoading, stop, clear, }; } export const experimental_useObject = useObject; --- File: /ai/packages/react/src/use-object.ui.test.tsx --- import { createTestServer, TestResponseController, } from '@ai-sdk/provider-utils/test'; import '@testing-library/jest-dom/vitest'; import { cleanup, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { z } from 'zod/v4'; import { experimental_useObject } from './use-object'; const server = createTestServer({ '/api/use-object': {}, }); describe('text stream', () => { let onErrorResult: Error | undefined; let onFinishCalls: Array<{ object: { content: string } | undefined; error: Error | undefined; }> = []; const TestComponent = ({ headers, credentials, }: { headers?: Record<string, string> | Headers; credentials?: RequestCredentials; }) => { const { object, error, submit, isLoading, stop, clear } = experimental_useObject({ api: '/api/use-object', schema: z.object({ content: z.string() }), onError(error) { onErrorResult = error; }, onFinish(event) { onFinishCalls.push(event); }, headers, credentials, }); return ( <div> <div data-testid="loading">{isLoading.toString()}</div> <div data-testid="object">{JSON.stringify(object)}</div> <div data-testid="error">{error?.toString()}</div> <button data-testid="submit-button" onClick={() => submit('test-input')} > Generate </button> <button data-testid="stop-button" onClick={stop}> Stop </button> <button data-testid="clear-button" onClick={clear}> Clear </button> </div> ); }; beforeEach(() => { onErrorResult = undefined; onFinishCalls = []; }); afterEach(() => { vi.restoreAllMocks(); cleanup(); onErrorResult = undefined; onFinishCalls = []; }); describe('basic component', () => { beforeEach(() => { render(<TestComponent />); }); describe("when the API returns 'Hello, world!'", () => { beforeEach(async () => { server.urls['/api/use-object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content": "Hello, ', 'world', '!"'], }; await userEvent.click(screen.getByTestId('submit-button')); }); it('should render stream', async () => { await screen.findByTestId('object'); expect(screen.getByTestId('object')).toHaveTextContent( JSON.stringify({ content: 'Hello, world!' }), ); }); it("should send 'test' to the API", async () => { expect(await server.calls[0].requestBodyJson).toBe('test-input'); }); it('should not have an error', async () => { await screen.findByTestId('error'); expect(screen.getByTestId('error')).toBeEmptyDOMElement(); expect(onErrorResult).toBeUndefined(); }); }); describe('isLoading', () => { it('should be true while loading', async () => { const controller = new TestResponseController(); server.urls['/api/use-object'].response = { type: 'controlled-stream', controller, }; controller.write('{"content": '); await userEvent.click(screen.getByTestId('submit-button')); // wait for element "loading" to have text content "true": await waitFor(() => { expect(screen.getByTestId('loading')).toHaveTextContent('true'); }); controller.write('"Hello, world!"}'); controller.close(); // wait for element "loading" to have text content "false": await waitFor(() => { expect(screen.getByTestId('loading')).toHaveTextContent('false'); }); }); }); it('should abort the stream and not consume any more data', async () => { const controller = new TestResponseController(); server.urls['/api/use-object'].response = { type: 'controlled-stream', controller, }; controller.write('{"content": "h'); await userEvent.click(screen.getByTestId('submit-button')); // wait for element "loading" and "object" to have text content: await waitFor(() => { expect(screen.getByTestId('loading')).toHaveTextContent('true'); }); await waitFor(() => { expect(screen.getByTestId('object')).toHaveTextContent( '{"content":"h"}', ); }); // click stop button: await userEvent.click(screen.getByTestId('stop-button')); // wait for element "loading" to have text content "false": await waitFor(() => { expect(screen.getByTestId('loading')).toHaveTextContent('false'); }); // this should not be consumed any more: await expect(controller.write('ello, world!"}')).rejects.toThrow(); await expect(controller.close()).rejects.toThrow(); // should only show start of object: await waitFor(() => { expect(screen.getByTestId('object')).toHaveTextContent( '{"content":"h"}', ); }); }); it('should stop and clear the object state after a call to submit then clear', async () => { const controller = new TestResponseController(); server.urls['/api/use-object'].response = { type: 'controlled-stream', controller, }; controller.write('{"content": "h'); await userEvent.click(screen.getByTestId('submit-button')); await waitFor(() => { expect(screen.getByTestId('loading')).toHaveTextContent('true'); }); await waitFor(() => { expect(screen.getByTestId('object')).toHaveTextContent( '{"content":"h"}', ); }); await userEvent.click(screen.getByTestId('clear-button')); await expect(controller.write('ello, world!"}')).rejects.toThrow(); await expect(controller.close()).rejects.toThrow(); await waitFor(() => { expect(screen.getByTestId('loading')).toHaveTextContent('false'); expect(screen.getByTestId('error')).toBeEmptyDOMElement(); expect(screen.getByTestId('object')).toBeEmptyDOMElement(); }); }); describe('when the API returns a 404', () => { it('should render error', async () => { server.urls['/api/use-object'].response = { type: 'error', status: 404, body: 'Not found', }; await userEvent.click(screen.getByTestId('submit-button')); await screen.findByTestId('error'); expect(screen.getByTestId('error')).toHaveTextContent('Not found'); expect(onErrorResult).toBeInstanceOf(Error); expect(screen.getByTestId('loading')).toHaveTextContent('false'); }); }); describe('onFinish', () => { it('should be called with an object when the stream finishes and the object matches the schema', async () => { server.urls['/api/use-object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'], }; await userEvent.click(screen.getByTestId('submit-button')); expect(onFinishCalls).toStrictEqual([ { object: { content: 'Hello, world!' }, error: undefined }, ]); }); it('should be called with an error when the stream finishes and the object does not match the schema', async () => { server.urls['/api/use-object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content-wrong": "Hello, ', 'world', '!"', '}'], }; await userEvent.click(screen.getByTestId('submit-button')); expect(onFinishCalls).toStrictEqual([ { object: undefined, error: expect.any(Error) }, ]); }); }); }); it('should send custom headers', async () => { server.urls['/api/use-object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'], }; render( <TestComponent headers={{ Authorization: 'Bearer TEST_TOKEN', 'X-Custom-Header': 'CustomValue', }} />, ); await userEvent.click(screen.getByTestId('submit-button')); expect(server.calls[0].requestHeaders).toStrictEqual({ 'content-type': 'application/json', authorization: 'Bearer TEST_TOKEN', 'x-custom-header': 'CustomValue', }); }); it('should send custom credentials', async () => { server.urls['/api/use-object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content": "Authenticated ', 'content', '!"', '}'], }; render(<TestComponent credentials="include" />); await userEvent.click(screen.getByTestId('submit-button')); expect(server.calls[0].requestCredentials).toBe('include'); }); it('should clear the object state after a call to clear', async () => { server.urls['/api/use-object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'], }; render(<TestComponent />); await userEvent.click(screen.getByTestId('submit-button')); await screen.findByTestId('object'); expect(screen.getByTestId('object')).toHaveTextContent( JSON.stringify({ content: 'Hello, world!' }), ); await userEvent.click(screen.getByTestId('clear-button')); await waitFor(() => { expect(screen.getByTestId('object')).toBeEmptyDOMElement(); expect(screen.getByTestId('error')).toBeEmptyDOMElement(); expect(screen.getByTestId('loading')).toHaveTextContent('false'); }); }); }); --- File: /ai/packages/react/.eslintrc.js --- module.exports = { root: true, extends: ['vercel-ai'], }; --- File: /ai/packages/react/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], outDir: 'dist', banner: {}, format: ['cjs', 'esm'], external: ['vue'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/react/vitest.config.js --- import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', globals: true, include: ['src/**/*.ui.test.ts', 'src/**/*.ui.test.tsx'], }, }); --- File: /ai/packages/replicate/src/index.ts --- export { createReplicate, replicate } from './replicate-provider'; export type { ReplicateProvider, ReplicateProviderSettings, } from './replicate-provider'; --- File: /ai/packages/replicate/src/replicate-error.ts --- import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; const replicateErrorSchema = z.object({ detail: z.string().optional(), error: z.string().optional(), }); export const replicateFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: replicateErrorSchema, errorToMessage: error => error.detail ?? error.error ?? 'Unknown Replicate error', }); --- File: /ai/packages/replicate/src/replicate-image-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { createReplicate } from './replicate-provider'; import { ReplicateImageModel } from './replicate-image-model'; const prompt = 'The Loch Ness monster getting a manicure'; const provider = createReplicate({ apiToken: 'test-api-token' }); const model = provider.image('black-forest-labs/flux-schnell'); describe('doGenerate', () => { const testDate = new Date(2024, 0, 1); const server = createTestServer({ 'https://api.replicate.com/*': {}, 'https://replicate.delivery/*': { response: { type: 'binary', body: Buffer.from('test-binary-content'), }, }, }); function prepareResponse({ output = ['https://replicate.delivery/xezq/abc/out-0.webp'], }: { output?: string | Array<string> } = {}) { server.urls['https://api.replicate.com/*'].response = { type: 'json-value', body: { id: 's7x1e3dcmhrmc0cm8rbatcneec', model: 'black-forest-labs/flux-schnell', version: 'dp-4d0bcc010b3049749a251855f12800be', input: { num_outputs: 1, prompt: 'The Loch Ness Monster getting a manicure', }, logs: '', output, data_removed: false, error: null, status: 'processing', created_at: '2025-01-08T13:24:38.692Z', urls: { cancel: 'https://api.replicate.com/v1/predictions/s7x1e3dcmhrmc0cm8rbatcneec/cancel', get: 'https://api.replicate.com/v1/predictions/s7x1e3dcmhrmc0cm8rbatcneec', stream: 'https://stream.replicate.com/v1/files/bcwr-3okdfv3o2wehstv5f2okyftwxy57hhypqsi6osiim5iaq5k7u24a', }, }, }; } it('should pass the model and the settings', async () => { prepareResponse(); await model.doGenerate({ prompt, n: 1, size: '1024x768', aspectRatio: '3:4', seed: 123, providerOptions: { replicate: { style: 'realistic_image', }, other: { something: 'else', }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ input: { prompt, num_outputs: 1, aspect_ratio: '3:4', size: '1024x768', seed: 123, style: 'realistic_image', }, }); }); it('should call the correct url', async () => { prepareResponse(); await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(server.calls[0].requestMethod).toStrictEqual('POST'); expect(server.calls[0].requestUrl).toStrictEqual( 'https://api.replicate.com/v1/models/black-forest-labs/flux-schnell/predictions', ); }); it('should pass headers and set the prefer header', async () => { prepareResponse(); const provider = createReplicate({ apiToken: 'test-api-token', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.image('black-forest-labs/flux-schnell').doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-token', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', prefer: 'wait', }); }); it('should extract the generated image from array response', async () => { prepareResponse({ output: ['https://replicate.delivery/xezq/abc/out-0.webp'], }); const result = await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.images).toStrictEqual([ new Uint8Array(Buffer.from('test-binary-content')), ]); expect(server.calls[1].requestMethod).toStrictEqual('GET'); expect(server.calls[1].requestUrl).toStrictEqual( 'https://replicate.delivery/xezq/abc/out-0.webp', ); }); it('should extract the generated image from string response', async () => { prepareResponse({ output: 'https://replicate.delivery/xezq/abc/out-0.webp', }); const result = await model.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.images).toStrictEqual([ new Uint8Array(Buffer.from('test-binary-content')), ]); expect(server.calls[1].requestMethod).toStrictEqual('GET'); expect(server.calls[1].requestUrl).toStrictEqual( 'https://replicate.delivery/xezq/abc/out-0.webp', ); }); it('should return response metadata', async () => { const modelWithTimestamp = new ReplicateImageModel( 'black-forest-labs/flux-schnell', { provider: 'replicate', baseURL: 'https://api.replicate.com', _internal: { currentDate: () => testDate }, }, ); prepareResponse({ output: ['https://replicate.delivery/xezq/abc/out-0.webp'], }); const result = await modelWithTimestamp.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.response).toStrictEqual({ timestamp: testDate, modelId: 'black-forest-labs/flux-schnell', headers: expect.any(Object), }); }); it('should include response headers in metadata', async () => { const modelWithTimestamp = new ReplicateImageModel( 'black-forest-labs/flux-schnell', { provider: 'replicate', baseURL: 'https://api.replicate.com', _internal: { currentDate: () => testDate, }, }, ); server.urls['https://api.replicate.com/*'].response = { type: 'json-value', headers: { 'custom-response-header': 'response-header-value', }, body: { id: 's7x1e3dcmhrmc0cm8rbatcneec', model: 'black-forest-labs/flux-schnell', version: 'dp-4d0bcc010b3049749a251855f12800be', input: { num_outputs: 1, prompt: 'The Loch Ness Monster getting a manicure', }, logs: '', output: ['https://replicate.delivery/xezq/abc/out-0.webp'], data_removed: false, error: null, status: 'processing', created_at: '2025-01-08T13:24:38.692Z', urls: { cancel: 'https://api.replicate.com/v1/predictions/s7x1e3dcmhrmc0cm8rbatcneec/cancel', get: 'https://api.replicate.com/v1/predictions/s7x1e3dcmhrmc0cm8rbatcneec', stream: 'https://stream.replicate.com/v1/files/bcwr-3okdfv3o2wehstv5f2okyftwxy57hhypqsi6osiim5iaq5k7u24a', }, }, }; const result = await modelWithTimestamp.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(result.response).toStrictEqual({ timestamp: testDate, modelId: 'black-forest-labs/flux-schnell', headers: { 'content-length': '646', 'content-type': 'application/json', 'custom-response-header': 'response-header-value', }, }); }); it('should set version in request body for versioned models', async () => { prepareResponse(); const versionedModel = provider.image( 'bytedance/sdxl-lightning-4step:5599ed30703defd1d160a25a63321b4dec97101d98b4674bcc56e41f62f35637', ); await versionedModel.doGenerate({ prompt, n: 1, size: undefined, aspectRatio: undefined, seed: undefined, providerOptions: {}, }); expect(server.calls[0].requestMethod).toStrictEqual('POST'); expect(server.calls[0].requestUrl).toStrictEqual( 'https://api.replicate.com/v1/predictions', ); expect(await server.calls[0].requestBodyJson).toStrictEqual({ input: { prompt, num_outputs: 1, }, version: '5599ed30703defd1d160a25a63321b4dec97101d98b4674bcc56e41f62f35637', }); }); }); --- File: /ai/packages/replicate/src/replicate-image-model.ts --- import type { ImageModelV2, ImageModelV2CallWarning } from '@ai-sdk/provider'; import type { Resolvable } from '@ai-sdk/provider-utils'; import { FetchFunction, combineHeaders, createBinaryResponseHandler, createJsonResponseHandler, getFromApi, postJsonToApi, resolve, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { replicateFailedResponseHandler } from './replicate-error'; import { ReplicateImageModelId } from './replicate-image-settings'; interface ReplicateImageModelConfig { provider: string; baseURL: string; headers?: Resolvable<Record<string, string | undefined>>; fetch?: FetchFunction; _internal?: { currentDate?: () => Date; }; } export class ReplicateImageModel implements ImageModelV2 { readonly specificationVersion = 'v2'; readonly maxImagesPerCall = 1; get provider(): string { return this.config.provider; } constructor( readonly modelId: ReplicateImageModelId, private readonly config: ReplicateImageModelConfig, ) {} async doGenerate({ prompt, n, aspectRatio, size, seed, providerOptions, headers, abortSignal, }: Parameters<ImageModelV2['doGenerate']>[0]): Promise< Awaited<ReturnType<ImageModelV2['doGenerate']>> > { const warnings: Array<ImageModelV2CallWarning> = []; const [modelId, version] = this.modelId.split(':'); const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { value: { output }, responseHeaders, } = await postJsonToApi({ url: // different endpoints for versioned vs unversioned models: version != null ? `${this.config.baseURL}/predictions` : `${this.config.baseURL}/models/${modelId}/predictions`, headers: combineHeaders(await resolve(this.config.headers), headers, { prefer: 'wait', }), body: { input: { prompt, aspect_ratio: aspectRatio, size, seed, num_outputs: n, ...(providerOptions.replicate ?? {}), }, // for versioned models, include the version in the body: ...(version != null ? { version } : {}), }, successfulResponseHandler: createJsonResponseHandler( replicateImageResponseSchema, ), failedResponseHandler: replicateFailedResponseHandler, abortSignal, fetch: this.config.fetch, }); // download the images: const outputArray = Array.isArray(output) ? output : [output]; const images = await Promise.all( outputArray.map(async url => { const { value: image } = await getFromApi({ url, successfulResponseHandler: createBinaryResponseHandler(), failedResponseHandler: replicateFailedResponseHandler, abortSignal, fetch: this.config.fetch, }); return image; }), ); return { images, warnings, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, }, }; } } const replicateImageResponseSchema = z.object({ output: z.union([z.array(z.string()), z.string()]), }); --- File: /ai/packages/replicate/src/replicate-image-settings.ts --- export type ReplicateImageModelId = | 'black-forest-labs/flux-1.1-pro' | 'black-forest-labs/flux-1.1-pro-ultra' | 'black-forest-labs/flux-dev' | 'black-forest-labs/flux-pro' | 'black-forest-labs/flux-schnell' | 'bytedance/sdxl-lightning-4step' | 'fofr/aura-flow' | 'fofr/latent-consistency-model' | 'fofr/realvisxl-v3-multi-controlnet-lora' | 'fofr/sdxl-emoji' | 'fofr/sdxl-multi-controlnet-lora' | 'ideogram-ai/ideogram-v2' | 'ideogram-ai/ideogram-v2-turbo' | 'lucataco/dreamshaper-xl-turbo' | 'lucataco/open-dalle-v1.1' | 'lucataco/realvisxl-v2.0' | 'lucataco/realvisxl2-lcm' | 'luma/photon' | 'luma/photon-flash' | 'nvidia/sana' | 'playgroundai/playground-v2.5-1024px-aesthetic' | 'recraft-ai/recraft-v3' | 'recraft-ai/recraft-v3-svg' | 'stability-ai/stable-diffusion-3.5-large' | 'stability-ai/stable-diffusion-3.5-large-turbo' | 'stability-ai/stable-diffusion-3.5-medium' | 'tstramer/material-diffusion' | (string & {}); --- File: /ai/packages/replicate/src/replicate-provider.test.ts --- import { describe, it, expect } from 'vitest'; import { createReplicate } from './replicate-provider'; import { ReplicateImageModel } from './replicate-image-model'; describe('createReplicate', () => { it('creates a provider with required settings', () => { const provider = createReplicate({ apiToken: 'test-token' }); expect(provider.image).toBeDefined(); }); it('creates a provider with custom settings', () => { const provider = createReplicate({ apiToken: 'test-token', baseURL: 'https://custom.replicate.com', }); expect(provider.image).toBeDefined(); }); it('creates an image model instance', () => { const provider = createReplicate({ apiToken: 'test-token' }); const model = provider.image('black-forest-labs/flux-schnell'); expect(model).toBeInstanceOf(ReplicateImageModel); }); }); --- File: /ai/packages/replicate/src/replicate-provider.ts --- import { NoSuchModelError, ProviderV2 } from '@ai-sdk/provider'; import type { FetchFunction } from '@ai-sdk/provider-utils'; import { loadApiKey } from '@ai-sdk/provider-utils'; import { ReplicateImageModel } from './replicate-image-model'; import { ReplicateImageModelId } from './replicate-image-settings'; export interface ReplicateProviderSettings { /** API token that is being send using the `Authorization` header. It defaults to the `REPLICATE_API_TOKEN` environment variable. */ apiToken?: string; /** Use a different URL prefix for API calls, e.g. to use proxy servers. The default prefix is `https://api.replicate.com/v1`. */ baseURL?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } export interface ReplicateProvider extends ProviderV2 { /** * Creates a Replicate image generation model. */ image(modelId: ReplicateImageModelId): ReplicateImageModel; /** * Creates a Replicate image generation model. */ imageModel(modelId: ReplicateImageModelId): ReplicateImageModel; } /** * Create a Replicate provider instance. */ export function createReplicate( options: ReplicateProviderSettings = {}, ): ReplicateProvider { const createImageModel = (modelId: ReplicateImageModelId) => new ReplicateImageModel(modelId, { provider: 'replicate', baseURL: options.baseURL ?? 'https://api.replicate.com/v1', headers: { Authorization: `Bearer ${loadApiKey({ apiKey: options.apiToken, environmentVariableName: 'REPLICATE_API_TOKEN', description: 'Replicate', })}`, ...options.headers, }, fetch: options.fetch, }); return { image: createImageModel, imageModel: createImageModel, languageModel: () => { throw new NoSuchModelError({ modelId: 'languageModel', modelType: 'languageModel', }); }, textEmbeddingModel: () => { throw new NoSuchModelError({ modelId: 'textEmbeddingModel', modelType: 'textEmbeddingModel', }); }, }; } /** * Default Replicate provider instance. */ export const replicate = createReplicate(); --- File: /ai/packages/replicate/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/replicate/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/replicate/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/revai/src/index.ts --- export { createRevai, revai } from './revai-provider'; export type { RevaiProvider, RevaiProviderSettings } from './revai-provider'; --- File: /ai/packages/revai/src/revai-api-types.ts --- export type RevaiTranscriptionAPITypes = { /** * Optional metadata that was provided during job submission. */ metadata?: string | null; /** * Optional configuration for a callback url to invoke when processing is complete, * in addition to auth headers if they are needed to invoke the callback url. * Cannot be set if callback_url is set. This option will not be visible in the submission response. */ notification_config?: { /** * Optional callback url to invoke when processing is complete */ url: string; /** * Optional authorization headers, if they are needed to invoke the callback. * There are a few constraints: 1) the "Authorization" header is the only header that can be passed in, * and 2) the header value must be of the form <scheme> <token>. * For example: {"Authorization": "Bearer $BEARER_TOKEN"} */ auth_headers?: { /** * Authorization header */ Authorization: string; } | null; } | null; /** * Amount of time after job completion when job is auto-deleted. Present only when preference set in job request. */ delete_after_seconds?: number | null; /** * Select which service you would like to transcribe this file with. * - machine: the default and routes to our standard (Reverb) model. * - low_cost: low-cost transcription which uses quantized ASR model (Reverb Turbo) with low-cost environment. * - fusion: higher quality ASR that combines multiple models to achieve the best results. Typically has better support for rare words. * @default "machine" */ transcriber?: 'machine' | 'low_cost' | 'fusion' | null; /** * Configures the transcriber to transcribe every syllable. This will include all false starts and disfluencies in the transcript. * * The behavior depends on the transcriber option: * - machine: the default is true. To turn it off false should be explicitly provided * - human: the default is false To turn it on true should be explicitly provided */ verbatim?: boolean; /** * [HIPAA Unsupported] Only available for human transcriber option * When this field is set to true your job is given higher priority and will be worked on sooner by our human transcribers. * @default false */ rush?: boolean | null; /** * [HIPAA Unsupported] Only available for human transcriber option * When this field is set to true the behavior will mock a normal human transcription job except no transcription will happen. * The primary use case is to test integrations without being charged for human transcription. * @default false */ test_mode?: boolean | null; /** * [HIPAA Unsupported] Only available for human transcriber option. * Use this option to specify which sections of the transcript need to be transcribed. * Segments must be at least 1 minute in length and cannot overlap. */ segments_to_transcribe?: Array<{ /** * The timestamp of the beginning of the segment relative to the beginning of the audio in seconds (centisecond precision) */ start: number; /** * The timestamp of the end of the segment relative to the beginning of the audio in seconds (centisecond precision) */ end: number; }> | null; /** * [HIPAA Unsupported] Only available for human transcriber option. * Use this option to specify up to 100 names of speakers in the transcript. * Names may only be up to 50 characters long. */ speaker_names?: Array<{ /** * The name of the speaker to be used when labeling monologues. Max of 50 characters. */ display_name: string; }> | null; /** * Specify if speaker diarization will be skipped by the speech engine * @default false */ skip_diarization?: boolean | null; /** * Only available for English and Spanish languages. * User-supplied preference on whether to skip post-processing operations such as inverse text normalization (ITN), casing and punctuation. * @default false */ skip_postprocessing?: boolean | null; /** * Specify if "punct" type elements will be skipped by the speech engine. * For JSON outputs, this includes removing spaces. For text outputs, words will still be delimited by a space * @default false */ skip_punctuation?: boolean | null; /** * Currently we only define disfluencies as 'ums' and 'uhs'. * When set to true, disfluencies will not appear in the transcript. * This option also removes atmospherics if the remove_atmospherics is not set. * This option is not available for human transcription jobs. * @default false */ remove_disfluencies?: boolean | null; /** * We define many atmospherics such <laugh>, <affirmative> etc. * When set to true, atmospherics will not appear in the transcript. * This option is not available for human transcription jobs. * @default false */ remove_atmospherics?: boolean | null; /** * Enabling this option will filter for approx. 600 profanities, which cover most use cases. * If a transcribed word matches a word on this list, then all the characters of that word will be replaced by asterisks * except for the first and last character. * @default false */ filter_profanity?: boolean | null; /** * Only available for English, Spanish and French languages. * Use to specify the total number of unique speaker channels in the audio. * * Given the number of audio channels provided, each channel will be transcribed separately and the channel id assigned to the speaker label. * The final output will be a combination of all individual channel outputs. * Overlapping monologues will have ordering broken by the order in which the first spoken element of each monologue occurs. * If speaker_channels_count is greater than the actual channels in the audio, the job will fail with invalid_media. * This option is not available for human transcription jobs. */ speaker_channels_count?: number | null; /** * Only available for English, Spanish and French languages. * Use to specify the total number of unique speakers in the audio. * * Given the count of speakers provided, it will be used to improve the diarization accuracy. * This option is not available for human transcription jobs. * @default null */ speakers_count?: number | null; /** * Use to specify diarization type. This option is not available for human transcription jobs and low-cost environment. * @default "standard" */ diarization_type?: 'standard' | 'premium' | null; /** * This feature is in beta. You can supply the id of a pre-completed custom vocabulary that you submitted through the Custom Vocabularies API * instead of uploading the list of phrases using the custom_vocabularies parameter. * Using custom_vocabulary_id or custom_vocabularies with the same list of phrases yields the same transcription result, * but custom_vocabulary_id enables your submission to finish processing faster by 6 seconds on average. * * You cannot use both custom_vocabulary_id and custom_vocabularies at the same time, and doing so will result in a 400 response. * If the supplied id represents an incomplete, deleted, or non-existent custom vocabulary then you will receive a 404 response. */ custom_vocabulary_id?: string | null; /** * Specify a collection of custom vocabulary to be used for this job. * Custom vocabulary informs and biases the speech recognition to find those phrases (at the cost of slightly slower transcription). */ custom_vocabularies?: Array<object>; /** * If true, only exact phrases will be used as custom vocabulary, i.e. phrases will not be split into individual words for processing. * By default is enabled. */ strict_custom_vocabulary?: boolean; /** * Use to specify summarization options. This option is not available for human transcription jobs. */ summarization_config?: { /** * Model type for summarization. * @default "standard" */ model?: 'standard' | 'premium' | null; /** * Summarization formatting type. Use Paragraph for a text summary or Bullets for a list of topics. * prompt and type parameters are mutuially exclusive. * @default "paragraph" */ type?: 'paragraph' | 'bullets' | null; /** * Custom prompt. Provides the most flexible way to create summaries, but may lead to unpredictable results. * Summary is produced in Markdown format. * prompt and type parameters are mutuially exclusive. */ prompt?: string | null; } | null; /** * Use to specify translation options. This option is not available for human transcription jobs. */ translation_config?: { /** * Target languages for translation. */ target_languages: Array<{ /** * Target language for translation. */ language: | 'en' | 'en-us' | 'en-gb' | 'ar' | 'pt' | 'pt-br' | 'pt-pt' | 'fr' | 'fr-ca' | 'es' | 'es-es' | 'es-la' | 'it' | 'ja' | 'ko' | 'de' | 'ru'; }>; /** * Model type for translation. * @default "standard" */ model?: 'standard' | 'premium' | null; } | null; /** * Language is provided as a ISO 639-1 language code, with exceptions. * Only 1 language can be selected per audio, i.e. no multiple languages in one transcription job. * @default "en" */ language?: string | null; /** * Provides improved accuracy for per-word timestamps for a transcript. * * The following languages are currently supported: * - English (en, en-us, en-gb) * - French (fr) * - Italian (it) * - German (de) * - Spanish (es) * * This option is not available in low-cost environment * @default false */ forced_alignment?: boolean | null; }; --- File: /ai/packages/revai/src/revai-config.ts --- import { FetchFunction } from '@ai-sdk/provider-utils'; export type RevaiConfig = { provider: string; url: (options: { modelId: string; path: string }) => string; headers: () => Record<string, string | undefined>; fetch?: FetchFunction; generateId?: () => string; }; --- File: /ai/packages/revai/src/revai-error.test.ts --- import { safeParseJSON } from '@ai-sdk/provider-utils'; import { revaiErrorDataSchema } from './revai-error'; describe('revaiErrorDataSchema', () => { it('should parse Rev.ai resource exhausted error', async () => { const error = ` {"error":{"message":"{\\n \\"error\\": {\\n \\"code\\": 429,\\n \\"message\\": \\"Resource has been exhausted (e.g. check quota).\\",\\n \\"status\\": \\"RESOURCE_EXHAUSTED\\"\\n }\\n}\\n","code":429}} `; const result = await safeParseJSON({ text: error, schema: revaiErrorDataSchema, }); expect(result).toStrictEqual({ success: true, value: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, rawValue: { error: { message: '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n', code: 429, }, }, }); }); }); --- File: /ai/packages/revai/src/revai-error.ts --- import { z } from 'zod/v4'; import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; export const revaiErrorDataSchema = z.object({ error: z.object({ message: z.string(), code: z.number(), }), }); export type RevaiErrorData = z.infer<typeof revaiErrorDataSchema>; export const revaiFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: revaiErrorDataSchema, errorToMessage: data => data.error.message, }); --- File: /ai/packages/revai/src/revai-provider.ts --- import { TranscriptionModelV2, ProviderV2, NoSuchModelError, } from '@ai-sdk/provider'; import { FetchFunction, loadApiKey } from '@ai-sdk/provider-utils'; import { RevaiTranscriptionModel } from './revai-transcription-model'; import { RevaiTranscriptionModelId } from './revai-transcription-options'; export interface RevaiProvider extends ProviderV2 { ( modelId: 'machine', settings?: {}, ): { transcription: RevaiTranscriptionModel; }; /** Creates a model for transcription. */ transcription(modelId: RevaiTranscriptionModelId): TranscriptionModelV2; } export interface RevaiProviderSettings { /** API key for authenticating requests. */ apiKey?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } /** Create a Rev.ai provider instance. */ export function createRevai( options: RevaiProviderSettings = {}, ): RevaiProvider { const getHeaders = () => ({ authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'REVAI_API_KEY', description: 'Rev.ai', })}`, ...options.headers, }); const createTranscriptionModel = (modelId: RevaiTranscriptionModelId) => new RevaiTranscriptionModel(modelId, { provider: `revai.transcription`, url: ({ path }) => `https://api.rev.ai${path}`, headers: getHeaders, fetch: options.fetch, }); const provider = function (modelId: RevaiTranscriptionModelId) { return { transcription: createTranscriptionModel(modelId), }; }; provider.transcription = createTranscriptionModel; provider.transcriptionModel = createTranscriptionModel; provider.languageModel = () => { throw new NoSuchModelError({ modelId: 'unknown', modelType: 'languageModel', message: 'Rev.ai does not provide language models', }); }; provider.textEmbeddingModel = () => { throw new NoSuchModelError({ modelId: 'unknown', modelType: 'textEmbeddingModel', message: 'Rev.ai does not provide text embedding models', }); }; provider.imageModel = () => { throw new NoSuchModelError({ modelId: 'unknown', modelType: 'imageModel', message: 'Rev.ai does not provide image models', }); }; return provider as RevaiProvider; } /** Default Rev.ai provider instance. */ export const revai = createRevai(); --- File: /ai/packages/revai/src/revai-transcription-model.test.ts --- import { createTestServer } from '@ai-sdk/provider-utils/test'; import { RevaiTranscriptionModel } from './revai-transcription-model'; import { createRevai } from './revai-provider'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; const audioData = await readFile(path.join(__dirname, 'transcript-test.mp3')); const provider = createRevai({ apiKey: 'test-api-key' }); const model = provider.transcription('machine'); const server = createTestServer({ 'https://api.rev.ai/speechtotext/v1/jobs': {}, 'https://api.rev.ai/speechtotext/v1/jobs/test-id': {}, 'https://api.rev.ai/speechtotext/v1/jobs/test-id/transcript': {}, }); describe('doGenerate', () => { function prepareJsonResponse({ headers, }: { headers?: Record<string, string>; } = {}) { server.urls['https://api.rev.ai/speechtotext/v1/jobs'].response = { type: 'json-value', headers, body: { id: 'test-id', status: 'in_progress', language: 'en', created_on: '2018-05-05T23:23:22.29Z', transcriber: 'machine', }, }; server.urls['https://api.rev.ai/speechtotext/v1/jobs/test-id'].response = { type: 'json-value', headers, body: { id: 'test-id', status: 'transcribed', language: 'en', created_on: '2018-05-05T23:23:22.29Z', transcriber: 'machine', }, }; server.urls[ 'https://api.rev.ai/speechtotext/v1/jobs/test-id/transcript' ].response = { type: 'json-value', headers, body: { monologues: [ { speaker: 1, elements: [ { type: 'text', value: 'Hello', ts: 0.5, end_ts: 1.5, confidence: 1, }, { type: 'punct', value: ' ', }, { type: 'text', value: 'World', ts: 1.75, end_ts: 2.85, confidence: 0.8, }, { type: 'punct', value: '.', }, ], }, { speaker: 2, elements: [ { type: 'text', value: 'monologues', ts: 3, end_ts: 3.5, confidence: 1, }, { type: 'punct', value: ' ', }, { type: 'text', value: 'are', ts: 3.6, end_ts: 3.9, confidence: 1, }, { type: 'punct', value: ' ', }, { type: 'text', value: 'a', ts: 4, end_ts: 4.3, confidence: 1, }, { type: 'punct', value: ' ', }, { type: 'text', value: 'block', ts: 4.5, end_ts: 5.5, confidence: 1, }, { type: 'punct', value: ' ', }, { type: 'text', value: 'of', ts: 5.75, end_ts: 6.14, confidence: 1, }, { type: 'punct', value: ' ', }, { type: 'unknown', value: '<inaudible>', }, { type: 'punct', value: ' ', }, { type: 'text', value: 'text', ts: 6.5, end_ts: 7.78, confidence: 1, }, { type: 'punct', value: '.', }, ], }, ], }, }; } it('should pass the model', async () => { prepareJsonResponse(); await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(await server.calls[0].requestBodyMultipart).toMatchObject({ media: expect.any(File), config: '{"transcriber":"machine"}', }); }); it('should pass headers', async () => { prepareJsonResponse(); const provider = createRevai({ apiKey: 'test-api-key', headers: { 'Custom-Provider-Header': 'provider-header-value', }, }); await provider.transcription('machine').doGenerate({ audio: audioData, mediaType: 'audio/wav', headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toMatchObject({ authorization: 'Bearer test-api-key', 'content-type': expect.stringMatching( /^multipart\/form-data; boundary=----formdata-undici-\d+$/, ), 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should extract the transcription text', async () => { prepareJsonResponse(); const result = await model.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.text).toBe( 'Hello World. monologues are a block of <inaudible> text.', ); }); it('should include response data with timestamp, modelId and headers', async () => { prepareJsonResponse({ headers: { 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); const testDate = new Date(0); const customModel = new RevaiTranscriptionModel('machine', { provider: 'test-provider', url: ({ path }) => `https://api.rev.ai${path}`, headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.response).toMatchObject({ timestamp: testDate, modelId: 'machine', headers: { 'content-type': 'application/json', 'x-request-id': 'test-request-id', 'x-ratelimit-remaining': '123', }, }); }); it('should use real date when no custom date provider is specified', async () => { prepareJsonResponse(); const testDate = new Date(0); const customModel = new RevaiTranscriptionModel('machine', { provider: 'test-provider', url: ({ path }) => `https://api.rev.ai${path}`, headers: () => ({}), _internal: { currentDate: () => testDate, }, }); const result = await customModel.doGenerate({ audio: audioData, mediaType: 'audio/wav', }); expect(result.response.timestamp.getTime()).toEqual(testDate.getTime()); expect(result.response.modelId).toBe('machine'); }); }); --- File: /ai/packages/revai/src/revai-transcription-model.ts --- import { AISDKError, TranscriptionModelV2, TranscriptionModelV2CallWarning, } from '@ai-sdk/provider'; import { combineHeaders, convertBase64ToUint8Array, createJsonResponseHandler, delay, getFromApi, parseProviderOptions, postFormDataToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { RevaiConfig } from './revai-config'; import { revaiFailedResponseHandler } from './revai-error'; import { RevaiTranscriptionModelId } from './revai-transcription-options'; import { RevaiTranscriptionAPITypes } from './revai-api-types'; // https://docs.rev.ai/api/asynchronous/reference/#operation/SubmitTranscriptionJob const revaiProviderOptionsSchema = z.object({ /** * Optional metadata string to associate with the transcription job. */ metadata: z.string().nullish(), /** * Configuration for webhook notifications when job is complete. */ notification_config: z .object({ /** * URL to send the notification to. */ url: z.string(), /** * Optional authorization headers for the notification request. */ auth_headers: z .object({ Authorization: z.string(), }) .nullish(), }) .nullish(), /** * Number of seconds after which the job will be automatically deleted. */ delete_after_seconds: z.number().nullish(), /** * Whether to include filler words and false starts in the transcription. */ verbatim: z.boolean().optional(), /** * Whether to prioritize the job for faster processing. */ rush: z.boolean().nullish().default(false), /** * Whether to run the job in test mode. */ test_mode: z.boolean().nullish().default(false), /** * Specific segments of the audio to transcribe. */ segments_to_transcribe: z .array( z.object({ /** * Start time of the segment in seconds. */ start: z.number(), /** * End time of the segment in seconds. */ end: z.number(), }), ) .nullish(), /** * Names to assign to speakers in the transcription. */ speaker_names: z .array( z.object({ /** * Display name for the speaker. */ display_name: z.string(), }), ) .nullish(), /** * Whether to skip speaker diarization. */ skip_diarization: z.boolean().nullish().default(false), /** * Whether to skip post-processing steps. */ skip_postprocessing: z.boolean().nullish().default(false), /** * Whether to skip adding punctuation to the transcription. */ skip_punctuation: z.boolean().nullish().default(false), /** * Whether to remove disfluencies (um, uh, etc.) from the transcription. */ remove_disfluencies: z.boolean().nullish().default(false), /** * Whether to remove atmospheric sounds from the transcription. */ remove_atmospherics: z.boolean().nullish().default(false), /** * Whether to filter profanity from the transcription. */ filter_profanity: z.boolean().nullish().default(false), /** * Number of speaker channels in the audio. */ speaker_channels_count: z.number().nullish(), /** * Expected number of speakers in the audio. */ speakers_count: z.number().nullish(), /** * Type of diarization to use. */ diarization_type: z .enum(['standard', 'premium']) .nullish() .default('standard'), /** * ID of a custom vocabulary to use for the transcription. */ custom_vocabulary_id: z.string().nullish(), /** * Custom vocabularies to use for the transcription. */ custom_vocabularies: z.array(z.object({})).optional(), /** * Whether to strictly enforce custom vocabulary. */ strict_custom_vocabulary: z.boolean().optional(), /** * Configuration for generating a summary of the transcription. */ summarization_config: z .object({ /** * Model to use for summarization. */ model: z.enum(['standard', 'premium']).nullish().default('standard'), /** * Format of the summary. */ type: z.enum(['paragraph', 'bullets']).nullish().default('paragraph'), /** * Custom prompt for the summarization. */ prompt: z.string().nullish(), }) .nullish(), /** * Configuration for translating the transcription. */ translation_config: z .object({ /** * Target languages for translation. */ target_languages: z.array( z.object({ /** * Language code for translation target. */ language: z.enum([ 'en', 'en-us', 'en-gb', 'ar', 'pt', 'pt-br', 'pt-pt', 'fr', 'fr-ca', 'es', 'es-es', 'es-la', 'it', 'ja', 'ko', 'de', 'ru', ]), }), ), /** * Model to use for translation. */ model: z.enum(['standard', 'premium']).nullish().default('standard'), }) .nullish(), /** * Language of the audio content. */ language: z.string().nullish().default('en'), /** * Whether to perform forced alignment. */ forced_alignment: z.boolean().nullish().default(false), }); export type RevaiTranscriptionCallOptions = z.infer< typeof revaiProviderOptionsSchema >; interface RevaiTranscriptionModelConfig extends RevaiConfig { _internal?: { currentDate?: () => Date; }; } export class RevaiTranscriptionModel implements TranscriptionModelV2 { readonly specificationVersion = 'v2'; get provider(): string { return this.config.provider; } constructor( readonly modelId: RevaiTranscriptionModelId, private readonly config: RevaiTranscriptionModelConfig, ) {} private async getArgs({ audio, mediaType, providerOptions, }: Parameters<TranscriptionModelV2['doGenerate']>[0]) { const warnings: TranscriptionModelV2CallWarning[] = []; // Parse provider options const revaiOptions = await parseProviderOptions({ provider: 'revai', providerOptions, schema: revaiProviderOptionsSchema, }); // Create form data with base fields const formData = new FormData(); const blob = audio instanceof Uint8Array ? new Blob([audio]) : new Blob([convertBase64ToUint8Array(audio)]); formData.append('media', new File([blob], 'audio', { type: mediaType })); const transcriptionModelOptions: RevaiTranscriptionAPITypes = { transcriber: this.modelId, }; // Add provider-specific options if (revaiOptions) { const formDataConfig: RevaiTranscriptionAPITypes = { metadata: revaiOptions.metadata ?? undefined, notification_config: revaiOptions.notification_config ?? undefined, delete_after_seconds: revaiOptions.delete_after_seconds ?? undefined, verbatim: revaiOptions.verbatim ?? undefined, rush: revaiOptions.rush ?? undefined, test_mode: revaiOptions.test_mode ?? undefined, segments_to_transcribe: revaiOptions.segments_to_transcribe ?? undefined, speaker_names: revaiOptions.speaker_names ?? undefined, skip_diarization: revaiOptions.skip_diarization ?? undefined, skip_postprocessing: revaiOptions.skip_postprocessing ?? undefined, skip_punctuation: revaiOptions.skip_punctuation ?? undefined, remove_disfluencies: revaiOptions.remove_disfluencies ?? undefined, remove_atmospherics: revaiOptions.remove_atmospherics ?? undefined, filter_profanity: revaiOptions.filter_profanity ?? undefined, speaker_channels_count: revaiOptions.speaker_channels_count ?? undefined, speakers_count: revaiOptions.speakers_count ?? undefined, diarization_type: revaiOptions.diarization_type ?? undefined, custom_vocabulary_id: revaiOptions.custom_vocabulary_id ?? undefined, custom_vocabularies: revaiOptions.custom_vocabularies ?? undefined, strict_custom_vocabulary: revaiOptions.strict_custom_vocabulary ?? undefined, summarization_config: revaiOptions.summarization_config ?? undefined, translation_config: revaiOptions.translation_config ?? undefined, language: revaiOptions.language ?? undefined, forced_alignment: revaiOptions.forced_alignment ?? undefined, }; for (const key in formDataConfig) { const value = formDataConfig[key as keyof RevaiTranscriptionAPITypes]; if (value !== undefined) { (transcriptionModelOptions as Record<string, unknown>)[ key as keyof RevaiTranscriptionAPITypes ] = value; } } } formData.append('config', JSON.stringify(transcriptionModelOptions)); return { formData, warnings, }; } async doGenerate( options: Parameters<TranscriptionModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<TranscriptionModelV2['doGenerate']>>> { const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const { formData, warnings } = await this.getArgs(options); const { value: submissionResponse } = await postFormDataToApi({ url: this.config.url({ path: '/speechtotext/v1/jobs', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), formData, failedResponseHandler: revaiFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( revaiTranscriptionJobResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); if (submissionResponse.status === 'failed') { throw new AISDKError({ message: 'Failed to submit transcription job to Rev.ai', name: 'TranscriptionJobSubmissionFailed', cause: submissionResponse, }); } const jobId = submissionResponse.id; const timeoutMs = 60 * 1000; // 60 seconds timeout const startTime = Date.now(); const pollingInterval = 1000; let jobResponse = submissionResponse; while (jobResponse.status !== 'transcribed') { // Check if we've exceeded the timeout if (Date.now() - startTime > timeoutMs) { throw new AISDKError({ message: 'Transcription job polling timed out', name: 'TranscriptionJobPollingTimedOut', cause: submissionResponse, }); } // Poll for job status const pollingResult = await getFromApi({ url: this.config.url({ path: `/speechtotext/v1/jobs/${jobId}`, modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), failedResponseHandler: revaiFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( revaiTranscriptionJobResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); jobResponse = pollingResult.value; if (jobResponse.status === 'failed') { throw new AISDKError({ message: 'Transcription job failed', name: 'TranscriptionJobFailed', cause: jobResponse, }); } // Wait before polling again (only if we need to continue polling) if (jobResponse.status !== 'transcribed') { await delay(pollingInterval); } } const { value: transcriptionResult, responseHeaders, rawValue: rawResponse, } = await getFromApi({ url: this.config.url({ path: `/speechtotext/v1/jobs/${jobId}/transcript`, modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), failedResponseHandler: revaiFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( revaiTranscriptionResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); let durationInSeconds = 0; const segments: { text: string; startSecond: number; endSecond: number; }[] = []; for (const monologue of transcriptionResult.monologues ?? []) { // Process each monologue to extract segments with timing information let currentSegmentText = ''; let segmentStartSecond = 0; let hasStartedSegment = false; for (const element of monologue?.elements ?? []) { // Add the element value to the current segment text currentSegmentText += element.value; // For text elements, track timing information if (element.type === 'text') { // Update the overall duration if this is the latest timestamp if (element.end_ts && element.end_ts > durationInSeconds) { durationInSeconds = element.end_ts; } // If this is the first text element in a segment, mark the start time if (!hasStartedSegment && typeof element.ts === 'number') { segmentStartSecond = element.ts; hasStartedSegment = true; } // If we have an end timestamp, we can complete a segment if (typeof element.end_ts === 'number' && hasStartedSegment) { // Only add non-empty segments if (currentSegmentText.trim()) { segments.push({ text: currentSegmentText.trim(), startSecond: segmentStartSecond, endSecond: element.end_ts, }); } // Reset for the next segment currentSegmentText = ''; hasStartedSegment = false; } } } // Handle any remaining segment text that wasn't added if (hasStartedSegment && currentSegmentText.trim()) { const endSecond = durationInSeconds > segmentStartSecond ? durationInSeconds : segmentStartSecond + 1; segments.push({ text: currentSegmentText.trim(), startSecond: segmentStartSecond, endSecond: endSecond, }); } } return { text: transcriptionResult.monologues ?.map(monologue => monologue?.elements?.map(element => element.value).join(''), ) .join(' ') ?? '', segments, language: submissionResponse.language ?? undefined, durationInSeconds, warnings, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, body: rawResponse, }, }; } } const revaiTranscriptionJobResponseSchema = z.object({ id: z.string().nullish(), status: z.string().nullish(), language: z.string().nullish(), }); const revaiTranscriptionResponseSchema = z.object({ monologues: z .array( z.object({ elements: z .array( z.object({ type: z.string().nullish(), value: z.string().nullish(), ts: z.number().nullish(), end_ts: z.number().nullish(), }), ) .nullish(), }), ) .nullish(), }); --- File: /ai/packages/revai/src/revai-transcription-options.ts --- export type RevaiTranscriptionModelId = 'machine' | 'low_cost' | 'fusion'; --- File: /ai/packages/revai/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/revai/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/revai/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/rsc/src/shared-client/context.tsx --- /* eslint-disable react-hooks/exhaustive-deps */ 'use client'; import * as React from 'react'; import * as jsondiffpatch from 'jsondiffpatch'; import { isFunction } from '../util/is-function'; import type { AIProvider, InferActions, InferAIState, InferUIState, InternalAIProviderProps, ValueOrUpdater, } from '../types'; const InternalUIStateProvider = React.createContext<null | any>(null); const InternalAIStateProvider = React.createContext<undefined | any>(undefined); const InternalActionProvider = React.createContext<null | any>(null); const InternalSyncUIStateProvider = React.createContext<null | any>(null); export function InternalAIProvider({ children, initialUIState, initialAIState, initialAIStatePatch, wrappedActions, wrappedSyncUIState, }: InternalAIProviderProps) { if (!('use' in React)) { throw new Error('Unsupported React version.'); } const uiState = React.useState(initialUIState); const setUIState = uiState[1]; const resolvedInitialAIStatePatch = initialAIStatePatch ? (React as any).use(initialAIStatePatch) : undefined; initialAIState = React.useMemo(() => { if (resolvedInitialAIStatePatch) { return jsondiffpatch.patch( jsondiffpatch.clone(initialAIState), resolvedInitialAIStatePatch, ); } return initialAIState; }, [initialAIState, resolvedInitialAIStatePatch]); const aiState = React.useState(initialAIState); const setAIState = aiState[1]; const aiStateRef = React.useRef(aiState[0]); React.useEffect(() => { aiStateRef.current = aiState[0]; }, [aiState[0]]); const clientWrappedActions = React.useMemo( () => Object.fromEntries( Object.entries(wrappedActions).map(([key, action]) => [ key, async (...args: any) => { const aiStateSnapshot = aiStateRef.current; const [aiStateDelta, result] = await action( aiStateSnapshot, ...args, ); (async () => { const delta = await aiStateDelta; if (delta !== undefined) { aiState[1]( jsondiffpatch.patch( jsondiffpatch.clone(aiStateSnapshot), delta, ), ); } })(); return result; }, ]), ), [wrappedActions], ); const clientWrappedSyncUIStateAction = React.useMemo(() => { if (!wrappedSyncUIState) { return () => {}; } return async () => { const aiStateSnapshot = aiStateRef.current; const [aiStateDelta, uiState] = await wrappedSyncUIState!(aiStateSnapshot); if (uiState !== undefined) { setUIState(uiState); } const delta = await aiStateDelta; if (delta !== undefined) { const patchedAiState = jsondiffpatch.patch( jsondiffpatch.clone(aiStateSnapshot), delta, ); setAIState(patchedAiState); } }; }, [wrappedSyncUIState]); return ( <InternalAIStateProvider.Provider value={aiState}> <InternalUIStateProvider.Provider value={uiState}> <InternalActionProvider.Provider value={clientWrappedActions}> <InternalSyncUIStateProvider.Provider value={clientWrappedSyncUIStateAction} > {children} </InternalSyncUIStateProvider.Provider> </InternalActionProvider.Provider> </InternalUIStateProvider.Provider> </InternalAIStateProvider.Provider> ); } export function useUIState<AI extends AIProvider = any>() { type T = InferUIState<AI, any>; const state = React.useContext< [T, (v: T | ((v_: T) => T)) => void] | null | undefined >(InternalUIStateProvider); if (state === null) { throw new Error('`useUIState` must be used inside an <AI> provider.'); } if (!Array.isArray(state)) { throw new Error('Invalid state'); } if (state[0] === undefined) { throw new Error( '`initialUIState` must be provided to `createAI` or `<AI>`', ); } return state; } // TODO: How do we avoid causing a re-render when the AI state changes but you // are only listening to a specific key? We need useSES perhaps? function useAIState<AI extends AIProvider = any>(): [ InferAIState<AI, any>, (newState: ValueOrUpdater<InferAIState<AI, any>>) => void, ]; function useAIState<AI extends AIProvider = any>( key: keyof InferAIState<AI, any>, ): [ InferAIState<AI, any>[typeof key], (newState: ValueOrUpdater<InferAIState<AI, any>[typeof key]>) => void, ]; function useAIState<AI extends AIProvider = any>( ...args: [] | [keyof InferAIState<AI, any>] ) { type T = InferAIState<AI, any>; const state = React.useContext< [T, (newState: ValueOrUpdater<T>) => void] | null | undefined >(InternalAIStateProvider); if (state === null) { throw new Error('`useAIState` must be used inside an <AI> provider.'); } if (!Array.isArray(state)) { throw new Error('Invalid state'); } if (state[0] === undefined) { throw new Error( '`initialAIState` must be provided to `createAI` or `<AI>`', ); } if (args.length >= 1 && typeof state[0] !== 'object') { throw new Error( 'When using `useAIState` with a key, the AI state must be an object.', ); } const key = args[0]; const setter = React.useCallback( typeof key === 'undefined' ? state[1] : (newState: ValueOrUpdater<T>) => { if (isFunction(newState)) { return state[1](s => { return { ...s, [key]: newState(s[key]) }; }); } else { return state[1]({ ...state[0], [key]: newState }); } }, [key], ); if (args.length === 0) { return state; } else { return [state[0][args[0]], setter]; } } export function useActions<AI extends AIProvider = any>() { type T = InferActions<AI, any>; const actions = React.useContext<T>(InternalActionProvider); return actions; } export function useSyncUIState() { const syncUIState = React.useContext<() => Promise<void>>( InternalSyncUIStateProvider, ); if (syncUIState === null) { throw new Error('`useSyncUIState` must be used inside an <AI> provider.'); } return syncUIState; } export { useAIState }; --- File: /ai/packages/rsc/src/shared-client/index.ts --- 'use client'; export { readStreamableValue } from '../streamable-value/read-streamable-value'; export { useStreamableValue } from '../streamable-value/use-streamable-value'; export { InternalAIProvider, useAIState, useActions, useSyncUIState, useUIState, } from './context'; --- File: /ai/packages/rsc/src/stream-ui/index.tsx --- export { streamUI } from './stream-ui'; --- File: /ai/packages/rsc/src/stream-ui/stream-ui.tsx --- import { LanguageModelV2, LanguageModelV2CallWarning } from '@ai-sdk/provider'; import { InferSchema, ProviderOptions, safeParseJSON, } from '@ai-sdk/provider-utils'; import { ReactNode } from 'react'; import * as z3 from 'zod/v3'; import * as z4 from 'zod/v4'; import { CallWarning, FinishReason, LanguageModelUsage, ToolChoice, Prompt, CallSettings, InvalidToolInputError, NoSuchToolError, Schema, } from 'ai'; import { standardizePrompt, prepareToolsAndToolChoice, prepareRetries, prepareCallSettings, convertToLanguageModelPrompt, } from 'ai/internal'; import { createResolvablePromise } from '../util/create-resolvable-promise'; import { isAsyncGenerator } from '../util/is-async-generator'; import { isGenerator } from '../util/is-generator'; import { createStreamableUI } from '../streamable-ui/create-streamable-ui'; type Streamable = ReactNode | Promise<ReactNode>; type Renderer<T extends Array<any>> = ( ...args: T ) => | Streamable | Generator<Streamable, Streamable, void> | AsyncGenerator<Streamable, Streamable, void>; type RenderTool< INPUT_SCHEMA extends z4.core.$ZodType | z3.Schema | Schema = any, > = { description?: string; inputSchema: INPUT_SCHEMA; generate?: Renderer< [ InferSchema<INPUT_SCHEMA>, { toolName: string; toolCallId: string; }, ] >; }; type RenderText = Renderer< [ { /** * The full text content from the model so far. */ content: string; /** * The new appended text content from the model since the last `text` call. */ delta: string; /** * Whether the model is done generating text. * If `true`, the `content` will be the final output and this call will be the last. */ done: boolean; }, ] >; type RenderResult = { value: ReactNode; } & Awaited<ReturnType<LanguageModelV2['doStream']>>; const defaultTextRenderer: RenderText = ({ content }: { content: string }) => content; /** * `streamUI` is a helper function to create a streamable UI from LLMs. */ export async function streamUI< TOOLS extends { [name: string]: z4.core.$ZodType | z3.Schema | Schema } = {}, >({ model, tools, toolChoice, system, prompt, messages, maxRetries, abortSignal, headers, initial, text, providerOptions, onFinish, ...settings }: CallSettings & Prompt & { /** * The language model to use. */ model: LanguageModelV2; /** * The tools that the model can call. The model needs to support calling tools. */ tools?: { [name in keyof TOOLS]: RenderTool<TOOLS[name]>; }; /** * The tool choice strategy. Default: 'auto'. */ toolChoice?: ToolChoice<TOOLS>; text?: RenderText; initial?: ReactNode; /** Additional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; /** * Callback that is called when the LLM response and the final object validation are finished. */ onFinish?: (event: { /** * The reason why the generation finished. */ finishReason: FinishReason; /** * The token usage of the generated response. */ usage: LanguageModelUsage; /** * The final ui node that was generated. */ value: ReactNode; /** * Warnings from the model provider (e.g. unsupported settings) */ warnings?: CallWarning[]; /** * Optional response data. */ response?: { /** * Response headers. */ headers?: Record<string, string>; }; }) => Promise<void> | void; }): Promise<RenderResult> { // TODO: Remove these errors after the experimental phase. if (typeof model === 'string') { throw new Error( '`model` cannot be a string in `streamUI`. Use the actual model instance instead.', ); } if ('functions' in settings) { throw new Error( '`functions` is not supported in `streamUI`, use `tools` instead.', ); } if ('provider' in settings) { throw new Error( '`provider` is no longer needed in `streamUI`. Use `model` instead.', ); } if (tools) { for (const [name, tool] of Object.entries(tools)) { if ('render' in tool) { throw new Error( 'Tool definition in `streamUI` should not have `render` property. Use `generate` instead. Found in tool: ' + name, ); } } } const ui = createStreamableUI(initial); // The default text renderer just returns the content as string. const textRender = text || defaultTextRenderer; let finished: Promise<void> | undefined; let finishEvent: { finishReason: FinishReason; usage: LanguageModelUsage; warnings?: CallWarning[]; response?: { headers?: Record<string, string>; }; } | null = null; async function render({ args, renderer, streamableUI, isLastCall = false, }: { renderer: undefined | Renderer<any>; args: [payload: any] | [payload: any, options: any]; streamableUI: ReturnType<typeof createStreamableUI>; isLastCall?: boolean; }) { if (!renderer) return; // create a promise that will be resolved when the render call is finished. // it is appended to the `finished` promise chain to ensure the render call // is finished before the next render call starts. const renderFinished = createResolvablePromise<void>(); finished = finished ? finished.then(() => renderFinished.promise) : renderFinished.promise; const rendererResult = renderer(...args); if (isAsyncGenerator(rendererResult) || isGenerator(rendererResult)) { while (true) { const { done, value } = await rendererResult.next(); const node = await value; if (isLastCall && done) { streamableUI.done(node); } else { streamableUI.update(node); } if (done) break; } } else { const node = await rendererResult; if (isLastCall) { streamableUI.done(node); } else { streamableUI.update(node); } } // resolve the promise to signal that the render call is finished renderFinished.resolve(undefined); } const { retry } = prepareRetries({ maxRetries, abortSignal }); const validatedPrompt = await standardizePrompt({ system, prompt, messages, }); const result = await retry(async () => model.doStream({ ...prepareCallSettings(settings), ...prepareToolsAndToolChoice({ tools: tools as any, toolChoice, activeTools: undefined, }), prompt: await convertToLanguageModelPrompt({ prompt: validatedPrompt, supportedUrls: await model.supportedUrls, }), providerOptions, abortSignal, headers, includeRawChunks: false, }), ); // For the stream and consume it asynchronously: const [stream, forkedStream] = result.stream.tee(); (async () => { try { let content = ''; let hasToolCall = false; let warnings: LanguageModelV2CallWarning[] | undefined; const reader = forkedStream.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; switch (value.type) { case 'stream-start': { warnings = value.warnings; break; } case 'text-delta': { content += value.delta; render({ renderer: textRender, args: [{ content, done: false, delta: value.delta }], streamableUI: ui, }); break; } case 'tool-input-start': case 'tool-input-delta': { hasToolCall = true; break; } case 'tool-call': { const toolName = value.toolName as keyof TOOLS & string; if (!tools) { throw new NoSuchToolError({ toolName }); } const tool = tools[toolName]; if (!tool) { throw new NoSuchToolError({ toolName, availableTools: Object.keys(tools), }); } hasToolCall = true; const parseResult = await safeParseJSON({ text: value.input, schema: tool.inputSchema, }); if (parseResult.success === false) { throw new InvalidToolInputError({ toolName, toolInput: value.input, cause: parseResult.error, }); } render({ renderer: tool.generate, args: [ parseResult.value, { toolName, toolCallId: value.toolCallId, }, ], streamableUI: ui, isLastCall: true, }); break; } case 'error': { throw value.error; } case 'finish': { finishEvent = { finishReason: value.finishReason, usage: value.usage, warnings, response: result.response, }; break; } } } if (!hasToolCall) { render({ renderer: textRender, args: [{ content, done: true }], streamableUI: ui, isLastCall: true, }); } await finished; if (finishEvent && onFinish) { await onFinish({ ...finishEvent, value: ui.value, }); } } catch (error) { // During the stream rendering, we don't want to throw the error to the // parent scope but only let the React's error boundary to catch it. ui.error(error); } })(); return { ...result, stream, value: ui.value, }; } --- File: /ai/packages/rsc/src/stream-ui/stream-ui.ui.test.tsx --- import { delay } from '@ai-sdk/provider-utils'; import { convertArrayToReadableStream } from '@ai-sdk/provider-utils/test'; import { LanguageModelUsage } from 'ai'; import { MockLanguageModelV2 } from 'ai/test'; import { z } from 'zod/v4'; import { streamUI } from './stream-ui'; async function recursiveResolve(val: any): Promise<any> { if (val && typeof val === 'object' && typeof val.then === 'function') { return await recursiveResolve(await val); } if (Array.isArray(val)) { return await Promise.all(val.map(recursiveResolve)); } if (val && typeof val === 'object') { const result: any = {}; for (const key in val) { result[key] = await recursiveResolve(val[key]); } return result; } return val; } async function simulateFlightServerRender(node: React.ReactNode) { async function traverse(node: any): Promise<any> { if (!node) return {}; // Let's only do one level of promise resolution here. As it's only for testing purposes. const props = await recursiveResolve({ ...node.props }); const { type } = node; const { children, ...otherProps } = props; const typeName = typeof type === 'function' ? type.name : String(type); return { type: typeName, props: otherProps, children: typeof children === 'string' ? children : Array.isArray(children) ? children.map(traverse) : await traverse(children), }; } return traverse(node); } const testUsage: LanguageModelUsage = { inputTokens: 3, outputTokens: 10, totalTokens: 13, }; const mockTextModel = new MockLanguageModelV2({ doStream: async () => { return { stream: convertArrayToReadableStream([ { type: 'text-start', id: '0' }, { type: 'text-delta', id: '0', delta: '{ ' }, { type: 'text-delta', id: '0', delta: '"content": ' }, { type: 'text-delta', id: '0', delta: `"Hello, ` }, { type: 'text-delta', id: '0', delta: `world` }, { type: 'text-delta', id: '0', delta: `!"` }, { type: 'text-delta', id: '0', delta: ' }' }, { type: 'text-end', id: '0' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }); const mockToolModel = new MockLanguageModelV2({ doStream: async () => { return { stream: convertArrayToReadableStream([ { type: 'tool-call', toolCallType: 'function', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }); describe('result.value', () => { it('should render text', async () => { const result = await streamUI({ model: mockTextModel, prompt: '', }); const rendered = await simulateFlightServerRender(result.value); expect(rendered).toMatchSnapshot(); }); it('should render text function returned ui', async () => { const result = await streamUI({ model: mockTextModel, prompt: '', text: ({ content }) => <h1>{content}</h1>, }); const rendered = await simulateFlightServerRender(result.value); expect(rendered).toMatchSnapshot(); }); it('should render tool call results', async () => { const result = await streamUI({ model: mockToolModel, prompt: '', tools: { tool1: { description: 'test tool 1', inputSchema: z.object({ value: z.string(), }), generate: async ({ value }) => { await delay(100); return <div>tool1: {value}</div>; }, }, }, }); const rendered = await simulateFlightServerRender(result.value); expect(rendered).toMatchSnapshot(); }); it('should render tool call results with generator render function', async () => { const result = await streamUI({ model: mockToolModel, prompt: '', tools: { tool1: { description: 'test tool 1', inputSchema: z.object({ value: z.string(), }), generate: async function* ({ value }) { yield <div>Loading...</div>; await delay(100); return <div>tool: {value}</div>; }, }, }, }); const rendered = await simulateFlightServerRender(result.value); expect(rendered).toMatchSnapshot(); }); it('should show better error messages if legacy options are passed', async () => { try { await streamUI({ model: mockToolModel, prompt: '', tools: { tool1: { description: 'test tool 1', inputSchema: z.object({ value: z.string(), }), // @ts-expect-error render: async function* () {}, }, }, }); } catch (e) { expect(e).toMatchSnapshot(); } }); }); describe('rsc - streamUI() onFinish callback', () => { let result: Parameters< Required<Parameters<typeof streamUI>[0]>['onFinish'] >[0]; beforeEach(async () => { const ui = await streamUI({ model: mockToolModel, prompt: '', tools: { tool1: { description: 'test tool 1', inputSchema: z.object({ value: z.string(), }), generate: async ({ value }) => { await delay(100); return <div>tool1: {value}</div>; }, }, }, onFinish: event => { result = event; }, }); // consume stream await simulateFlightServerRender(ui.value); }); it('should contain token usage', () => { expect(result.usage).toStrictEqual(testUsage); }); it('should contain finish reason', async () => { expect(result.finishReason).toBe('stop'); }); it('should contain final React node', async () => { expect(result.value).toMatchSnapshot(); }); }); describe('options.headers', () => { it('should pass headers to model', async () => { const result = await streamUI({ model: new MockLanguageModelV2({ doStream: async ({ headers }) => { expect(headers).toStrictEqual({ 'custom-request-header': 'request-header-value', }); return { stream: convertArrayToReadableStream([ { type: 'text-start', id: '0' }, { type: 'text-delta', id: '0', delta: '{ "content": "headers test" }', }, { type: 'text-end', id: '0' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }), prompt: '', headers: { 'custom-request-header': 'request-header-value' }, }); expect(await simulateFlightServerRender(result.value)).toMatchSnapshot(); }); }); describe('options.providerMetadata', () => { it('should pass provider metadata to model', async () => { const result = await streamUI({ model: new MockLanguageModelV2({ doStream: async ({ providerOptions }) => { expect(providerOptions).toStrictEqual({ aProvider: { someKey: 'someValue' }, }); return { stream: convertArrayToReadableStream([ { type: 'text-start', id: '0' }, { type: 'text-delta', id: '0', delta: '{ "content": "provider metadata test" }', }, { type: 'text-end', id: '0' }, { type: 'finish', finishReason: 'stop', usage: testUsage, }, ]), }; }, }), prompt: '', providerOptions: { aProvider: { someKey: 'someValue' }, }, }); expect(await simulateFlightServerRender(result.value)).toMatchSnapshot(); }); }); --- File: /ai/packages/rsc/src/streamable-ui/create-streamable-ui.tsx --- import { HANGING_STREAM_WARNING_TIME_MS } from '../util/constants'; import { createResolvablePromise } from '../util/create-resolvable-promise'; import { createSuspendedChunk } from './create-suspended-chunk'; // It's necessary to define the type manually here, otherwise TypeScript compiler // will not be able to infer the correct return type as it's circular. type StreamableUIWrapper = { /** * The value of the streamable UI. This can be returned from a Server Action and received by the client. */ readonly value: React.ReactNode; /** * This method updates the current UI node. It takes a new UI node and replaces the old one. */ update(value: React.ReactNode): StreamableUIWrapper; /** * This method is used to append a new UI node to the end of the old one. * Once appended a new UI node, the previous UI node cannot be updated anymore. * * @example * ```jsx * const ui = createStreamableUI(<div>hello</div>) * ui.append(<div>world</div>) * * // The UI node will be: * // <> * // <div>hello</div> * // <div>world</div> * // </> * ``` */ append(value: React.ReactNode): StreamableUIWrapper; /** * This method is used to signal that there is an error in the UI stream. * It will be thrown on the client side and caught by the nearest error boundary component. */ error(error: any): StreamableUIWrapper; /** * This method marks the UI node as finalized. You can either call it without any parameters or with a new UI node as the final state. * Once called, the UI node cannot be updated or appended anymore. * * This method is always **required** to be called, otherwise the response will be stuck in a loading state. */ done(...args: [React.ReactNode] | []): StreamableUIWrapper; }; /** * Create a piece of changeable UI that can be streamed to the client. * On the client side, it can be rendered as a normal React node. */ function createStreamableUI(initialValue?: React.ReactNode) { let currentValue = initialValue; let closed = false; let { row, resolve, reject } = createSuspendedChunk(initialValue); function assertStream(method: string) { if (closed) { throw new Error(method + ': UI stream is already closed.'); } } let warningTimeout: NodeJS.Timeout | undefined; function warnUnclosedStream() { if (process.env.NODE_ENV === 'development') { if (warningTimeout) { clearTimeout(warningTimeout); } warningTimeout = setTimeout(() => { console.warn( 'The streamable UI has been slow to update. This may be a bug or a performance issue or you forgot to call `.done()`.', ); }, HANGING_STREAM_WARNING_TIME_MS); } } warnUnclosedStream(); const streamable: StreamableUIWrapper = { value: row, update(value: React.ReactNode) { assertStream('.update()'); // There is no need to update the value if it's referentially equal. if (value === currentValue) { warnUnclosedStream(); return streamable; } const resolvable = createResolvablePromise(); currentValue = value; resolve({ value: currentValue, done: false, next: resolvable.promise }); resolve = resolvable.resolve; reject = resolvable.reject; warnUnclosedStream(); return streamable; }, append(value: React.ReactNode) { assertStream('.append()'); const resolvable = createResolvablePromise(); currentValue = value; resolve({ value, done: false, append: true, next: resolvable.promise }); resolve = resolvable.resolve; reject = resolvable.reject; warnUnclosedStream(); return streamable; }, error(error: any) { assertStream('.error()'); if (warningTimeout) { clearTimeout(warningTimeout); } closed = true; reject(error); return streamable; }, done(...args: [] | [React.ReactNode]) { assertStream('.done()'); if (warningTimeout) { clearTimeout(warningTimeout); } closed = true; if (args.length) { resolve({ value: args[0], done: true }); return streamable; } resolve({ value: currentValue, done: true }); return streamable; }, }; return streamable; } export { createStreamableUI }; --- File: /ai/packages/rsc/src/streamable-ui/create-streamable-ui.ui.test.tsx --- import { delay } from '@ai-sdk/provider-utils'; import { createStreamableUI } from './create-streamable-ui'; // This is a workaround to render the Flight response in a test environment. async function flightRender(node: React.ReactNode, byChunk?: boolean) { const ReactDOM = require('react-dom'); ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactDOMCurrentDispatcher = { current: {} }; const React = require('react'); React.__SECRET_SERVER_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = { ReactSharedServerInternals: {}, ReactCurrentCache: { current: null, }, }; const { renderToReadableStream, } = require('react-server-dom-webpack/server.edge'); const stream = renderToReadableStream(node); const reader = stream.getReader(); const chunks = []; let result = ''; while (true) { const { done, value } = await reader.read(); if (done) { break; } const decoded = new TextDecoder().decode(value); if (byChunk) { chunks.push(decoded); } else { result += decoded; } } return byChunk ? chunks : result; } async function recursiveResolve(val: any): Promise<any> { if (val && typeof val === 'object' && typeof val.then === 'function') { return await recursiveResolve(await val); } if (Array.isArray(val)) { return await Promise.all(val.map(recursiveResolve)); } if (val && typeof val === 'object') { const result: any = {}; for (const key in val) { result[key] = await recursiveResolve(val[key]); } return result; } return val; } async function simulateFlightServerRender(node: React.ReactNode) { async function traverse(node: any): Promise<any> { if (!node) return {}; // Let's only do one level of promise resolution here. As it's only for testing purposes. const props = await recursiveResolve({ ...node.props }); const { type } = node; const { children, ...otherProps } = props; const typeName = typeof type === 'function' ? type.name : String(type); return { type: typeName, props: otherProps, children: typeof children === 'string' ? children : Array.isArray(children) ? children.map(traverse) : await traverse(children), }; } return traverse(node); } function getFinalValueFromResolved(node: any) { if (!node) return node; if (node.type === 'Symbol(react.suspense)') { return getFinalValueFromResolved(node.children); } else if (node.type === '') { let wrapper; let value = node.props.value; let next = node.props.n; let current = node.props.c; while (next) { if (next.append) { if (wrapper === undefined) { wrapper = current; } else if (typeof current === 'string' && typeof wrapper === 'string') { wrapper = wrapper + current; } else { wrapper = ( <> {wrapper} {current} </> ); } } value = next.value; next = next.next; current = value; } return getFinalValueFromResolved( wrapper === undefined ? ( value ) : typeof value === 'string' && typeof wrapper === 'string' ? ( wrapper + value ) : ( <> {wrapper} {value} </> ), ); } return node; } describe('rsc - createStreamableUI()', () => { it('should emit React Nodes that can be updated', async () => { const ui = createStreamableUI(<div>1</div>); ui.update(<div>2</div>); ui.update(<div>3</div>); ui.done(); const final = getFinalValueFromResolved( await simulateFlightServerRender(ui.value), ); expect(final).toMatchInlineSnapshot(` <div> 3 </div> `); }); it('should emit React Nodes that can be updated with .done()', async () => { const ui = createStreamableUI(<div>1</div>); ui.update(<div>2</div>); ui.update(<div>3</div>); ui.done(<div>4</div>); const final = getFinalValueFromResolved( await simulateFlightServerRender(ui.value), ); expect(final).toMatchInlineSnapshot(` <div> 4 </div> `); }); it('should support .append()', async () => { const ui = createStreamableUI(<div>1</div>); ui.update(<div>2</div>); ui.append(<div>3</div>); ui.append(<div>4</div>); ui.done(); const final = getFinalValueFromResolved( await simulateFlightServerRender(ui.value), ); expect(final).toMatchInlineSnapshot(` <React.Fragment> <React.Fragment> <div> 2 </div> <div> 3 </div> </React.Fragment> <div> 4 </div> </React.Fragment> `); }); it('should support streaming .append() result before .done()', async () => { const ui = createStreamableUI(<div>1</div>); ui.append(<div>2</div>); ui.append(<div>3</div>); const currentResolved = (ui.value as React.ReactElement).props.children .props.n; const tryResolve1 = await Promise.race([currentResolved, delay()]); expect(tryResolve1).toBeDefined(); const tryResolve2 = await Promise.race([tryResolve1.next, delay()]); expect(tryResolve2).toBeDefined(); expect(getFinalValueFromResolved(tryResolve2.value)).toMatchInlineSnapshot(` <div> 3 </div> `); ui.append(<div>4</div>); ui.done(); const final = getFinalValueFromResolved( await simulateFlightServerRender(ui.value), ); expect(final).toMatchInlineSnapshot(` <React.Fragment> <React.Fragment> <React.Fragment> <div> 1 </div> <div> 2 </div> </React.Fragment> <div> 3 </div> </React.Fragment> <div> 4 </div> </React.Fragment> `); }); it('should support updating the appended ui', async () => { const ui = createStreamableUI(<div>1</div>); ui.update(<div>2</div>); ui.append(<div>3</div>); ui.done(<div>4</div>); const final = getFinalValueFromResolved( await simulateFlightServerRender(ui.value), ); expect(final).toMatchInlineSnapshot(` <React.Fragment> <div> 2 </div> <div> 4 </div> </React.Fragment> `); }); it('should re-use the text node when appending strings', async () => { const ui = createStreamableUI('hello'); ui.append(' world'); ui.append('!'); ui.done(); const final = getFinalValueFromResolved( await simulateFlightServerRender(ui.value), ); expect(final).toMatchInlineSnapshot('"hello world!"'); }); it('should send minimal incremental diffs when appending strings', async () => { const ui = createStreamableUI('hello'); ui.append(' world'); ui.append(' and'); ui.append(' universe'); ui.done(); expect(await flightRender(ui.value)).toMatchInlineSnapshot(` "1:"$Sreact.suspense" 2:D{"name":"","env":"Server"} 0:["$","$1",null,{"fallback":"hello","children":"$L2"}] 3:D{"name":"","env":"Server"} 2:["hello",["$","$1",null,{"fallback":" world","children":"$L3"}]] 4:D{"name":"","env":"Server"} 3:[" world",["$","$1",null,{"fallback":" and","children":"$L4"}]] 5:D{"name":"","env":"Server"} 4:[" and",["$","$1",null,{"fallback":" universe","children":"$L5"}]] 5:" universe" " `); const final = getFinalValueFromResolved( await simulateFlightServerRender(ui.value), ); expect(final).toStrictEqual('hello world and universe'); }); it('should error when updating a closed streamable', async () => { const ui = createStreamableUI(<div>1</div>); ui.done(<div>2</div>); expect(() => { ui.update(<div>3</div>); }).toThrowErrorMatchingInlineSnapshot( '[Error: .update(): UI stream is already closed.]', ); }); it('should avoid sending data again if the same UI is passed', async () => { const node = <div>1</div>; const ui = createStreamableUI(node); ui.update(node); ui.update(node); ui.update(node); ui.update(node); ui.update(node); ui.update(node); ui.done(); expect(await flightRender(ui.value)).toMatchInlineSnapshot(` "1:"$Sreact.suspense" 2:D{"name":"","env":"Server"} 0:["$","$1",null,{"fallback":["$","div",null,{"children":"1"}],"children":"$L2"}] 4:{"children":"1"} 3:["$","div",null,"$4"] 2:"$3" " `); }); it('should return self', async () => { const ui = createStreamableUI(<div>1</div>) .update(<div>2</div>) .update(<div>3</div>) .done(<div>4</div>); expect(await flightRender(ui.value)).toMatchInlineSnapshot(` "1:"$Sreact.suspense" 2:D{"name":"","env":"Server"} 0:["$","$1",null,{"fallback":["$","div",null,{"children":"1"}],"children":"$L2"}] 3:D{"name":"","env":"Server"} 2:["$","$1",null,{"fallback":["$","div",null,{"children":"2"}],"children":"$L3"}] 4:D{"name":"","env":"Server"} 3:["$","$1",null,{"fallback":["$","div",null,{"children":"3"}],"children":"$L4"}] 4:["$","div",null,{"children":"4"}] " `); }); }); --- File: /ai/packages/rsc/src/streamable-ui/create-suspended-chunk.tsx --- import React, { Suspense } from 'react'; import { createResolvablePromise } from '../util/create-resolvable-promise'; // Recursive type for the chunk. type ChunkType = | { done: false; value: React.ReactNode; next: Promise<ChunkType>; append?: boolean; } | { done: true; value: React.ReactNode; }; // Use single letter names for the variables to reduce the size of the RSC payload. // `R` for `Row`, `c` for `current`, `n` for `next`. // Note: Array construction is needed to access the name R. const R = [ (async ({ c: current, n: next, }: { c: React.ReactNode; n: Promise<ChunkType>; }) => { const chunk = await next; if (chunk.done) { return chunk.value; } if (chunk.append) { return ( <> {current} <Suspense fallback={chunk.value}> <R c={chunk.value} n={chunk.next} /> </Suspense> </> ); } return ( <Suspense fallback={chunk.value}> <R c={chunk.value} n={chunk.next} /> </Suspense> ); }) as unknown as React.FC<{ c: React.ReactNode; n: Promise<ChunkType>; }>, ][0]; /** * Creates a suspended chunk for React Server Components. * * This function generates a suspenseful React component that can be dynamically updated. * It's useful for streaming updates to the client in a React Server Components context. * * @param {React.ReactNode} initialValue - The initial value to render while the promise is pending. * @returns {Object} An object containing: * - row: A React node that renders the suspenseful content. * - resolve: A function to resolve the promise with a new value. * - reject: A function to reject the promise with an error. */ export function createSuspendedChunk(initialValue: React.ReactNode): { row: React.ReactNode; resolve: (value: ChunkType) => void; reject: (error: unknown) => void; } { const { promise, resolve, reject } = createResolvablePromise<ChunkType>(); return { row: ( <Suspense fallback={initialValue}> <R c={initialValue} n={promise} /> </Suspense> ), resolve, reject, }; } --- File: /ai/packages/rsc/src/streamable-value/create-streamable-value.test.tsx --- import { delay } from '@ai-sdk/provider-utils'; import { convertArrayToReadableStream } from '@ai-sdk/provider-utils/test'; import { createStreamableValue } from './create-streamable-value'; import { STREAMABLE_VALUE_TYPE, StreamableValue } from './streamable-value'; async function getRawChunks(streamableValue: StreamableValue<any, any>) { const chunks = []; let currentValue = streamableValue; while (true) { const { next, ...otherFields } = currentValue; chunks.push(otherFields); if (!next) break; currentValue = await next; } return chunks; } it('should return latest value', async () => { const streamableValue = createStreamableValue(1).update(2).update(3).done(4); expect(streamableValue.value.curr).toStrictEqual(4); }); it('should be able to stream any JSON values', async () => { const streamable = createStreamableValue(); streamable.update({ v: 123 }); expect(streamable.value.curr).toStrictEqual({ v: 123 }); streamable.done(); }); it('should support .error()', async () => { const streamable = createStreamableValue(); streamable.error('This is an error'); expect(streamable.value).toStrictEqual({ error: 'This is an error', type: STREAMABLE_VALUE_TYPE, }); }); it('should directly emit the final value when reading .value', async () => { const streamable = createStreamableValue('1'); streamable.update('2'); streamable.update('3'); expect(streamable.value.curr).toStrictEqual('3'); streamable.done('4'); expect(streamable.value.curr).toStrictEqual('4'); }); it('should be able to append strings as patch', async () => { const streamable = createStreamableValue(); const value = streamable.value; streamable.update('hello'); streamable.update('hello world'); streamable.update('hello world!'); streamable.update('new string'); streamable.done('new string with patch!'); expect(await getRawChunks(value)).toStrictEqual([ { curr: undefined, type: STREAMABLE_VALUE_TYPE }, { curr: 'hello' }, { diff: [0, ' world'] }, { diff: [0, '!'] }, { curr: 'new string' }, { diff: [0, ' with patch!'] }, ]); }); it('should be able to call .append() to send patches', async () => { const streamable = createStreamableValue(); const value = streamable.value; streamable.append('hello'); streamable.append(' world'); streamable.append('!'); streamable.done(); expect(await getRawChunks(value)).toStrictEqual([ { curr: undefined, type: STREAMABLE_VALUE_TYPE }, { curr: 'hello' }, { diff: [0, ' world'] }, { diff: [0, '!'] }, {}, ]); }); it('should be able to mix .update() and .append() with optimized payloads', async () => { const streamable = createStreamableValue('hello'); const value = streamable.value; streamable.append(' world'); streamable.update('hello world!!'); streamable.update('some new'); streamable.update('some new string'); streamable.append(' with patch!'); streamable.done(); expect(await getRawChunks(value)).toStrictEqual([ { curr: 'hello', type: STREAMABLE_VALUE_TYPE }, { diff: [0, ' world'] }, { diff: [0, '!!'] }, { curr: 'some new' }, { diff: [0, ' string'] }, { diff: [0, ' with patch!'] }, {}, ]); }); it('should behave like .update() with .append() and .done()', async () => { const streamable = createStreamableValue('hello'); const value = streamable.value; streamable.append(' world'); streamable.done('fin'); expect(await getRawChunks(value)).toStrictEqual([ { curr: 'hello', type: STREAMABLE_VALUE_TYPE }, { diff: [0, ' world'] }, { curr: 'fin' }, ]); }); it('should be able to accept readableStream as the source', async () => { const streamable = createStreamableValue( convertArrayToReadableStream(['hello', ' world', '!']), ); const value = streamable.value; expect(await getRawChunks(value)).toStrictEqual([ { curr: undefined, type: STREAMABLE_VALUE_TYPE }, { curr: 'hello' }, { diff: [0, ' world'] }, { diff: [0, '!'] }, {}, ]); }); it('should accept readableStream with JSON payloads', async () => { const streamable = createStreamableValue( convertArrayToReadableStream([{ v: 1 }, { v: 2 }, { v: 3 }]), ); const value = streamable.value; expect(await getRawChunks(value)).toStrictEqual([ { curr: undefined, type: STREAMABLE_VALUE_TYPE }, { curr: { v: 1 } }, { curr: { v: 2 } }, { curr: { v: 3 } }, {}, ]); }); it('should lock the streamable if from readableStream', async () => { const streamable = createStreamableValue( new ReadableStream({ async start(controller) { await delay(); controller.enqueue('hello'); controller.close(); }, }), ); expect(() => streamable.update('world')).toThrowErrorMatchingInlineSnapshot( '[Error: .update(): Value stream is locked and cannot be updated.]', ); }); --- File: /ai/packages/rsc/src/streamable-value/create-streamable-value.ts --- import { HANGING_STREAM_WARNING_TIME_MS } from '../util/constants'; import { createResolvablePromise } from '../util/create-resolvable-promise'; import { STREAMABLE_VALUE_TYPE, StreamablePatch, StreamableValue, } from './streamable-value'; const STREAMABLE_VALUE_INTERNAL_LOCK = Symbol('streamable.value.lock'); /** * Create a wrapped, changeable value that can be streamed to the client. * On the client side, the value can be accessed via the readStreamableValue() API. */ function createStreamableValue<T = any, E = any>( initialValue?: T | ReadableStream<T>, ) { const isReadableStream = initialValue instanceof ReadableStream || (typeof initialValue === 'object' && initialValue !== null && 'getReader' in initialValue && typeof initialValue.getReader === 'function' && 'locked' in initialValue && typeof initialValue.locked === 'boolean'); if (!isReadableStream) { return createStreamableValueImpl<T, E>(initialValue); } const streamableValue = createStreamableValueImpl<T, E>(); // Since the streamable value will be from a readable stream, it's not allowed // to update the value manually as that introduces race conditions and // unexpected behavior. // We lock the value to prevent any updates from the user. streamableValue[STREAMABLE_VALUE_INTERNAL_LOCK] = true; (async () => { try { // Consume the readable stream and update the value. const reader = initialValue.getReader(); while (true) { const { value, done } = await reader.read(); if (done) { break; } // Unlock the value to allow updates. streamableValue[STREAMABLE_VALUE_INTERNAL_LOCK] = false; if (typeof value === 'string') { streamableValue.append(value); } else { streamableValue.update(value); } // Lock the value again. streamableValue[STREAMABLE_VALUE_INTERNAL_LOCK] = true; } streamableValue[STREAMABLE_VALUE_INTERNAL_LOCK] = false; streamableValue.done(); } catch (e) { streamableValue[STREAMABLE_VALUE_INTERNAL_LOCK] = false; streamableValue.error(e); } })(); return streamableValue; } // It's necessary to define the type manually here, otherwise TypeScript compiler // will not be able to infer the correct return type as it's circular. type StreamableValueWrapper<T, E> = { /** * The value of the streamable. This can be returned from a Server Action and * received by the client. To read the streamed values, use the * `readStreamableValue` or `useStreamableValue` APIs. */ readonly value: StreamableValue<T, E>; /** * This method updates the current value with a new one. */ update(value: T): StreamableValueWrapper<T, E>; /** * This method is used to append a delta string to the current value. It * requires the current value of the streamable to be a string. * * @example * ```jsx * const streamable = createStreamableValue('hello'); * streamable.append(' world'); * * // The value will be 'hello world' * ``` */ append(value: T): StreamableValueWrapper<T, E>; /** * This method is used to signal that there is an error in the value stream. * It will be thrown on the client side when consumed via * `readStreamableValue` or `useStreamableValue`. */ error(error: any): StreamableValueWrapper<T, E>; /** * This method marks the value as finalized. You can either call it without * any parameters or with a new value as the final state. * Once called, the value cannot be updated or appended anymore. * * This method is always **required** to be called, otherwise the response * will be stuck in a loading state. */ done(...args: [T] | []): StreamableValueWrapper<T, E>; /** * @internal This is an internal lock to prevent the value from being * updated by the user. */ [STREAMABLE_VALUE_INTERNAL_LOCK]: boolean; }; function createStreamableValueImpl<T = any, E = any>(initialValue?: T) { let closed = false; let locked = false; let resolvable = createResolvablePromise<StreamableValue<T, E>>(); let currentValue = initialValue; let currentError: E | undefined; let currentPromise: typeof resolvable.promise | undefined = resolvable.promise; let currentPatchValue: StreamablePatch; function assertStream(method: string) { if (closed) { throw new Error(method + ': Value stream is already closed.'); } if (locked) { throw new Error( method + ': Value stream is locked and cannot be updated.', ); } } let warningTimeout: NodeJS.Timeout | undefined; function warnUnclosedStream() { if (process.env.NODE_ENV === 'development') { if (warningTimeout) { clearTimeout(warningTimeout); } warningTimeout = setTimeout(() => { console.warn( 'The streamable value has been slow to update. This may be a bug or a performance issue or you forgot to call `.done()`.', ); }, HANGING_STREAM_WARNING_TIME_MS); } } warnUnclosedStream(); function createWrapped(initialChunk?: boolean): StreamableValue<T, E> { // This makes the payload much smaller if there're mutative updates before the first read. let init: Partial<StreamableValue<T, E>>; if (currentError !== undefined) { init = { error: currentError }; } else { if (currentPatchValue && !initialChunk) { init = { diff: currentPatchValue }; } else { init = { curr: currentValue }; } } if (currentPromise) { init.next = currentPromise; } if (initialChunk) { init.type = STREAMABLE_VALUE_TYPE; } return init; } // Update the internal `currentValue` and `currentPatchValue` if needed. function updateValueStates(value: T) { // If we can only send a patch over the wire, it's better to do so. currentPatchValue = undefined; if (typeof value === 'string') { if (typeof currentValue === 'string') { if (value.startsWith(currentValue)) { currentPatchValue = [0, value.slice(currentValue.length)]; } } } currentValue = value; } const streamable: StreamableValueWrapper<T, E> = { set [STREAMABLE_VALUE_INTERNAL_LOCK](state: boolean) { locked = state; }, get value() { return createWrapped(true); }, update(value: T) { assertStream('.update()'); const resolvePrevious = resolvable.resolve; resolvable = createResolvablePromise(); updateValueStates(value); currentPromise = resolvable.promise; resolvePrevious(createWrapped()); warnUnclosedStream(); return streamable; }, append(value: T) { assertStream('.append()'); if ( typeof currentValue !== 'string' && typeof currentValue !== 'undefined' ) { throw new Error( `.append(): The current value is not a string. Received: ${typeof currentValue}`, ); } if (typeof value !== 'string') { throw new Error( `.append(): The value is not a string. Received: ${typeof value}`, ); } const resolvePrevious = resolvable.resolve; resolvable = createResolvablePromise(); if (typeof currentValue === 'string') { currentPatchValue = [0, value]; (currentValue as string) = currentValue + value; } else { currentPatchValue = undefined; currentValue = value; } currentPromise = resolvable.promise; resolvePrevious(createWrapped()); warnUnclosedStream(); return streamable; }, error(error: any) { assertStream('.error()'); if (warningTimeout) { clearTimeout(warningTimeout); } closed = true; currentError = error; currentPromise = undefined; resolvable.resolve({ error }); return streamable; }, done(...args: [] | [T]) { assertStream('.done()'); if (warningTimeout) { clearTimeout(warningTimeout); } closed = true; currentPromise = undefined; if (args.length) { updateValueStates(args[0]); resolvable.resolve(createWrapped()); return streamable; } resolvable.resolve({}); return streamable; }, }; return streamable; } export { createStreamableValue }; --- File: /ai/packages/rsc/src/streamable-value/is-streamable-value.ts --- import { STREAMABLE_VALUE_TYPE, StreamableValue } from './streamable-value'; export function isStreamableValue(value: unknown): value is StreamableValue { return ( value != null && typeof value === 'object' && 'type' in value && value.type === STREAMABLE_VALUE_TYPE ); } --- File: /ai/packages/rsc/src/streamable-value/read-streamable-value.tsx --- import { isStreamableValue } from './is-streamable-value'; import { StreamableValue } from './streamable-value'; /** * `readStreamableValue` takes a streamable value created via the `createStreamableValue().value` API, * and returns an async iterator. * * ```js * // Inside your AI action: * * async function action() { * 'use server' * const streamable = createStreamableValue(); * * streamable.update(1); * streamable.update(2); * streamable.done(3); * // ... * return streamable.value; * } * ``` * * And to read the value: * * ```js * const streamableValue = await action() * for await (const v of readStreamableValue(streamableValue)) { * console.log(v) * } * ``` * * This logs out 1, 2, 3 on console. */ export function readStreamableValue<T = unknown>( streamableValue: StreamableValue<T>, ): AsyncIterable<T | undefined> { if (!isStreamableValue(streamableValue)) { throw new Error( 'Invalid value: this hook only accepts values created via `createStreamableValue`.', ); } return { [Symbol.asyncIterator]() { let row: StreamableValue<T> | Promise<StreamableValue<T>> = streamableValue; let value = row.curr; // the current value let isDone = false; let isFirstIteration = true; return { async next() { // the iteration is done already, return the last value: if (isDone) return { value, done: true }; // resolve the promise at the beginning of each iteration: row = await row; // throw error if any: if (row.error !== undefined) { throw row.error; } // if there is a value or a patch, use it: if ('curr' in row || row.diff) { if (row.diff) { // streamable patch (text only): if (row.diff[0] === 0) { if (typeof value !== 'string') { throw new Error( 'Invalid patch: can only append to string types. This is a bug in the AI SDK.', ); } // casting required to remove T & string limitation (value as string) = value + row.diff[1]; } } else { // replace the value (full new value) value = row.curr; } // The last emitted { done: true } won't be used as the value // by the for await...of syntax. if (!row.next) { isDone = true; return { value, done: false }; } } // there are no further rows to iterate over: if (row.next === undefined) { return { value, done: true }; } row = row.next; if (isFirstIteration) { isFirstIteration = false; // TODO should this be set for every return? if (value === undefined) { // This is the initial chunk and there isn't an initial value yet. // Let's skip this one. return this.next(); } } return { value, done: false }; }, }; }, }; } --- File: /ai/packages/rsc/src/streamable-value/read-streamable-value.ui.test.tsx --- import { delay } from '@ai-sdk/provider-utils'; import { createStreamableValue } from './create-streamable-value'; import { readStreamableValue } from './read-streamable-value'; it('should return an async iterable', () => { const streamable = createStreamableValue(); const result = readStreamableValue(streamable.value); streamable.done(); expect(result).toBeDefined(); expect(result[Symbol.asyncIterator]).toBeDefined(); }); it('should support reading streamed values and errors', async () => { const streamable = createStreamableValue(1); (async () => { await delay(); streamable.update(2); await delay(); streamable.update(3); await delay(); streamable.error('This is an error'); })(); const values = []; try { for await (const v of readStreamableValue(streamable.value)) { values.push(v); } expect.fail('should not be reached'); } catch (e) { expect(e).toStrictEqual('This is an error'); } expect(values).toStrictEqual([1, 2, 3]); }); it('should be able to read values asynchronously with different value types', async () => { const streamable = createStreamableValue({}); (async () => { await delay(); streamable.update([1]); streamable.update(['2']); streamable.done({ 3: 3 }); })(); const values = []; for await (const v of readStreamableValue(streamable.value)) { values.push(v); } expect(values).toStrictEqual([{}, [1], ['2'], { '3': 3 }]); }); it('should be able to replay errors', async () => { const streamable = createStreamableValue(0); (async () => { await delay(); streamable.update(1); streamable.update(2); streamable.error({ customErrorMessage: 'this is an error' }); })(); const values = []; try { for await (const v of readStreamableValue(streamable.value)) { values.push(v); } expect.fail('should not be reached'); } catch (e) { expect(e).toStrictEqual({ customErrorMessage: 'this is an error', }); } expect(values).toStrictEqual([0, 1, 2]); }); it('should be able to append strings as patch', async () => { const streamable = createStreamableValue(); const value = streamable.value; streamable.update('hello'); streamable.update('hello world'); streamable.update('hello world!'); streamable.update('new string'); streamable.done('new string with patch!'); const values = []; for await (const v of readStreamableValue(value)) { values.push(v); } expect(values).toStrictEqual([ 'hello', 'hello world', 'hello world!', 'new string', 'new string with patch!', ]); }); it('should be able to call .append() to send patches', async () => { const streamable = createStreamableValue(); const value = streamable.value; streamable.append('hello'); streamable.append(' world'); streamable.append('!'); streamable.done(); const values = []; for await (const v of readStreamableValue(value)) { values.push(v); } expect(values).toStrictEqual(['hello', 'hello world', 'hello world!']); }); it('should be able to mix .update() and .append() with optimized payloads', async () => { const streamable = createStreamableValue('hello'); const value = streamable.value; streamable.append(' world'); streamable.update('hello world!!'); streamable.update('some new'); streamable.update('some new string'); streamable.append(' with patch!'); streamable.done(); const values = []; for await (const v of readStreamableValue(value)) { values.push(v); } expect(values).toStrictEqual([ 'hello', 'hello world', 'hello world!!', 'some new', 'some new string', 'some new string with patch!', ]); }); it('should behave like .update() with .append() and .done()', async () => { const streamable = createStreamableValue('hello'); const value = streamable.value; streamable.append(' world'); streamable.done('fin'); const values = []; for await (const v of readStreamableValue(value)) { values.push(v); } expect(values).toStrictEqual(['hello', 'hello world', 'fin']); }); --- File: /ai/packages/rsc/src/streamable-value/streamable-value.ts --- export const STREAMABLE_VALUE_TYPE = Symbol.for('ui.streamable.value'); export type StreamablePatch = undefined | [0, string]; // Append string. declare const __internal_curr: unique symbol; declare const __internal_error: unique symbol; /** * StreamableValue is a value that can be streamed over the network via AI Actions. * To read the streamed values, use the `readStreamableValue` or `useStreamableValue` APIs. */ export type StreamableValue<T = any, E = any> = { /** * @internal Use `readStreamableValue` to read the values. */ type?: typeof STREAMABLE_VALUE_TYPE; /** * @internal Use `readStreamableValue` to read the values. */ curr?: T; /** * @internal Use `readStreamableValue` to read the values. */ error?: E; /** * @internal Use `readStreamableValue` to read the values. */ diff?: StreamablePatch; /** * @internal Use `readStreamableValue` to read the values. */ next?: Promise<StreamableValue<T, E>>; // branded types to maintain type signature after internal properties are stripped. [__internal_curr]?: T; [__internal_error]?: E; }; --- File: /ai/packages/rsc/src/streamable-value/use-streamable-value.tsx --- import { startTransition, useLayoutEffect, useState } from 'react'; import { readStreamableValue } from './read-streamable-value'; import { StreamableValue } from './streamable-value'; import { isStreamableValue } from './is-streamable-value'; function checkStreamableValue(value: unknown): value is StreamableValue { const hasSignature = isStreamableValue(value); if (!hasSignature && value !== undefined) { throw new Error( 'Invalid value: this hook only accepts values created via `createStreamableValue`.', ); } return hasSignature; } /** * `useStreamableValue` is a React hook that takes a streamable value created via the `createStreamableValue().value` API, * and returns the current value, error, and pending state. * * This is useful for consuming streamable values received from a component's props. For example: * * ```js * function MyComponent({ streamableValue }) { * const [data, error, pending] = useStreamableValue(streamableValue); * * if (pending) return <div>Loading...</div>; * if (error) return <div>Error: {error.message}</div>; * * return <div>Data: {data}</div>; * } * ``` */ export function useStreamableValue<T = unknown, Error = unknown>( streamableValue?: StreamableValue<T>, ): [data: T | undefined, error: Error | undefined, pending: boolean] { const [curr, setCurr] = useState<T | undefined>( checkStreamableValue(streamableValue) ? streamableValue.curr : undefined, ); const [error, setError] = useState<Error | undefined>( checkStreamableValue(streamableValue) ? streamableValue.error : undefined, ); const [pending, setPending] = useState<boolean>( checkStreamableValue(streamableValue) ? !!streamableValue.next : false, ); useLayoutEffect(() => { if (!checkStreamableValue(streamableValue)) return; let cancelled = false; const iterator = readStreamableValue(streamableValue); if (streamableValue.next) { startTransition(() => { if (cancelled) return; setPending(true); }); } (async () => { try { for await (const value of iterator) { if (cancelled) return; startTransition(() => { if (cancelled) return; setCurr(value); }); } } catch (e) { if (cancelled) return; startTransition(() => { if (cancelled) return; setError(e as Error); }); } finally { if (cancelled) return; startTransition(() => { if (cancelled) return; setPending(false); }); } })(); return () => { cancelled = true; }; }, [streamableValue]); return [curr, error, pending]; } --- File: /ai/packages/rsc/src/types/index.ts --- export type * from '../index'; --- File: /ai/packages/rsc/src/util/constants.ts --- /** * Warning time for notifying developers that a stream is hanging in dev mode * using a console.warn. */ export const HANGING_STREAM_WARNING_TIME_MS = 15 * 1000; --- File: /ai/packages/rsc/src/util/create-resolvable-promise.ts --- /** * Creates a Promise with externally accessible resolve and reject functions. * * @template T - The type of the value that the Promise will resolve to. * @returns An object containing: * - promise: A Promise that can be resolved or rejected externally. * - resolve: A function to resolve the Promise with a value of type T. * - reject: A function to reject the Promise with an error. */ export function createResolvablePromise<T = any>(): { promise: Promise<T>; resolve: (value: T) => void; reject: (error: unknown) => void; } { let resolve: (value: T) => void; let reject: (error: unknown) => void; const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve: resolve!, reject: reject!, }; } --- File: /ai/packages/rsc/src/util/is-async-generator.ts --- export function isAsyncGenerator<T, TReturn, TNext>( value: unknown, ): value is AsyncGenerator<T, TReturn, TNext> { return ( value != null && typeof value === 'object' && Symbol.asyncIterator in value ); } --- File: /ai/packages/rsc/src/util/is-function.ts --- /** * Checks if the given value is a function. * * @param {unknown} value - The value to check. * @returns {boolean} True if the value is a function, false otherwise. */ export const isFunction = (value: unknown): value is Function => typeof value === 'function'; --- File: /ai/packages/rsc/src/util/is-generator.ts --- export function isGenerator<T, TReturn, TNext>( value: unknown, ): value is Generator<T, TReturn, TNext> { return value != null && typeof value === 'object' && Symbol.iterator in value; } --- File: /ai/packages/rsc/src/ai-state.test.ts --- import { describe, it, expect, vi, beforeEach } from 'vitest'; import { withAIState, getAIState, getMutableAIState, sealMutableAIState, getAIStateDeltaPromise, } from './ai-state'; describe('AI State Management', () => { beforeEach(() => { vi.resetAllMocks(); }); it('should get the current AI state', () => { const initialState = { foo: 'bar' }; const result = withAIState({ state: initialState, options: {} }, () => { return getAIState(); }); expect(result).toEqual(initialState); }); it('should get a specific key from the AI state', () => { const initialState = { foo: 'bar', baz: 'qux' }; const result = withAIState({ state: initialState, options: {} }, () => { return getAIState('foo'); }); expect(result).toBe('bar'); }); it('should update the AI state', () => { const initialState = { foo: 'bar' }; withAIState({ state: initialState, options: {} }, () => { const mutableState = getMutableAIState(); mutableState.update({ foo: 'baz' }); expect(getAIState()).toEqual({ foo: 'baz' }); }); }); it('should update a specific key in the AI state', () => { const initialState = { foo: 'bar', baz: 'qux' }; withAIState({ state: initialState, options: {} }, () => { const mutableState = getMutableAIState('foo'); mutableState.update('newValue'); expect(getAIState()).toEqual({ foo: 'newValue', baz: 'qux' }); }); }); it('should throw an error when accessing AI state outside of withAIState', () => { expect(() => getAIState()).toThrow( '`getAIState` must be called within an AI Action.', ); }); it('should throw an error when updating AI state after it has been sealed', () => { withAIState({ state: {}, options: {} }, () => { sealMutableAIState(); expect(() => getMutableAIState()).toThrow( '`getMutableAIState` must be called before returning from an AI Action.', ); }); }); it('should call onSetAIState when updating state', () => { const onSetAIState = vi.fn(); const initialState = { foo: 'bar' }; withAIState({ state: initialState, options: { onSetAIState } }, () => { const mutableState = getMutableAIState(); mutableState.update({ foo: 'baz' }); mutableState.done({ foo: 'baz' }); }); expect(onSetAIState).toHaveBeenCalledWith( expect.objectContaining({ state: { foo: 'baz' }, done: true, }), ); }); it('should handle updates with and without key', async () => { type Message = { role: string; content: string }; type AIState = { chatId: string; messages: Array<Message>; }; const initialState: AIState = { chatId: '123', messages: [], }; await withAIState({ state: initialState, options: {} }, async () => { // Test with getMutableState() const stateWithoutKey = getMutableAIState(); stateWithoutKey.update((current: AIState) => ({ ...current, messages: [...current.messages, { role: 'user', content: 'Hello!' }], })); stateWithoutKey.done((current: AIState) => ({ ...current, messages: [ ...current.messages, { role: 'assistant', content: 'Hello! How can I assist you today?' }, ], })); const deltaWithoutKey = await getAIStateDeltaPromise(); expect(deltaWithoutKey).toBeDefined(); expect(getAIState()).toEqual({ chatId: '123', messages: [ { role: 'user', content: 'Hello!' }, { role: 'assistant', content: 'Hello! How can I assist you today?' }, ], }); }); await withAIState({ state: initialState, options: {} }, async () => { // Test with getMutableState('messages') const stateWithKey = getMutableAIState('messages'); stateWithKey.update((current: Array<Message>) => [ ...current, { role: 'user', content: 'Hello!' }, ]); stateWithKey.done((current: Array<Message>) => [ ...current, { role: 'assistant', content: 'Hello! How can I assist you today?' }, ]); const deltaWithKey = await getAIStateDeltaPromise(); expect(deltaWithKey).toBeDefined(); expect(getAIState()).toEqual({ chatId: '123', messages: [ { role: 'user', content: 'Hello!' }, { role: 'assistant', content: 'Hello! How can I assist you today?' }, ], }); }); }); }); --- File: /ai/packages/rsc/src/ai-state.tsx --- import * as jsondiffpatch from 'jsondiffpatch'; import { AsyncLocalStorage } from 'node:async_hooks'; import { createResolvablePromise } from './util/create-resolvable-promise'; import { isFunction } from './util/is-function'; import type { AIProvider, InferAIState, InternalAIStateStorageOptions, MutableAIState, ValueOrUpdater, } from './types'; // It is possible that multiple AI requests get in concurrently, for different // AI instances. So ALS is necessary here for a simpler API. const asyncAIStateStorage = new AsyncLocalStorage<{ currentState: any; originalState: any; sealed: boolean; options: InternalAIStateStorageOptions; mutationDeltaPromise?: Promise<any>; mutationDeltaResolve?: (v: any) => void; }>(); function getAIStateStoreOrThrow(message: string) { const store = asyncAIStateStorage.getStore(); if (!store) { throw new Error(message); } return store; } export function withAIState<S, T>( { state, options }: { state: S; options: InternalAIStateStorageOptions }, fn: () => T, ): T { return asyncAIStateStorage.run( { currentState: JSON.parse(JSON.stringify(state)), // deep clone object originalState: state, sealed: false, options, }, fn, ); } export function getAIStateDeltaPromise() { const store = getAIStateStoreOrThrow('Internal error occurred.'); return store.mutationDeltaPromise; } // Internal method. This will be called after the AI Action has been returned // and you can no longer call `getMutableAIState()` inside any async callbacks // created by that Action. export function sealMutableAIState() { const store = getAIStateStoreOrThrow('Internal error occurred.'); store.sealed = true; } /** * Get the current AI state. * If `key` is provided, it will return the value of the specified key in the * AI state, if it's an object. If it's not an object, it will throw an error. * * @example const state = getAIState() // Get the entire AI state * @example const field = getAIState('key') // Get the value of the key */ function getAIState<AI extends AIProvider = any>(): Readonly< InferAIState<AI, any> >; function getAIState<AI extends AIProvider = any>( key: keyof InferAIState<AI, any>, ): Readonly<InferAIState<AI, any>[typeof key]>; function getAIState<AI extends AIProvider = any>( ...args: [] | [key: keyof InferAIState<AI, any>] ) { const store = getAIStateStoreOrThrow( '`getAIState` must be called within an AI Action.', ); if (args.length > 0) { const key = args[0]; if (typeof store.currentState !== 'object') { throw new Error( `You can't get the "${String( key, )}" field from the AI state because it's not an object.`, ); } return store.currentState[key as keyof typeof store.currentState]; } return store.currentState; } /** * Get the mutable AI state. Note that you must call `.done()` when finishing * updating the AI state. * * @example * ```tsx * const state = getMutableAIState() * state.update({ ...state.get(), key: 'value' }) * state.update((currentState) => ({ ...currentState, key: 'value' })) * state.done() * ``` * * @example * ```tsx * const state = getMutableAIState() * state.done({ ...state.get(), key: 'value' }) // Done with a new state * ``` */ function getMutableAIState<AI extends AIProvider = any>(): MutableAIState< InferAIState<AI, any> >; function getMutableAIState<AI extends AIProvider = any>( key: keyof InferAIState<AI, any>, ): MutableAIState<InferAIState<AI, any>[typeof key]>; function getMutableAIState<AI extends AIProvider = any>( ...args: [] | [key: keyof InferAIState<AI, any>] ) { type AIState = InferAIState<AI, any>; type AIStateWithKey = typeof args extends [key: keyof AIState] ? AIState[(typeof args)[0]] : AIState; type NewStateOrUpdater = ValueOrUpdater<AIStateWithKey>; const store = getAIStateStoreOrThrow( '`getMutableAIState` must be called within an AI Action.', ); if (store.sealed) { throw new Error( "`getMutableAIState` must be called before returning from an AI Action. Please move it to the top level of the Action's function body.", ); } if (!store.mutationDeltaPromise) { const { promise, resolve } = createResolvablePromise(); store.mutationDeltaPromise = promise; store.mutationDeltaResolve = resolve; } function doUpdate(newState: NewStateOrUpdater, done: boolean) { if (args.length > 0) { if (typeof store.currentState !== 'object') { const key = args[0]; throw new Error( `You can't modify the "${String( key, )}" field of the AI state because it's not an object.`, ); } } if (isFunction(newState)) { if (args.length > 0) { store.currentState[args[0]] = newState(store.currentState[args[0]]); } else { store.currentState = newState(store.currentState); } } else { if (args.length > 0) { store.currentState[args[0]] = newState; } else { store.currentState = newState; } } store.options.onSetAIState?.({ key: args.length > 0 ? args[0] : undefined, state: store.currentState, done, }); } const mutableState = { get: () => { if (args.length > 0) { const key = args[0]; if (typeof store.currentState !== 'object') { throw new Error( `You can't get the "${String( key, )}" field from the AI state because it's not an object.`, ); } return store.currentState[key] as Readonly<AIStateWithKey>; } return store.currentState as Readonly<AIState>; }, update: function update(newAIState: NewStateOrUpdater) { doUpdate(newAIState, false); }, done: function done(...doneArgs: [] | [NewStateOrUpdater]) { if (doneArgs.length > 0) { doUpdate(doneArgs[0] as NewStateOrUpdater, true); } const delta = jsondiffpatch.diff(store.originalState, store.currentState); store.mutationDeltaResolve!(delta); }, }; return mutableState; } export { getAIState, getMutableAIState }; --- File: /ai/packages/rsc/src/index.ts --- export { getAIState, getMutableAIState, createStreamableUI, createStreamableValue, streamUI, createAI, } from './rsc-server'; export { readStreamableValue, useStreamableValue, useUIState, useAIState, useActions, useSyncUIState, } from './rsc-client'; export type { StreamableValue } from './streamable-value/streamable-value'; export * from './types'; --- File: /ai/packages/rsc/src/provider.tsx --- // This file provides the AI context to all AI Actions via AsyncLocalStorage. import * as React from 'react'; import { InternalAIProvider } from './rsc-shared.mjs'; import { withAIState, getAIStateDeltaPromise, sealMutableAIState, } from './ai-state'; import type { ServerWrappedActions, AIAction, AIActions, AIProvider, InternalAIStateStorageOptions, OnSetAIState, OnGetUIState, } from './types'; async function innerAction<T>( { action, options, }: { action: AIAction; options: InternalAIStateStorageOptions }, state: T, ...args: unknown[] ) { 'use server'; return await withAIState( { state, options, }, async () => { const result = await action(...args); sealMutableAIState(); return [getAIStateDeltaPromise() as Promise<T>, result]; }, ); } function wrapAction<T = unknown>( action: AIAction, options: InternalAIStateStorageOptions, ) { return innerAction.bind(null, { action, options }) as AIAction<T>; } export function createAI< AIState = any, UIState = any, Actions extends AIActions = {}, >({ actions, initialAIState, initialUIState, onSetAIState, onGetUIState, }: { actions: Actions; initialAIState?: AIState; initialUIState?: UIState; /** * This function is called whenever the AI state is updated by an Action. * You can use this to persist the AI state to a database, or to send it to a * logging service. */ onSetAIState?: OnSetAIState<AIState>; /** * This function is used to retrieve the UI state based on the AI state. * For example, to render the initial UI state based on a given AI state, or * to sync the UI state when the application is already loaded. * * If returning `undefined`, the client side UI state will not be updated. * * This function must be annotated with the `"use server"` directive. * * @example * ```tsx * onGetUIState: async () => { * 'use server'; * * const currentAIState = getAIState(); * const externalAIState = await loadAIStateFromDatabase(); * * if (currentAIState === externalAIState) return undefined; * * // Update current AI state and return the new UI state * const state = getMutableAIState() * state.done(externalAIState) * * return <div>...</div>; * } * ``` */ onGetUIState?: OnGetUIState<UIState>; }) { // Wrap all actions with our HoC. const wrappedActions: ServerWrappedActions = {}; for (const name in actions) { wrappedActions[name] = wrapAction(actions[name], { onSetAIState, }); } const wrappedSyncUIState = onGetUIState ? wrapAction(onGetUIState, {}) : undefined; const AI: AIProvider<AIState, UIState, Actions> = async props => { if ('useState' in React) { // This file must be running on the React Server layer. // Ideally we should be using `import "server-only"` here but we can have a // more customized error message with this implementation. throw new Error( 'This component can only be used inside Server Components.', ); } let uiState = props.initialUIState ?? initialUIState; let aiState = props.initialAIState ?? initialAIState; let aiStateDelta = undefined; if (wrappedSyncUIState) { const [newAIStateDelta, newUIState] = await wrappedSyncUIState(aiState); if (newUIState !== undefined) { aiStateDelta = newAIStateDelta; uiState = newUIState; } } return ( <InternalAIProvider wrappedActions={wrappedActions} wrappedSyncUIState={wrappedSyncUIState} initialUIState={uiState} initialAIState={aiState} initialAIStatePatch={aiStateDelta} > {props.children} </InternalAIProvider> ); }; return AI; } --- File: /ai/packages/rsc/src/rsc-client.ts --- export { readStreamableValue, useStreamableValue, useUIState, useAIState, useActions, useSyncUIState, } from './rsc-shared.mjs'; --- File: /ai/packages/rsc/src/rsc-server.ts --- export { getAIState, getMutableAIState } from './ai-state'; export { createAI } from './provider'; export { streamUI } from './stream-ui'; export { createStreamableUI } from './streamable-ui/create-streamable-ui'; export { createStreamableValue } from './streamable-value/create-streamable-value'; --- File: /ai/packages/rsc/src/types.test-d.ts --- import { expectTypeOf } from 'vitest'; import type { StreamableValue } from '.'; describe('StreamableValue type', () => { it('should yield a type error when assigning a wrong value', () => { expectTypeOf<StreamableValue<string>>().not.toEqualTypeOf< StreamableValue<boolean> >(); expectTypeOf<StreamableValue<string>>().not.toEqualTypeOf<string>(); expectTypeOf< StreamableValue<string> >().not.toEqualTypeOf<'THIS IS NOT A STREAMABLE VALUE'>(); }); }); --- File: /ai/packages/rsc/src/types.ts --- export type JSONValue = string | number | boolean | JSONObject | JSONArray; interface JSONObject { [x: string]: JSONValue; } interface JSONArray extends Array<JSONValue> {} export type AIAction<T = any, R = any> = (...args: T[]) => Promise<R>; export type AIActions<T = any, R = any> = Record<string, AIAction<T, R>>; export type ServerWrappedAction<T = unknown> = ( aiState: T, ...args: unknown[] ) => Promise<[Promise<T>, unknown]>; export type ServerWrappedActions<T = unknown> = Record< string, ServerWrappedAction<T> >; export type InternalAIProviderProps<AIState = any, UIState = any> = { children: React.ReactNode; initialUIState: UIState; initialAIState: AIState; initialAIStatePatch: undefined | Promise<AIState>; wrappedActions: ServerWrappedActions<AIState>; wrappedSyncUIState?: ServerWrappedAction<AIState>; }; export type AIProviderProps<AIState = any, UIState = any, Actions = any> = { children: React.ReactNode; initialAIState?: AIState; initialUIState?: UIState; /** $ActionTypes is only added for type inference and is never used at runtime **/ $ActionTypes?: Actions; }; export type AIProvider<AIState = any, UIState = any, Actions = any> = ( props: AIProviderProps<AIState, UIState, Actions>, ) => Promise<React.ReactElement>; export type InferAIState<T, Fallback> = T extends AIProvider<infer AIState, any, any> ? AIState : Fallback; export type InferUIState<T, Fallback> = T extends AIProvider<any, infer UIState, any> ? UIState : Fallback; export type InferActions<T, Fallback> = T extends AIProvider<any, any, infer Actions> ? Actions : Fallback; export type InternalAIStateStorageOptions = { onSetAIState?: OnSetAIState<any>; }; export type OnSetAIState<S> = ({ key, state, done, }: { key: string | number | symbol | undefined; state: S; done: boolean; }) => void | Promise<void>; export type OnGetUIState<S> = AIAction<void, S | undefined>; export type ValueOrUpdater<T> = T | ((current: T) => T); export type MutableAIState<AIState> = { get: () => AIState; update: (newState: ValueOrUpdater<AIState>) => void; done: ((newState: AIState) => void) | (() => void); }; --- File: /ai/packages/rsc/tests/e2e/next-server/app/rsc/actions.jsx --- 'use server'; import { createStreamableUI, createStreamableValue } from '@ai-sdk/rsc'; import { ClientInfo } from './client-utils'; function sleep(ms = 0) { return new Promise(resolve => setTimeout(resolve, ms)); } export async function streamableUI() { const streamable = createStreamableUI(); (async () => { await sleep(); streamable.update( <ClientInfo> <p>I am a paragraph</p> </ClientInfo>, ); await sleep(); streamable.update( <ClientInfo> <button>I am a button</button> </ClientInfo>, ); await sleep(); streamable.done(); })(); return streamable.value; } export async function streamableValue() { const streamable = createStreamableValue('hello'); (async () => { await sleep(); streamable.append(', world'); await sleep(); streamable.append('!'); await sleep(); streamable.update({ value: 'I am a JSON' }); await sleep(); streamable.done(['Finished']); })(); return streamable.value; } --- File: /ai/packages/rsc/tests/e2e/next-server/app/rsc/client-utils.js --- 'use client'; export function ClientInfo({ children }) { return <div>{children}</div>; } --- File: /ai/packages/rsc/tests/e2e/next-server/app/rsc/client.js --- 'use client'; import { useState } from 'react'; import { readStreamableValue } from '@ai-sdk/rsc'; export function Client({ actions }) { const [log, setLog] = useState(''); // Test `createStreamableValue` and `readStreamableValue` APIs async function testStreamableValue() { const value = await actions.streamableValue(); const values = []; for await (const val of readStreamableValue(value)) { values.push(val); setLog(JSON.stringify(values)); } } // Test `createStreamableUI` API async function testStreamableUI() { const value = await actions.streamableUI(); setLog(value); } return ( <div> <pre id="log" style={{ border: '1px solid #ccc', padding: 5 }}> {log} </pre> {/* Test suites */} <div style={{ display: 'inline-flex', flexDirection: 'column', gap: 5 }}> <button id="test-streamable-value" onClick={testStreamableValue}> Test Streamable Value </button> <button id="test-streamable-ui" onClick={testStreamableUI}> Test Streamable UI </button> </div> </div> ); } --- File: /ai/packages/rsc/tests/e2e/next-server/app/rsc/page.js --- import { streamableUI, streamableValue } from './actions'; import { Client } from './client'; export default function Page() { return ( <Client actions={{ streamableUI, streamableValue, }} /> ); } --- File: /ai/packages/rsc/tests/e2e/next-server/app/layout.js --- export const metadata = { title: 'Next.js', description: 'Generated by Next.js', }; export default function RootLayout({ children }) { return ( <html lang="en"> <body>{children}</body> </html> ); } --- File: /ai/packages/rsc/tests/e2e/next-server/app/page.js --- export default function Page() { return <div>Page</div>; } --- File: /ai/packages/rsc/tests/e2e/spec/streamable.e2e.test.ts --- import { test, expect } from '@playwright/test'; test('createStreamableValue and readStreamableValue', async ({ page }) => { await page.goto('/rsc'); await page.click('#test-streamable-value'); const logs = page.locator('#log'); await expect(logs).toHaveText( '["hello","hello, world","hello, world!",{"value":"I am a JSON"},["Finished"]]', ); }); test('test-streamable-ui', async ({ page }) => { await page.goto('/rsc'); await page.click('#test-streamable-ui'); const logs = page.locator('#log'); await expect(logs).toHaveText('I am a button'); }); --- File: /ai/packages/rsc/.eslintrc.js --- module.exports = { root: true, extends: ['vercel-ai'], }; --- File: /ai/packages/rsc/playwright.config.ts --- import type { PlaywrightTestConfig } from '@playwright/test'; import { devices } from '@playwright/test'; const PORT = process.env.PORT || 3000; const baseURL = `http://localhost:${PORT}`; /** * See https://playwright.dev/docs/test-configuration. */ const config: PlaywrightTestConfig = { testDir: './tests/e2e/spec', snapshotPathTemplate: './tests/e2e/__snapshots__/{testFilePath}/{arg}{ext}', timeout: 20_000, expect: { timeout: 10_000, }, fullyParallel: false, workers: 3, retries: 2, reporter: process.env.CI ? [['github'], ['json', { outputFile: 'test-results.json' }]] : 'list', projects: [ { name: 'chromium', use: devices['Desktop Chrome'], }, ], use: { baseURL, trace: 'retain-on-failure', userAgent: 'playwright-test bot', }, webServer: { cwd: './tests/e2e/next-server', command: 'pnpm run dev', url: baseURL, timeout: 120 * 1000, reuseExistingServer: false, }, }; export default config; --- File: /ai/packages/rsc/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ // RSC APIs - shared client { // Entry is `.mts` as the entrypoints that import it will be ESM so it needs exact imports that includes the `.mjs` extension. entry: ['src/rsc-shared.mts'], outDir: 'dist', format: ['esm'], external: ['react', 'zod'], dts: true, sourcemap: true, }, // RSC APIs - server, client { entry: ['src/rsc-server.ts', 'src/rsc-client.ts'], outDir: 'dist', format: ['esm'], external: ['react', 'zod', /\/rsc-shared/], dts: true, sourcemap: true, }, // RSC APIs - types { entry: ['src/types/index.ts'], outDir: 'dist', dts: true, outExtension() { return { // It must be `.d.ts` instead of `.d.mts` to support node resolution. // See https://github.com/vercel/ai/issues/1028. dts: '.d.ts', js: '.mjs', }; }, }, ]); --- File: /ai/packages/rsc/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts{,x}'], exclude: [ '**/*.ui.test.ts{,x}', '**/*.e2e.test.ts{,x}', '**/node_modules/**', ], typecheck: { enabled: true, }, }, }); --- File: /ai/packages/rsc/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts{,x}'], exclude: [ '**/*.ui.test.ts{,x}', '**/*.e2e.test.ts{,x}', '**/node_modules/**', ], typecheck: { enabled: true, }, }, }); --- File: /ai/packages/rsc/vitest.ui.react.config.js --- import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', globals: true, include: ['src/**/*.ui.test.ts', 'src/**/*.ui.test.tsx'], }, }); --- File: /ai/packages/svelte/src/chat.svelte.test.ts --- import { createTestServer, mockId, TestResponseController, } from '@ai-sdk/provider-utils/test'; import { DefaultChatTransport, isToolUIPart, TextStreamChatTransport, type UIMessageChunk, } from 'ai'; import { flushSync } from 'svelte'; import { Chat } from './chat.svelte.js'; import { promiseWithResolvers } from './utils.svelte.js'; function formatChunk(part: UIMessageChunk) { return `data: ${JSON.stringify(part)}\n\n`; } function createFileList(...files: File[]): FileList { // file lists are really hard to create :( const input = document.createElement('input'); input.setAttribute('type', 'file'); input.setAttribute('name', 'file-upload'); input.multiple = true; const fileList: FileList = Object.create(input.files); for (let i = 0; i < files.length; i++) { fileList[i] = files[i]; } Object.defineProperty(fileList, 'length', { value: files.length }); return fileList; } const server = createTestServer({ '/api/chat': {}, }); describe('data protocol stream', () => { let chat: Chat; beforeEach(() => { chat = new Chat({ generateId: mockId(), }); }); it('should correctly manage streamed response in messages', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), ], }; await chat.sendMessage({ parts: [{ text: 'hi', type: 'text' }], }); expect(chat.messages.at(0)).toStrictEqual( expect.objectContaining({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }), ); expect(chat.messages.at(1)).toStrictEqual( expect.objectContaining({ role: 'assistant', parts: [{ type: 'text', text: 'Hello, world.', state: 'done' }], }), ); }); it('should show error response when there is a server error', async () => { server.urls['/api/chat'].response = { type: 'error', status: 404, body: 'Not found', }; await chat.sendMessage({ text: 'hi', }); expect(chat.error).toBeInstanceOf(Error); expect(chat.error?.message).toBe('Not found'); }); it('should show error response when there is a streaming error', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'error', errorText: 'custom error message', }), ], }; await chat.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); expect(chat.error).toBeInstanceOf(Error); expect(chat.error?.message).toBe('custom error message'); }); describe('status', () => { it('should show status', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; const appendOperation = chat.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); await vi.waitFor(() => expect(chat.status).toBe('submitted')); controller.write(formatChunk({ type: 'text-start', id: '0' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), ); controller.write(formatChunk({ type: 'text-end', id: '0' })); await vi.waitFor(() => expect(chat.status).toBe('streaming')); controller.close(); await appendOperation; expect(chat.status).toBe('ready'); }); it('should set status to error when there is a server error', async () => { server.urls['/api/chat'].response = { type: 'error', status: 404, body: 'Not found', }; chat.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); await vi.waitFor(() => expect(chat.status).toBe('error')); }); }); it('should invoke onFinish when the stream finishes', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), formatChunk({ type: 'finish', messageMetadata: { example: 'metadata', }, }), ], }; const onFinish = vi.fn(); const chatWithOnFinish = new Chat({ onFinish, generateId: mockId(), }); await chatWithOnFinish.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); expect(onFinish).toHaveBeenCalledExactlyOnceWith({ message: { id: 'id-2', metadata: { example: 'metadata', }, parts: [ { text: 'Hello, world.', type: 'text', state: 'done', }, ], role: 'assistant', }, }); }); describe('id', () => { it('should send the id to the server', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: ['0:"Hello"\n', '0:","\n', '0:" world"\n', '0:"."\n'], }; await chat.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "text": "hi", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); }); }); describe('text stream', () => { let chat: Chat; beforeEach(() => { const generateId = mockId(); chat = new Chat({ generateId, transport: new TextStreamChatTransport({ api: '/api/chat', }), }); }); it('should show streamed response', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: ['Hello', ',', ' world', '.'], }; await chat.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); expect(chat.messages).toMatchInlineSnapshot(` [ { "id": "id-1", "metadata": undefined, "parts": [ { "text": "hi", "type": "text", }, ], "role": "user", }, { "id": "id-2", "metadata": undefined, "parts": [ { "type": "step-start", }, { "providerMetadata": undefined, "state": "done", "text": "Hello, world.", "type": "text", }, ], "role": "assistant", }, ] `); }); it('should have stable message ids', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; const appendOperation = chat.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); controller.write('He'); await vi.waitFor(() => expect(chat.messages.at(1)).toStrictEqual( expect.objectContaining({ id: expect.any(String), role: 'assistant', metadata: undefined, parts: [ { type: 'step-start' }, { text: 'He', type: 'text', state: 'streaming' }, ], }), ), ); const id = chat.messages.at(1)?.id; controller.write('llo'); controller.close(); await appendOperation; expect(id).toBeDefined(); expect(chat.messages.at(1)).toStrictEqual( expect.objectContaining({ id, role: 'assistant', }), ); }); it('should invoke onFinish when the stream finishes', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: ['Hello', ',', ' world', '.'], }; const onFinish = vi.fn(); const chatWithOnFinish = new Chat({ onFinish, transport: new TextStreamChatTransport({ api: '/api/chat', }), }); await chatWithOnFinish.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); expect(onFinish).toHaveBeenCalledExactlyOnceWith({ message: { id: expect.any(String), role: 'assistant', metadata: undefined, parts: [ { type: 'step-start' }, { text: 'Hello, world.', type: 'text', state: 'done' }, ], }, }); }); }); describe('onToolCall', () => { let resolve: () => void; let toolCallPromise: Promise<void>; let chat: Chat; beforeEach(() => { ({ resolve, promise: toolCallPromise } = promiseWithResolvers<void>()); chat = new Chat({ async onToolCall({ toolCall }) { await toolCallPromise; chat.addToolResult({ tool: 'test-tool', toolCallId: toolCall.toolCallId, output: `test-tool-response: ${toolCall.toolName} ${ toolCall.toolCallId } ${JSON.stringify(toolCall.input)}`, }); }, }); }); it("should invoke onToolCall when a tool call is received from the server's response", async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), ], }; const appendOperation = chat.sendMessage({ text: 'hi' }); await vi.waitFor(() => { expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'input-available', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: undefined, providerExecuted: undefined, }, ]); }); resolve(); await appendOperation; expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'output-available', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: 'test-tool-response: test-tool tool-call-0 {"testArg":"test-value"}', providerExecuted: undefined, }, ]); }); }); describe('tool invocations', () => { let chat: Chat; beforeEach(() => { const generateId = mockId(); chat = new Chat({ generateId, transport: new DefaultChatTransport({ api: '/api/chat', }), }); }); it('should display partial tool call, tool call, and tool result', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; const appendOperation = chat.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); controller.write( formatChunk({ type: 'tool-input-start', toolCallId: 'tool-call-0', toolName: 'test-tool', }), ); await vi.waitFor(() => { expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'input-streaming', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: undefined, output: undefined, providerExecuted: undefined, }, ]); }); controller.write( formatChunk({ type: 'tool-input-delta', toolCallId: 'tool-call-0', inputTextDelta: '{"testArg":"t', }), ); await vi.waitFor(() => { expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'input-streaming', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 't' }, output: undefined, providerExecuted: undefined, }, ]); }); controller.write( formatChunk({ type: 'tool-input-delta', toolCallId: 'tool-call-0', inputTextDelta: 'est-value"}}', }), ); await vi.waitFor(() => { expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'input-streaming', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: undefined, providerExecuted: undefined, }, ]); }); controller.write( formatChunk({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), ); await vi.waitFor(() => { expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'input-available', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: undefined, providerExecuted: undefined, }, ]); }); controller.write( formatChunk({ type: 'tool-output-available', toolCallId: 'tool-call-0', output: 'test-result', }), ); controller.close(); await appendOperation; expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'output-available', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: 'test-result', providerExecuted: undefined, }, ]); }); it('should display partial tool call and tool result (when there is no tool call streaming)', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; const appendOperation = chat.sendMessage({ text: 'hi' }); controller.write( formatChunk({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), ); await vi.waitFor(() => { expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'input-available', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: undefined, providerExecuted: undefined, }, ]); }); controller.write( formatChunk({ type: 'tool-output-available', toolCallId: 'tool-call-0', output: 'test-result', }), ); controller.close(); await appendOperation; expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'output-available', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: 'test-result', providerExecuted: undefined, }, ]); }); it('should update tool call to result when addToolResult is called', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), ], }; await chat.sendMessage({ text: 'hi', }); await vi.waitFor(() => { expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'input-available', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: undefined, providerExecuted: undefined, }, ]); }); chat.addToolResult({ tool: 'test-tool', toolCallId: 'tool-call-0', output: 'test-result', }); await vi.waitFor(() => { expect(chat.messages.at(1)?.parts.filter(isToolUIPart)).toStrictEqual([ { state: 'output-available', errorText: undefined, rawInput: undefined, toolCallId: 'tool-call-0', type: 'tool-test-tool', input: { testArg: 'test-value' }, output: 'test-result', providerExecuted: undefined, }, ]); }); }); }); describe('file attachments with data url', () => { let chat: Chat; beforeEach(() => { chat = new Chat({ generateId: mockId(), }); }); it('should handle text file attachment and submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0', }), formatChunk({ type: 'text-delta', id: '0', delta: 'Response to message with text attachment', }), formatChunk({ type: 'text-end', id: '0', }), ], }; await chat.sendMessage({ text: 'Message with text attachment', files: createFileList( new File(['test file content'], 'test.txt', { type: 'text/plain', }), ), }); expect(chat.messages).toMatchInlineSnapshot(` [ { "id": "id-1", "metadata": undefined, "parts": [ { "filename": "test.txt", "mediaType": "text/plain", "type": "file", "url": "data:text/plain;base64,dGVzdCBmaWxlIGNvbnRlbnQ=", }, { "text": "Message with text attachment", "type": "text", }, ], "role": "user", }, { "id": "id-2", "metadata": undefined, "parts": [ { "providerMetadata": undefined, "state": "done", "text": "Response to message with text attachment", "type": "text", }, ], "role": "assistant", }, ] `); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "filename": "test.txt", "mediaType": "text/plain", "type": "file", "url": "data:text/plain;base64,dGVzdCBmaWxlIGNvbnRlbnQ=", }, { "text": "Message with text attachment", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); it('should handle image file attachment and submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0', }), formatChunk({ type: 'text-delta', id: '0', delta: 'Response to message with image attachment', }), formatChunk({ type: 'text-end', id: '0', }), ], }; await chat.sendMessage({ text: 'Message with image attachment', files: createFileList( new File(['test image content'], 'test.png', { type: 'image/png', }), ), }); expect(chat.messages).toMatchInlineSnapshot(` [ { "id": "id-1", "metadata": undefined, "parts": [ { "filename": "test.png", "mediaType": "image/png", "type": "file", "url": "data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50", }, { "text": "Message with image attachment", "type": "text", }, ], "role": "user", }, { "id": "id-2", "metadata": undefined, "parts": [ { "providerMetadata": undefined, "state": "done", "text": "Response to message with image attachment", "type": "text", }, ], "role": "assistant", }, ] `); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "filename": "test.png", "mediaType": "image/png", "type": "file", "url": "data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50", }, { "text": "Message with image attachment", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); }); describe('file attachments with url', () => { let chat: Chat; beforeEach(() => { chat = new Chat({ generateId: mockId(), }); }); it('should handle image file attachment and submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0', }), formatChunk({ type: 'text-delta', id: '0', delta: 'Response to message with image attachment', }), formatChunk({ type: 'text-end', id: '0', }), ], }; await chat.sendMessage({ text: 'Message with image attachment', files: createFileList( new File(['test image content'], 'test.png', { type: 'image/png', }), ), }); expect(chat.messages).toMatchInlineSnapshot(` [ { "id": "id-1", "metadata": undefined, "parts": [ { "filename": "test.png", "mediaType": "image/png", "type": "file", "url": "data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50", }, { "text": "Message with image attachment", "type": "text", }, ], "role": "user", }, { "id": "id-2", "metadata": undefined, "parts": [ { "providerMetadata": undefined, "state": "done", "text": "Response to message with image attachment", "type": "text", }, ], "role": "assistant", }, ] `); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "filename": "test.png", "mediaType": "image/png", "type": "file", "url": "data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50", }, { "text": "Message with image attachment", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); }); describe('file attachments with empty text content', () => { let chat: Chat; beforeEach(() => { chat = new Chat({ generateId: mockId(), }); }); it('should handle image file attachment and submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0', }), formatChunk({ type: 'text-delta', id: '0', delta: 'Response to message with image attachment', }), formatChunk({ type: 'text-end', id: '0', }), ], }; await chat.sendMessage({ files: createFileList( new File(['test image content'], 'test.png', { type: 'image/png', }), ), }); flushSync(); expect(chat.messages).toMatchInlineSnapshot(` [ { "id": "id-1", "metadata": undefined, "parts": [ { "filename": "test.png", "mediaType": "image/png", "type": "file", "url": "data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50", }, ], "role": "user", }, { "id": "id-2", "metadata": undefined, "parts": [ { "providerMetadata": undefined, "state": "done", "text": "Response to message with image attachment", "type": "text", }, ], "role": "assistant", }, ] `); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "filename": "test.png", "mediaType": "image/png", "type": "file", "url": "data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); }); describe('reload', () => { let chat: Chat; beforeEach(() => { chat = new Chat({ generateId: mockId(), }); }); it('should show streamed response', async () => { server.urls['/api/chat'].response = [ { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'first response', }), formatChunk({ type: 'text-end', id: '0' }), ], }, { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'second response', }), formatChunk({ type: 'text-end', id: '0' }), ], }, ]; await chat.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); expect(chat.messages.at(0)).toStrictEqual( expect.objectContaining({ role: 'user', }), ); expect(chat.messages.at(1)).toStrictEqual( expect.objectContaining({ role: 'assistant', parts: [{ text: 'first response', type: 'text', state: 'done' }], }), ); // Setup done, call regenerate: await chat.regenerate({ body: { 'request-body-key': 'request-body-value' }, headers: { 'header-key': 'header-value' }, }); expect(await server.calls[1].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "parts": [ { "text": "hi", "type": "text", }, ], "role": "user", }, ], "request-body-key": "request-body-value", "trigger": "regenerate-message", } `); expect(server.calls[1].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'header-key': 'header-value', }); expect(chat.messages.at(1)).toStrictEqual( expect.objectContaining({ role: 'assistant', parts: [{ text: 'second response', type: 'text', state: 'done' }], }), ); }); }); describe('test sending additional fields during message submission', () => { let chat: Chat; beforeEach(() => { chat = new Chat({ generateId: mockId(), }); }); it('should send metadata with the message', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: ['0:"first response"\n'], }; await chat.sendMessage({ role: 'user', metadata: { test: 'example' }, parts: [{ text: 'hi', type: 'text' }], }); expect(chat.messages.at(0)).toStrictEqual( expect.objectContaining({ role: 'user', }), ); expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` { "id": "id-0", "messages": [ { "id": "id-1", "metadata": { "test": "example", }, "parts": [ { "text": "hi", "type": "text", }, ], "role": "user", }, ], "trigger": "submit-message", } `); }); }); describe('generateId function', () => { it('should use the provided generateId function for both user and assistant messages', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'start', messageId: '123' }), formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), ], }; const chatWithCustomId = new Chat({ generateId: mockId({ prefix: 'testid' }), }); await chatWithCustomId.sendMessage({ role: 'user', parts: [{ text: 'hi', type: 'text' }], }); expect(chatWithCustomId.messages).toMatchInlineSnapshot(` [ { "id": "testid-1", "metadata": undefined, "parts": [ { "text": "hi", "type": "text", }, ], "role": "user", }, { "id": "123", "metadata": undefined, "parts": [ { "providerMetadata": undefined, "state": "done", "text": "Hello, world.", "type": "text", }, ], "role": "assistant", }, ] `); }); }); describe('reactivity', () => { it('should be able to render as a derived', () => { const chat = $derived(new Chat({})); // If this isn't handled correctly, it'd show a `state_unsafe_mutation` error. // eslint-disable-next-line @typescript-eslint/no-unused-expressions chat.messages; }); }); --- File: /ai/packages/svelte/src/chat.svelte.ts --- import { AbstractChat, type ChatInit, type ChatState, type ChatStatus, type CreateUIMessage, type UIMessage, } from 'ai'; export type { CreateUIMessage, UIMessage }; export class Chat< UI_MESSAGE extends UIMessage = UIMessage, > extends AbstractChat<UI_MESSAGE> { constructor(init: ChatInit<UI_MESSAGE>) { super({ ...init, state: new SvelteChatState(init.messages), }); } } class SvelteChatState<UI_MESSAGE extends UIMessage> implements ChatState<UI_MESSAGE> { messages: UI_MESSAGE[]; status = $state<ChatStatus>('ready'); error = $state<Error | undefined>(undefined); constructor(messages: UI_MESSAGE[] = []) { this.messages = $state(messages); } setMessages = (messages: UI_MESSAGE[]) => { this.messages = messages; }; pushMessage = (message: UI_MESSAGE) => { this.messages.push(message); }; popMessage = () => { this.messages.pop(); }; replaceMessage = (index: number, message: UI_MESSAGE) => { this.messages[index] = message; }; snapshot = <T>(thing: T): T => $state.snapshot(thing) as T; } --- File: /ai/packages/svelte/src/completion-context.svelte.ts --- import type { JSONValue } from 'ai'; import { SvelteMap } from 'svelte/reactivity'; import { createContext, KeyedStore } from './utils.svelte.js'; class CompletionStore { completions = new SvelteMap<string, string>(); data = $state<JSONValue[]>([]); loading = $state(false); error = $state<Error>(); } export class KeyedCompletionStore extends KeyedStore<CompletionStore> { constructor( value?: Iterable<readonly [string, CompletionStore]> | null | undefined, ) { super(CompletionStore, value); } } export const { hasContext: hasCompletionContext, getContext: getCompletionContext, setContext: setCompletionContext, } = createContext<KeyedCompletionStore>('Completion'); --- File: /ai/packages/svelte/src/completion.svelte.test.ts --- import { createTestServer, TestResponseController, } from '@ai-sdk/provider-utils/test'; import { render } from '@testing-library/svelte'; import type { UIMessageChunk } from 'ai'; import { Completion } from './completion.svelte.js'; import CompletionSynchronization from './tests/completion-synchronization.svelte'; function formatChunk(part: UIMessageChunk) { return `data: ${JSON.stringify(part)}\n\n`; } const server = createTestServer({ '/api/completion': {}, }); describe('Completion', () => { it('should render a data stream', async () => { server.urls['/api/completion'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), ], }; const completion = new Completion(); await completion.complete('hi'); expect(completion.completion).toBe('Hello, world.'); }); it('should render a text stream', async () => { server.urls['/api/completion'].response = { type: 'stream-chunks', chunks: ['Hello', ',', ' world', '.'], }; const completion = new Completion({ streamProtocol: 'text' }); await completion.complete('hi'); expect(completion.completion).toBe('Hello, world.'); }); it('should call `onFinish` callback', async () => { server.urls['/api/completion'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), ], }; const onFinish = vi.fn(); const completion = new Completion({ onFinish }); await completion.complete('hi'); expect(onFinish).toHaveBeenCalledExactlyOnceWith('hi', 'Hello, world.'); }); it('should show loading state', async () => { const controller = new TestResponseController(); server.urls['/api/completion'].response = { type: 'controlled-stream', controller, }; const completion = new Completion(); const completionOperation = completion.complete('hi'); controller.write('0:"Hello"\n'); await vi.waitFor(() => expect(completion.loading).toBe(true)); controller.close(); await completionOperation; expect(completion.loading).toBe(false); }); it('should reset loading state on error', async () => { server.urls['/api/completion'].response = { type: 'error', status: 404, body: 'Not found', }; const completion = new Completion(); await completion.complete('hi'); expect(completion.error).toBeInstanceOf(Error); expect(completion.loading).toBe(false); }); it('should reset error state on subsequent completion', async () => { server.urls['/api/completion'].response = [ { type: 'error', status: 404, body: 'Not found', }, { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), ], }, ]; const completion = new Completion(); await completion.complete('hi'); expect(completion.error).toBeInstanceOf(Error); expect(completion.loading).toBe(false); await completion.complete('hi'); expect(completion.error).toBe(undefined); expect(completion.completion).toBe('Hello, world.'); }); }); describe('synchronization', () => { it('correctly synchronizes content between hook instances', async () => { server.urls['/api/completion'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), ], }; const { component: { completion1, completion2 }, } = render(CompletionSynchronization, { id: crypto.randomUUID() }); await completion1.complete('hi'); expect(completion1.completion).toBe('Hello, world.'); expect(completion2.completion).toBe('Hello, world.'); }); it('correctly synchronizes loading and error state between hook instances', async () => { const controller = new TestResponseController(); server.urls['/api/completion'].response = { type: 'controlled-stream', controller, }; const { component: { completion1, completion2 }, } = render(CompletionSynchronization, { id: crypto.randomUUID() }); const completionOperation = completion1.complete('hi'); await vi.waitFor(() => { expect(completion1.loading).toBe(true); expect(completion2.loading).toBe(true); }); controller.write(formatChunk({ type: 'text-start', id: '0' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), ); controller.write(formatChunk({ type: 'text-end', id: '0' })); await vi.waitFor(() => { expect(completion1.completion).toBe('Hello'); expect(completion2.completion).toBe('Hello'); }); controller.error(new Error('Failed to be cool enough')); await completionOperation; expect(completion1.loading).toBe(false); expect(completion2.loading).toBe(false); expect(completion1.error).toBeInstanceOf(Error); expect(completion1.error?.message).toBe('Failed to be cool enough'); expect(completion2.error).toBeInstanceOf(Error); expect(completion2.error?.message).toBe('Failed to be cool enough'); }); }); --- File: /ai/packages/svelte/src/completion.svelte.ts --- import { callCompletionApi, generateId, type CompletionRequestOptions, type UseCompletionOptions, } from 'ai'; import { KeyedCompletionStore, getCompletionContext, hasCompletionContext, } from './completion-context.svelte.js'; export type CompletionOptions = Readonly<UseCompletionOptions>; export class Completion { readonly #options: CompletionOptions = {}; readonly #api = $derived(this.#options.api ?? '/api/completion'); readonly #id = $derived(this.#options.id ?? generateId()); readonly #streamProtocol = $derived(this.#options.streamProtocol ?? 'data'); readonly #keyedStore = $state<KeyedCompletionStore>()!; readonly #store = $derived(this.#keyedStore.get(this.#id)); #abortController: AbortController | undefined; /** The current completion result */ get completion(): string { return this.#store.completions.get(this.#id) ?? ''; } set completion(value: string) { this.#store.completions.set(this.#id, value); } /** The error object of the API request */ get error() { return this.#store.error; } /** The current value of the input. Writable, so it can be bound to form inputs. */ input = $state<string>()!; /** * Flag that indicates whether an API request is in progress. */ get loading() { return this.#store.loading; } constructor(options: CompletionOptions = {}) { this.#keyedStore = hasCompletionContext() ? getCompletionContext() : new KeyedCompletionStore(); this.#options = options; this.completion = options.initialCompletion ?? ''; this.input = options.initialInput ?? ''; } /** * Abort the current request immediately, keep the generated tokens if any. */ stop = () => { try { this.#abortController?.abort(); } catch { // ignore } finally { this.#store.loading = false; this.#abortController = undefined; } }; /** * Send a new prompt to the API endpoint and update the completion state. */ complete = async (prompt: string, options?: CompletionRequestOptions) => this.#triggerRequest(prompt, options); /** Form submission handler to automatically reset input and call the completion API */ handleSubmit = async (event?: { preventDefault?: () => void }) => { event?.preventDefault?.(); if (this.input) { await this.complete(this.input); } }; #triggerRequest = async ( prompt: string, options?: CompletionRequestOptions, ) => { return callCompletionApi({ api: this.#api, prompt, credentials: this.#options.credentials, headers: { ...this.#options.headers, ...options?.headers }, body: { ...this.#options.body, ...options?.body, }, streamProtocol: this.#streamProtocol, fetch: this.#options.fetch, // throttle streamed ui updates: setCompletion: completion => { this.completion = completion; }, setLoading: loading => { this.#store.loading = loading; }, setError: error => { this.#store.error = error; }, setAbortController: abortController => { this.#abortController = abortController ?? undefined; }, onFinish: this.#options.onFinish, onError: this.#options.onError, }); }; } --- File: /ai/packages/svelte/src/context-provider.ts --- import { KeyedCompletionStore, setCompletionContext, } from './completion-context.svelte.js'; import { KeyedStructuredObjectStore, setStructuredObjectContext, } from './structured-object-context.svelte.js'; export function createAIContext() { const completionStore = new KeyedCompletionStore(); setCompletionContext(completionStore); const objectStore = new KeyedStructuredObjectStore(); setStructuredObjectContext(objectStore); } --- File: /ai/packages/svelte/src/index.ts --- export { Chat, type CreateUIMessage, type UIMessage } from './chat.svelte.js'; export { Completion, type CompletionOptions } from './completion.svelte.js'; export { createAIContext } from './context-provider.js'; export { StructuredObject as Experimental_StructuredObject, type Experimental_StructuredObjectOptions, } from './structured-object.svelte.js'; --- File: /ai/packages/svelte/src/structured-object-context.svelte.ts --- import type { DeepPartial } from 'ai'; import { createContext, KeyedStore } from './utils.svelte.js'; export class StructuredObjectStore<RESULT> { object = $state<DeepPartial<RESULT>>(); loading = $state(false); error = $state<Error>(); } export class KeyedStructuredObjectStore extends KeyedStore< StructuredObjectStore<unknown> > { constructor( value?: | Iterable<readonly [string, StructuredObjectStore<unknown>]> | null | undefined, ) { super(StructuredObjectStore, value); } } export const { hasContext: hasStructuredObjectContext, getContext: getStructuredObjectContext, setContext: setStructuredObjectContext, } = createContext<KeyedStructuredObjectStore>('StructuredObject'); --- File: /ai/packages/svelte/src/structured-object.svelte.test.ts --- import { createTestServer, TestResponseController, } from '@ai-sdk/provider-utils/test'; import { render } from '@testing-library/svelte'; import { z } from 'zod/v4'; import { StructuredObject } from './structured-object.svelte.js'; import StructuredObjectSynchronization from './tests/structured-object-synchronization.svelte'; const server = createTestServer({ '/api/object': {}, }); describe('text stream', () => { const schema = z.object({ content: z.string() }); let structuredObject: StructuredObject<typeof schema>; beforeEach(() => { structuredObject = new StructuredObject({ api: '/api/object', schema, }); }); describe('when the API returns "Hello, world!"', () => { beforeEach(async () => { server.urls['/api/object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content": "Hello, ', 'world', '!"', ' }'], }; await structuredObject.submit('test-input'); }); it('should render the stream', () => { expect(structuredObject.object).toEqual({ content: 'Hello, world!' }); }); it('should send the correct input to the API', async () => { expect(await server.calls[0].requestBodyJson).toBe('test-input'); }); it('should not have an error', () => { expect(structuredObject.error).toBeUndefined(); }); }); describe('loading', () => { it('should be true while loading', async () => { const controller = new TestResponseController(); server.urls['/api/object'].response = { type: 'controlled-stream', controller, }; controller.write('{"content": '); const submitOperation = structuredObject.submit('test-input'); await vi.waitFor(() => { expect(structuredObject.loading).toBe(true); }); controller.write('"Hello, world!"}'); controller.close(); await submitOperation; expect(structuredObject.loading).toBe(false); }); }); describe('stop', () => { it('should abort the stream and not consume any more data', async () => { const controller = new TestResponseController(); server.urls['/api/object'].response = { type: 'controlled-stream', controller, }; controller.write('{"content": "h'); const submitOperation = structuredObject.submit('test-input'); await vi.waitFor(() => { expect(structuredObject.loading).toBe(true); expect(structuredObject.object).toStrictEqual({ content: 'h', }); }); structuredObject.stop(); await vi.waitFor(() => { expect(structuredObject.loading).toBe(false); }); await expect(controller.write('ello, world!"}')).rejects.toThrow(); await expect(controller.close()).rejects.toThrow(); await submitOperation; expect(structuredObject.loading).toBe(false); expect(structuredObject.object).toStrictEqual({ content: 'h', }); }); it('should stop and clear the object state after a call to submit then clear', async () => { const controller = new TestResponseController(); server.urls['/api/object'].response = { type: 'controlled-stream', controller, }; controller.write('{"content": "h'); const submitOperation = structuredObject.submit('test-input'); await vi.waitFor(() => { expect(structuredObject.loading).toBe(true); expect(structuredObject.object).toStrictEqual({ content: 'h', }); }); structuredObject.clear(); await vi.waitFor(() => { expect(structuredObject.loading).toBe(false); }); await expect(controller.write('ello, world!"}')).rejects.toThrow(); await expect(controller.close()).rejects.toThrow(); await submitOperation; expect(structuredObject.loading).toBe(false); expect(structuredObject.error).toBeUndefined(); expect(structuredObject.object).toBeUndefined(); }); }); describe('when the API returns a 404', () => { it('should produce the correct error state', async () => { server.urls['/api/object'].response = { type: 'error', status: 404, body: 'Not found', }; await structuredObject.submit('test-input'); expect(structuredObject.error).toBeInstanceOf(Error); expect(structuredObject.error?.message).toBe('Not found'); expect(structuredObject.loading).toBe(false); }); }); describe('onFinish', () => { it('should be called with an object when the stream finishes and the object matches the schema', async () => { server.urls['/api/object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'], }; const onFinish = vi.fn(); const structuredObjectWithOnFinish = new StructuredObject({ api: '/api/object', schema: z.object({ content: z.string() }), onFinish, }); await structuredObjectWithOnFinish.submit('test-input'); expect(onFinish).toHaveBeenCalledExactlyOnceWith({ object: { content: 'Hello, world!' }, error: undefined, }); }); it('should be called with an error when the stream finishes and the object does not match the schema', async () => { server.urls['/api/object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content-wrong": "Hello, ', 'world', '!"', '}'], }; const onFinish = vi.fn(); const structuredObjectWithOnFinish = new StructuredObject({ api: '/api/object', schema: z.object({ content: z.string() }), onFinish, }); await structuredObjectWithOnFinish.submit('test-input'); expect(onFinish).toHaveBeenCalledExactlyOnceWith({ object: undefined, error: expect.any(Error), }); }); }); it('should send custom headers', async () => { server.urls['/api/object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'], }; const structuredObjectWithCustomHeaders = new StructuredObject({ api: '/api/object', schema: z.object({ content: z.string() }), headers: { Authorization: 'Bearer TEST_TOKEN', 'X-Custom-Header': 'CustomValue', }, }); await structuredObjectWithCustomHeaders.submit('test-input'); expect(server.calls[0].requestHeaders).toStrictEqual({ 'content-type': 'application/json', authorization: 'Bearer TEST_TOKEN', 'x-custom-header': 'CustomValue', }); }); it('should send custom credentials', async () => { server.urls['/api/object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'], }; const structuredObjectWithCustomCredentials = new StructuredObject({ api: '/api/object', schema: z.object({ content: z.string() }), credentials: 'include', }); await structuredObjectWithCustomCredentials.submit('test-input'); expect(server.calls[0].requestCredentials).toBe('include'); }); it('should clear the object state after a call to clear', async () => { server.urls['/api/object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'], }; const structuredObjectWithOnFinish = new StructuredObject({ api: '/api/object', schema: z.object({ content: z.string() }), }); await structuredObjectWithOnFinish.submit('test-input'); expect(structuredObjectWithOnFinish.object).toBeDefined(); structuredObjectWithOnFinish.clear(); expect(structuredObjectWithOnFinish.object).toBeUndefined(); expect(structuredObjectWithOnFinish.error).toBeUndefined(); expect(structuredObjectWithOnFinish.loading).toBe(false); }); }); describe('synchronization', () => { it('correctly synchronizes content between hook instances', async () => { server.urls['/api/object'].response = { type: 'stream-chunks', chunks: ['{ ', '"content": "Hello, ', 'world', '!"', '}'], }; const { component: { object1, object2 }, } = render(StructuredObjectSynchronization, { id: crypto.randomUUID(), api: '/api/object', schema: z.object({ content: z.string() }), }); await object1.submit('hi'); expect(object1.object).toStrictEqual({ content: 'Hello, world!' }); expect(object2.object).toStrictEqual(object1.object); }); it('correctly synchronizes loading and error state between hook instances', async () => { const controller = new TestResponseController(); server.urls['/api/object'].response = { type: 'controlled-stream', controller, }; const { component: { object1, object2 }, } = render(StructuredObjectSynchronization, { id: crypto.randomUUID(), api: '/api/object', schema: z.object({ content: z.string() }), }); const submitOperation = object1.submit('hi'); await vi.waitFor(() => { expect(object1.loading).toBe(true); expect(object2.loading).toBe(true); }); controller.write('{ "content": "Hello"'); await vi.waitFor(() => { expect(object1.object).toStrictEqual({ content: 'Hello' }); expect(object2.object).toStrictEqual(object1.object); }); controller.error(new Error('Failed to be cool enough')); await submitOperation; expect(object1.loading).toBe(false); expect(object2.loading).toBe(false); expect(object1.error).toBeInstanceOf(Error); expect(object1.error?.message).toBe('Failed to be cool enough'); expect(object2.error).toBeInstanceOf(Error); expect(object2.error?.message).toBe('Failed to be cool enough'); }); }); --- File: /ai/packages/svelte/src/structured-object.svelte.ts --- import { generateId, isAbortError, safeValidateTypes, type FetchFunction, type InferSchema, } from '@ai-sdk/provider-utils'; import { asSchema, isDeepEqualData, parsePartialJson, type DeepPartial, type Schema, } from 'ai'; import type * as z3 from 'zod/v3'; import type * as z4 from 'zod/v4'; import { getStructuredObjectContext, hasStructuredObjectContext, KeyedStructuredObjectStore, type StructuredObjectStore, } from './structured-object-context.svelte.js'; export type Experimental_StructuredObjectOptions< SCHEMA extends z3.Schema | z4.core.$ZodType | Schema, RESULT = InferSchema<SCHEMA>, > = { /** * The API endpoint. It should stream JSON that matches the schema as chunked text. */ api: string; /** * A Zod schema that defines the shape of the complete object. */ schema: SCHEMA; /** * An unique identifier. If not provided, a random one will be * generated. When provided, the `useObject` hook with the same `id` will * have shared states across components. */ id?: string; /** * An optional value for the initial object. */ initialValue?: DeepPartial<RESULT>; /** * Custom fetch implementation. You can use it as a middleware to intercept requests, * or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; /** * Callback that is called when the stream has finished. */ onFinish?: (event: { /** * The generated object (typed according to the schema). * Can be undefined if the final object does not match the schema. */ object: RESULT | undefined; /** * Optional error object. This is e.g. a TypeValidationError when the final object does not match the schema. */ error: Error | undefined; }) => Promise<void> | void; /** * Callback function to be called when an error is encountered. */ onError?: (error: Error) => void; /** * Additional HTTP headers to be included in the request. */ headers?: Record<string, string> | Headers; /** * The credentials mode to be used for the fetch request. * Possible values are: 'omit', 'same-origin', 'include'. * Defaults to 'same-origin'. */ credentials?: RequestCredentials; }; export class StructuredObject< SCHEMA extends z3.Schema | z4.core.$ZodType | Schema, RESULT = InferSchema<SCHEMA>, INPUT = unknown, > { #options: Experimental_StructuredObjectOptions<SCHEMA, RESULT> = {} as Experimental_StructuredObjectOptions<SCHEMA, RESULT>; readonly #id = $derived(this.#options.id ?? generateId()); readonly #keyedStore = $state<KeyedStructuredObjectStore>()!; readonly #store = $derived( this.#keyedStore.get(this.#id), ) as StructuredObjectStore<RESULT>; #abortController: AbortController | undefined; /** * The current value for the generated object. Updated as the API streams JSON chunks. */ get object(): DeepPartial<RESULT> | undefined { return this.#store.object; } set #object(value: DeepPartial<RESULT> | undefined) { this.#store.object = value; } /** The error object of the API request */ get error() { return this.#store.error; } /** * Flag that indicates whether an API request is in progress. */ get loading() { return this.#store.loading; } constructor(options: Experimental_StructuredObjectOptions<SCHEMA, RESULT>) { if (hasStructuredObjectContext()) { this.#keyedStore = getStructuredObjectContext(); } else { this.#keyedStore = new KeyedStructuredObjectStore(); } this.#options = options; this.#object = options.initialValue; } /** * Abort the current request immediately, keep the current partial object if any. */ stop = () => { try { this.#abortController?.abort(); } catch { // ignore } finally { this.#store.loading = false; this.#abortController = undefined; } }; /** * Calls the API with the provided input as JSON body. */ submit = async (input: INPUT) => { try { this.#clearObject(); this.#store.loading = true; const abortController = new AbortController(); this.#abortController = abortController; const actualFetch = this.#options.fetch ?? fetch; const response = await actualFetch(this.#options.api, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.#options.headers, }, credentials: this.#options.credentials, signal: abortController.signal, body: JSON.stringify(input), }); if (!response.ok) { throw new Error( (await response.text()) ?? 'Failed to fetch the response.', ); } if (response.body == null) { throw new Error('The response body is empty.'); } let accumulatedText = ''; let latestObject: DeepPartial<RESULT> | undefined = undefined; await response.body.pipeThrough(new TextDecoderStream()).pipeTo( new WritableStream<string>({ write: async chunk => { if (abortController?.signal.aborted) { throw new DOMException('Stream aborted', 'AbortError'); } accumulatedText += chunk; const { value } = await parsePartialJson(accumulatedText); const currentObject = value as DeepPartial<RESULT>; if (!isDeepEqualData(latestObject, currentObject)) { latestObject = currentObject; this.#store.object = currentObject; } }, close: async () => { this.#store.loading = false; this.#abortController = undefined; if (this.#options.onFinish != null) { const validationResult = await safeValidateTypes({ value: latestObject, schema: asSchema(this.#options.schema), }); this.#options.onFinish( validationResult.success ? { object: validationResult.value, error: undefined } : { object: undefined, error: validationResult.error }, ); } }, }), ); } catch (error) { if (isAbortError(error)) { return; } const coalescedError = error instanceof Error ? error : new Error(String(error)); if (this.#options.onError) { this.#options.onError(coalescedError); } this.#store.loading = false; this.#store.error = coalescedError; } }; /** * Clears the object state. */ clear = () => { this.stop(); this.#clearObject(); }; #clearObject = () => { this.#store.object = undefined; this.#store.error = undefined; this.#store.loading = false; }; } --- File: /ai/packages/svelte/src/utils.svelte.ts --- import { getContext, hasContext, setContext, untrack } from 'svelte'; import { SvelteMap } from 'svelte/reactivity'; export function createContext<T>(name: string) { const key = Symbol(name); return { hasContext: () => { // At the time of writing there's no way to determine if we're // currently initializing a component without a try-catch try { return hasContext(key); } catch (e) { if ( typeof e === 'object' && e !== null && 'message' in e && typeof e.message === 'string' && e.message?.includes('lifecycle_outside_component') ) { return false; } throw e; } }, getContext: () => getContext<T>(key), setContext: (value: T) => setContext(key, value), }; } export function promiseWithResolvers<T>(): { promise: Promise<T>; resolve: (value: T) => void; reject: (reason?: unknown) => void; } { let resolve: (value: T) => void; let reject: (reason?: unknown) => void; const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve: resolve!, reject: reject! }; } export class KeyedStore<T> extends SvelteMap<string, T> { #itemConstructor: new () => T; constructor( itemConstructor: new () => T, value?: Iterable<readonly [string, T]> | null | undefined, ) { super(value); this.#itemConstructor = itemConstructor; } get(key: string): T { const test = super.get(key) ?? // Untrack here because this is technically a state mutation, meaning // deriveds downstream would fail. Because this is idempotent (even // though it's not pure), it's safe. untrack(() => this.set(key, new this.#itemConstructor())).get(key)!; return test; } } --- File: /ai/packages/svelte/eslint.config.js --- import prettier from 'eslint-config-prettier'; import js from '@eslint/js'; import { includeIgnoreFile } from '@eslint/compat'; import svelte from 'eslint-plugin-svelte'; import globals from 'globals'; import { fileURLToPath } from 'node:url'; import ts from 'typescript-eslint'; import svelteConfig from './svelte.config.js'; const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); export default ts.config( includeIgnoreFile(gitignorePath), js.configs.recommended, ...ts.configs.recommended, ...svelte.configs.recommended, prettier, ...svelte.configs.prettier, { languageOptions: { globals: { ...globals.browser, ...globals.node, }, }, }, { files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], ignores: ['eslint.config.js', 'svelte.config.js'], languageOptions: { parserOptions: { projectService: true, extraFileExtensions: ['.svelte'], parser: ts.parser, svelteConfig, }, }, }, ); --- File: /ai/packages/svelte/svelte.config.js --- import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { // Consult https://svelte.dev/docs/kit/integrations // for more information about preprocessors preprocess: vitePreprocess(), }; export default config; --- File: /ai/packages/svelte/vite.config.ts --- import { svelteTesting } from '@testing-library/svelte/vite'; import { defineConfig } from 'vite'; import { svelte } from '@sveltejs/vite-plugin-svelte'; export default defineConfig({ plugins: [svelte()], test: { globals: true, workspace: [ { extends: './vite.config.ts', plugins: [svelteTesting()], test: { name: 'client', environment: 'jsdom', clearMocks: true, include: ['src/**/*.svelte.{test,spec}.{js,ts}'], exclude: ['src/lib/server/**'], setupFiles: ['./vitest-setup-client.ts'], }, }, { extends: './vite.config.ts', test: { name: 'server', environment: 'node', include: ['src/**/*.{test,spec}.{js,ts}'], exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'], }, }, ], }, }); --- File: /ai/packages/svelte/vitest-setup-client.ts --- import '@testing-library/jest-dom/vitest'; import { vi } from 'vitest'; // required for svelte5 + jsdom as jsdom does not support matchMedia Object.defineProperty(window, 'matchMedia', { writable: true, enumerable: true, value: vi.fn().mockImplementation(query => ({ matches: false, media: query, onchange: null, addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), })), }); // add more mocks here if you need them --- File: /ai/packages/togetherai/src/index.ts --- export { createTogetherAI, togetherai } from './togetherai-provider'; export type { TogetherAIProvider, TogetherAIProviderSettings, } from './togetherai-provider'; export type { OpenAICompatibleErrorData as TogetherAIErrorData } from '@ai-sdk/openai-compatible'; --- File: /ai/packages/togetherai/src/togetherai-chat-options.ts --- // https://docs.together.ai/docs/serverless-models#chat-models export type TogetherAIChatModelId = | 'meta-llama/Llama-3.3-70B-Instruct-Turbo' | 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo' | 'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo' | 'meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo' | 'meta-llama/Meta-Llama-3-8B-Instruct-Turbo' | 'meta-llama/Meta-Llama-3-70B-Instruct-Turbo' | 'meta-llama/Llama-3.2-3B-Instruct-Turbo' | 'meta-llama/Meta-Llama-3-8B-Instruct-Lite' | 'meta-llama/Meta-Llama-3-70B-Instruct-Lite' | 'meta-llama/Llama-3-8b-chat-hf' | 'meta-llama/Llama-3-70b-chat-hf' | 'nvidia/Llama-3.1-Nemotron-70B-Instruct-HF' | 'Qwen/Qwen2.5-Coder-32B-Instruct' | 'Qwen/QwQ-32B-Preview' | 'microsoft/WizardLM-2-8x22B' | 'google/gemma-2-27b-it' | 'google/gemma-2-9b-it' | 'databricks/dbrx-instruct' | 'deepseek-ai/deepseek-llm-67b-chat' | 'deepseek-ai/DeepSeek-V3' | 'google/gemma-2b-it' | 'Gryphe/MythoMax-L2-13b' | 'meta-llama/Llama-2-13b-chat-hf' | 'mistralai/Mistral-7B-Instruct-v0.1' | 'mistralai/Mistral-7B-Instruct-v0.2' | 'mistralai/Mistral-7B-Instruct-v0.3' | 'mistralai/Mixtral-8x7B-Instruct-v0.1' | 'mistralai/Mixtral-8x22B-Instruct-v0.1' | 'NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO' | 'Qwen/Qwen2.5-7B-Instruct-Turbo' | 'Qwen/Qwen2.5-72B-Instruct-Turbo' | 'Qwen/Qwen2-72B-Instruct' | 'upstage/SOLAR-10.7B-Instruct-v1.0' | (string & {}); --- File: /ai/packages/togetherai/src/togetherai-completion-options.ts --- // https://docs.together.ai/docs/serverless-models#language-models export type TogetherAICompletionModelId = | 'meta-llama/Llama-2-70b-hf' | 'mistralai/Mistral-7B-v0.1' | 'mistralai/Mixtral-8x7B-v0.1' | 'Meta-Llama/Llama-Guard-7b' | 'codellama/CodeLlama-34b-Instruct-hf' | 'Qwen/Qwen2.5-Coder-32B-Instruct' | (string & {}); --- File: /ai/packages/togetherai/src/togetherai-embedding-options.ts --- // https://docs.together.ai/docs/serverless-models#embedding-models export type TogetherAIEmbeddingModelId = | 'togethercomputer/m2-bert-80M-2k-retrieval' | 'togethercomputer/m2-bert-80M-32k-retrieval' | 'togethercomputer/m2-bert-80M-8k-retrieval' | 'WhereIsAI/UAE-Large-V1' | 'BAAI/bge-large-en-v1.5' | 'BAAI/bge-base-en-v1.5' | 'sentence-transformers/msmarco-bert-base-dot-v5' | 'bert-base-uncased' | (string & {}); --- File: /ai/packages/togetherai/src/togetherai-image-model.test.ts --- import { FetchFunction } from '@ai-sdk/provider-utils'; import { createTestServer } from '@ai-sdk/provider-utils/test'; import { describe, expect, it } from 'vitest'; import { TogetherAIImageModel } from './togetherai-image-model'; const prompt = 'A cute baby sea otter'; function createBasicModel({ headers, fetch, currentDate, }: { headers?: () => Record<string, string>; fetch?: FetchFunction; currentDate?: () => Date; } = {}) { return new TogetherAIImageModel('stabilityai/stable-diffusion-xl', { provider: 'togetherai', baseURL: 'https://api.example.com', headers: headers ?? (() => ({ 'api-key': 'test-key' })), fetch, _internal: { currentDate, }, }); } const server = createTestServer({ 'https://api.example.com/*': { response: { type: 'json-value', body: { id: 'test-id', data: [{ index: 0, b64_json: 'test-base64-content' }], model: 'stabilityai/stable-diffusion-xl', object: 'list', }, }, }, }); describe('doGenerate', () => { it('should pass the correct parameters including size and seed', async () => { const model = createBasicModel(); await model.doGenerate({ prompt, n: 1, size: '1024x1024', seed: 42, providerOptions: { togetherai: { additional_param: 'value' } }, aspectRatio: undefined, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'stabilityai/stable-diffusion-xl', prompt, seed: 42, n: 1, width: 1024, height: 1024, response_format: 'base64', additional_param: 'value', }); }); it('should call the correct url', async () => { const model = createBasicModel(); await model.doGenerate({ prompt, n: 1, size: '1024x1024', seed: 42, providerOptions: {}, aspectRatio: undefined, }); expect(server.calls[0].requestMethod).toStrictEqual('POST'); expect(server.calls[0].requestUrl).toStrictEqual( 'https://api.example.com/images/generations', ); }); it('should pass headers', async () => { const modelWithHeaders = createBasicModel({ headers: () => ({ 'Custom-Provider-Header': 'provider-header-value', }), }); await modelWithHeaders.doGenerate({ prompt, n: 1, size: undefined, seed: undefined, providerOptions: {}, aspectRatio: undefined, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should handle API errors', async () => { server.urls['https://api.example.com/*'].response = { type: 'error', status: 400, body: JSON.stringify({ error: { message: 'Bad Request', }, }), }; const model = createBasicModel(); await expect( model.doGenerate({ prompt, n: 1, size: undefined, seed: undefined, providerOptions: {}, aspectRatio: undefined, }), ).rejects.toMatchObject({ message: 'Bad Request', }); }); describe('warnings', () => { it('should return aspectRatio warning when aspectRatio is provided', async () => { const model = createBasicModel(); const result = await model.doGenerate({ prompt, n: 1, size: '1024x1024', aspectRatio: '1:1', seed: 123, providerOptions: {}, }); expect(result.warnings).toContainEqual({ type: 'unsupported-setting', setting: 'aspectRatio', details: 'This model does not support the `aspectRatio` option. Use `size` instead.', }); }); }); it('should respect the abort signal', async () => { const model = createBasicModel(); const controller = new AbortController(); const generatePromise = model.doGenerate({ prompt, n: 1, size: undefined, seed: undefined, providerOptions: {}, aspectRatio: undefined, abortSignal: controller.signal, }); controller.abort(); await expect(generatePromise).rejects.toThrow('This operation was aborted'); }); describe('response metadata', () => { it('should include timestamp, headers and modelId in response', async () => { const testDate = new Date('2024-01-01T00:00:00Z'); const model = createBasicModel({ currentDate: () => testDate, }); const result = await model.doGenerate({ prompt, n: 1, size: undefined, seed: undefined, providerOptions: {}, aspectRatio: undefined, }); expect(result.response).toStrictEqual({ timestamp: testDate, modelId: 'stabilityai/stable-diffusion-xl', headers: expect.any(Object), }); }); it('should include response headers from API call', async () => { server.urls['https://api.example.com/*'].response = { type: 'json-value', body: { id: 'test-id', data: [{ index: 0, b64_json: 'test-base64-content' }], model: 'stabilityai/stable-diffusion-xl', object: 'list', }, headers: { 'x-request-id': 'test-request-id', 'content-length': '128', }, }; const model = createBasicModel(); const result = await model.doGenerate({ prompt, n: 1, size: undefined, seed: undefined, providerOptions: {}, aspectRatio: undefined, }); expect(result.response.headers).toStrictEqual({ 'x-request-id': 'test-request-id', 'content-type': 'application/json', 'content-length': '128', }); }); }); }); describe('constructor', () => { it('should expose correct provider and model information', () => { const model = createBasicModel(); expect(model.provider).toBe('togetherai'); expect(model.modelId).toBe('stabilityai/stable-diffusion-xl'); expect(model.specificationVersion).toBe('v2'); expect(model.maxImagesPerCall).toBe(1); }); }); --- File: /ai/packages/togetherai/src/togetherai-image-model.ts --- import { ImageModelV2, ImageModelV2CallWarning } from '@ai-sdk/provider'; import { combineHeaders, createJsonResponseHandler, createJsonErrorResponseHandler, FetchFunction, postJsonToApi, } from '@ai-sdk/provider-utils'; import { TogetherAIImageModelId } from './togetherai-image-settings'; import { z } from 'zod/v4'; interface TogetherAIImageModelConfig { provider: string; baseURL: string; headers: () => Record<string, string>; fetch?: FetchFunction; _internal?: { currentDate?: () => Date; }; } export class TogetherAIImageModel implements ImageModelV2 { readonly specificationVersion = 'v2'; readonly maxImagesPerCall = 1; get provider(): string { return this.config.provider; } constructor( readonly modelId: TogetherAIImageModelId, private config: TogetherAIImageModelConfig, ) {} async doGenerate({ prompt, n, size, seed, providerOptions, headers, abortSignal, }: Parameters<ImageModelV2['doGenerate']>[0]): Promise< Awaited<ReturnType<ImageModelV2['doGenerate']>> > { const warnings: Array<ImageModelV2CallWarning> = []; if (size != null) { warnings.push({ type: 'unsupported-setting', setting: 'aspectRatio', details: 'This model does not support the `aspectRatio` option. Use `size` instead.', }); } const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const splitSize = size?.split('x'); // https://docs.together.ai/reference/post_images-generations const { value: response, responseHeaders } = await postJsonToApi({ url: `${this.config.baseURL}/images/generations`, headers: combineHeaders(this.config.headers(), headers), body: { model: this.modelId, prompt, seed, n, ...(splitSize && { width: parseInt(splitSize[0]), height: parseInt(splitSize[1]), }), response_format: 'base64', ...(providerOptions.togetherai ?? {}), }, failedResponseHandler: createJsonErrorResponseHandler({ errorSchema: togetheraiErrorSchema, errorToMessage: data => data.error.message, }), successfulResponseHandler: createJsonResponseHandler( togetheraiImageResponseSchema, ), abortSignal, fetch: this.config.fetch, }); return { images: response.data.map(item => item.b64_json), warnings, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, }, }; } } // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const togetheraiImageResponseSchema = z.object({ data: z.array( z.object({ b64_json: z.string(), }), ), }); // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const togetheraiErrorSchema = z.object({ error: z.object({ message: z.string(), }), }); --- File: /ai/packages/togetherai/src/togetherai-image-settings.ts --- // https://api.together.ai/models export type TogetherAIImageModelId = | 'stabilityai/stable-diffusion-xl-base-1.0' | 'black-forest-labs/FLUX.1-dev' | 'black-forest-labs/FLUX.1-dev-lora' | 'black-forest-labs/FLUX.1-schnell' | 'black-forest-labs/FLUX.1-canny' | 'black-forest-labs/FLUX.1-depth' | 'black-forest-labs/FLUX.1-redux' | 'black-forest-labs/FLUX.1.1-pro' | 'black-forest-labs/FLUX.1-pro' | 'black-forest-labs/FLUX.1-schnell-Free' | (string & {}); --- File: /ai/packages/togetherai/src/togetherai-provider.test.ts --- import { OpenAICompatibleChatLanguageModel, OpenAICompatibleCompletionLanguageModel, OpenAICompatibleEmbeddingModel, } from '@ai-sdk/openai-compatible'; import { LanguageModelV2, EmbeddingModelV2 } from '@ai-sdk/provider'; import { loadApiKey } from '@ai-sdk/provider-utils'; import { TogetherAIImageModel } from './togetherai-image-model'; import { createTogetherAI } from './togetherai-provider'; import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; // Add type assertion for the mocked class const OpenAICompatibleChatLanguageModelMock = OpenAICompatibleChatLanguageModel as unknown as Mock; vi.mock('@ai-sdk/openai-compatible', () => ({ OpenAICompatibleChatLanguageModel: vi.fn(), OpenAICompatibleCompletionLanguageModel: vi.fn(), OpenAICompatibleEmbeddingModel: vi.fn(), })); vi.mock('@ai-sdk/provider-utils', () => ({ loadApiKey: vi.fn().mockReturnValue('mock-api-key'), withoutTrailingSlash: vi.fn(url => url), })); vi.mock('./togetherai-image-model', () => ({ TogetherAIImageModel: vi.fn(), })); describe('TogetherAIProvider', () => { let mockLanguageModel: LanguageModelV2; let mockEmbeddingModel: EmbeddingModelV2<string>; let createOpenAICompatibleMock: Mock; beforeEach(() => { // Mock implementations of models mockLanguageModel = { // Add any required methods for LanguageModelV2 } as LanguageModelV2; mockEmbeddingModel = { // Add any required methods for EmbeddingModelV2 } as EmbeddingModelV2<string>; // Reset mocks vi.clearAllMocks(); }); describe('createTogetherAI', () => { it('should create a TogetherAIProvider instance with default options', () => { const provider = createTogetherAI(); const model = provider('model-id'); // Use the mocked version const constructorCall = OpenAICompatibleChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; config.headers(); expect(loadApiKey).toHaveBeenCalledWith({ apiKey: undefined, environmentVariableName: 'TOGETHER_AI_API_KEY', description: 'TogetherAI', }); }); it('should create a TogetherAIProvider instance with custom options', () => { const options = { apiKey: 'custom-key', baseURL: 'https://custom.url', headers: { 'Custom-Header': 'value' }, }; const provider = createTogetherAI(options); const model = provider('model-id'); const constructorCall = OpenAICompatibleChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; config.headers(); expect(loadApiKey).toHaveBeenCalledWith({ apiKey: 'custom-key', environmentVariableName: 'TOGETHER_AI_API_KEY', description: 'TogetherAI', }); }); it('should return a chat model when called as a function', () => { const provider = createTogetherAI(); const modelId = 'foo-model-id'; const model = provider(modelId); expect(model).toBeInstanceOf(OpenAICompatibleChatLanguageModel); }); }); describe('chatModel', () => { it('should construct a chat model with correct configuration', () => { const provider = createTogetherAI(); const modelId = 'together-chat-model'; const model = provider.chatModel(modelId); expect(model).toBeInstanceOf(OpenAICompatibleChatLanguageModel); }); }); describe('completionModel', () => { it('should construct a completion model with correct configuration', () => { const provider = createTogetherAI(); const modelId = 'together-completion-model'; const model = provider.completionModel(modelId); expect(model).toBeInstanceOf(OpenAICompatibleCompletionLanguageModel); }); }); describe('textEmbeddingModel', () => { it('should construct a text embedding model with correct configuration', () => { const provider = createTogetherAI(); const modelId = 'together-embedding-model'; const model = provider.textEmbeddingModel(modelId); expect(model).toBeInstanceOf(OpenAICompatibleEmbeddingModel); }); }); describe('image', () => { it('should construct an image model with correct configuration', () => { const provider = createTogetherAI(); const modelId = 'stabilityai/stable-diffusion-xl'; const model = provider.image(modelId); expect(TogetherAIImageModel).toHaveBeenCalledWith( modelId, expect.objectContaining({ provider: 'togetherai.image', baseURL: 'https://api.together.xyz/v1/', }), ); expect(model).toBeInstanceOf(TogetherAIImageModel); }); it('should pass custom baseURL to image model', () => { const provider = createTogetherAI({ baseURL: 'https://custom.url/', }); const modelId = 'stabilityai/stable-diffusion-xl'; provider.image(modelId); expect(TogetherAIImageModel).toHaveBeenCalledWith( modelId, expect.objectContaining({ baseURL: 'https://custom.url/', }), ); }); }); }); --- File: /ai/packages/togetherai/src/togetherai-provider.ts --- import { LanguageModelV2, EmbeddingModelV2, ProviderV2, ImageModelV2, } from '@ai-sdk/provider'; import { OpenAICompatibleChatLanguageModel, OpenAICompatibleCompletionLanguageModel, OpenAICompatibleEmbeddingModel, } from '@ai-sdk/openai-compatible'; import { FetchFunction, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { TogetherAIChatModelId } from './togetherai-chat-options'; import { TogetherAIEmbeddingModelId } from './togetherai-embedding-options'; import { TogetherAICompletionModelId } from './togetherai-completion-options'; import { TogetherAIImageModel } from './togetherai-image-model'; import { TogetherAIImageModelId } from './togetherai-image-settings'; export interface TogetherAIProviderSettings { /** TogetherAI API key. */ apiKey?: string; /** Base URL for the API calls. */ baseURL?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } export interface TogetherAIProvider extends ProviderV2 { /** Creates a model for text generation. */ (modelId: TogetherAIChatModelId): LanguageModelV2; /** Creates a chat model for text generation. */ chatModel(modelId: TogetherAIChatModelId): LanguageModelV2; /** Creates a chat model for text generation. */ languageModel(modelId: TogetherAIChatModelId): LanguageModelV2; /** Creates a completion model for text generation. */ completionModel(modelId: TogetherAICompletionModelId): LanguageModelV2; /** Creates a text embedding model for text generation. */ textEmbeddingModel( modelId: TogetherAIEmbeddingModelId, ): EmbeddingModelV2<string>; /** Creates a model for image generation. */ image(modelId: TogetherAIImageModelId): ImageModelV2; /** Creates a model for image generation. */ imageModel(modelId: TogetherAIImageModelId): ImageModelV2; } export function createTogetherAI( options: TogetherAIProviderSettings = {}, ): TogetherAIProvider { const baseURL = withoutTrailingSlash( options.baseURL ?? 'https://api.together.xyz/v1/', ); const getHeaders = () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'TOGETHER_AI_API_KEY', description: 'TogetherAI', })}`, ...options.headers, }); interface CommonModelConfig { provider: string; url: ({ path }: { path: string }) => string; headers: () => Record<string, string>; fetch?: FetchFunction; } const getCommonModelConfig = (modelType: string): CommonModelConfig => ({ provider: `togetherai.${modelType}`, url: ({ path }) => `${baseURL}${path}`, headers: getHeaders, fetch: options.fetch, }); const createChatModel = (modelId: TogetherAIChatModelId) => { return new OpenAICompatibleChatLanguageModel( modelId, getCommonModelConfig('chat'), ); }; const createCompletionModel = (modelId: TogetherAICompletionModelId) => new OpenAICompatibleCompletionLanguageModel( modelId, getCommonModelConfig('completion'), ); const createTextEmbeddingModel = (modelId: TogetherAIEmbeddingModelId) => new OpenAICompatibleEmbeddingModel( modelId, getCommonModelConfig('embedding'), ); const createImageModel = (modelId: TogetherAIImageModelId) => new TogetherAIImageModel(modelId, { ...getCommonModelConfig('image'), baseURL: baseURL ?? 'https://api.together.xyz/v1/', }); const provider = (modelId: TogetherAIChatModelId) => createChatModel(modelId); provider.completionModel = createCompletionModel; provider.languageModel = createChatModel; provider.chatModel = createChatModel; provider.textEmbeddingModel = createTextEmbeddingModel; provider.image = createImageModel; provider.imageModel = createImageModel; return provider; } export const togetherai = createTogetherAI(); --- File: /ai/packages/togetherai/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/togetherai/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/togetherai/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/valibot/src/index.ts --- export { valibotSchema } from './valibot-schema'; --- File: /ai/packages/valibot/src/valibot-schema.ts --- import { toJsonSchema as valibotToJsonSchema } from '@valibot/to-json-schema'; import { jsonSchema, Schema } from '@ai-sdk/provider-utils'; import * as v from 'valibot'; export function valibotSchema< SCHEMA extends v.GenericSchema<unknown, unknown, v.BaseIssue<unknown>>, >(valibotSchema: SCHEMA): Schema<v.InferOutput<SCHEMA>> { return jsonSchema(valibotToJsonSchema(valibotSchema), { validate: value => { const result = v.safeParse(valibotSchema, value); return result.success ? { success: true, value: result.output } : { success: false, error: new v.ValiError(result.issues) }; }, }); } --- File: /ai/packages/valibot/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/valibot/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/valibot/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/vercel/src/index.ts --- export { createVercel, vercel } from './vercel-provider'; export type { VercelProvider, VercelProviderSettings } from './vercel-provider'; export type { OpenAICompatibleErrorData as VercelErrorData } from '@ai-sdk/openai-compatible'; --- File: /ai/packages/vercel/src/vercel-chat-options.ts --- // https://v0.dev/docs/v0-model-api export type VercelChatModelId = | 'v0-1.0-md' | 'v0-1.5-md' | 'v0-1.5-lg' | (string & {}); --- File: /ai/packages/vercel/src/vercel-provider.test.ts --- import { createVercel } from './vercel-provider'; import { OpenAICompatibleChatLanguageModel } from '@ai-sdk/openai-compatible'; import { LanguageModelV2 } from '@ai-sdk/provider'; import { loadApiKey } from '@ai-sdk/provider-utils'; import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; const OpenAICompatibleChatLanguageModelMock = OpenAICompatibleChatLanguageModel as unknown as Mock; vi.mock('@ai-sdk/openai-compatible', () => ({ OpenAICompatibleChatLanguageModel: vi.fn(), OpenAICompatibleCompletionLanguageModel: vi.fn(), })); vi.mock('@ai-sdk/provider-utils', () => ({ loadApiKey: vi.fn().mockReturnValue('mock-api-key'), withoutTrailingSlash: vi.fn(url => url), })); vi.mock('./vercel-image-model', () => ({ VercelImageModel: vi.fn(), })); describe('VercelProvider', () => { let mockLanguageModel: LanguageModelV2; beforeEach(() => { mockLanguageModel = { // Add any required methods for LanguageModelV1 } as LanguageModelV2; // Reset mocks vi.clearAllMocks(); }); describe('createVercel', () => { it('should create a VercelProvider instance with default options', () => { const provider = createVercel(); provider('model-id'); // Use the mocked version const constructorCall = OpenAICompatibleChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; config.headers(); expect(loadApiKey).toHaveBeenCalledWith({ apiKey: undefined, environmentVariableName: 'VERCEL_API_KEY', description: 'Vercel', }); }); it('should create a VercelProvider instance with custom options', () => { const options = { apiKey: 'custom-key', baseURL: 'https://custom.url', headers: { 'Custom-Header': 'value' }, }; const provider = createVercel(options); provider('model-id'); const constructorCall = OpenAICompatibleChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; config.headers(); expect(loadApiKey).toHaveBeenCalledWith({ apiKey: 'custom-key', environmentVariableName: 'VERCEL_API_KEY', description: 'Vercel', }); }); it('should return a chat model when called as a function', () => { const provider = createVercel(); const modelId = 'foo-model-id'; const model = provider(modelId); expect(model).toBeInstanceOf(OpenAICompatibleChatLanguageModel); }); }); it('should construct a language model with correct configuration', () => { const provider = createVercel(); const modelId = 'vercel-chat-model'; const model = provider.languageModel(modelId); expect(model).toBeInstanceOf(OpenAICompatibleChatLanguageModel); expect(OpenAICompatibleChatLanguageModelMock).toHaveBeenCalledWith( modelId, expect.objectContaining({ provider: 'vercel.chat', }), ); }); }); --- File: /ai/packages/vercel/src/vercel-provider.ts --- import { LanguageModelV2, NoSuchModelError, ProviderV2, } from '@ai-sdk/provider'; import { OpenAICompatibleChatLanguageModel } from '@ai-sdk/openai-compatible'; import { FetchFunction, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { VercelChatModelId } from './vercel-chat-options'; export interface VercelProviderSettings { /** Vercel API key. */ apiKey?: string; /** Base URL for the API calls. */ baseURL?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } export interface VercelProvider extends ProviderV2 { /** Creates a model for text generation. */ (modelId: VercelChatModelId): LanguageModelV2; /** Creates a language model for text generation. */ languageModel(modelId: VercelChatModelId): LanguageModelV2; } export function createVercel( options: VercelProviderSettings = {}, ): VercelProvider { const baseURL = withoutTrailingSlash( options.baseURL ?? 'https://api.v0.dev/v1', ); const getHeaders = () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'VERCEL_API_KEY', description: 'Vercel', })}`, ...options.headers, }); interface CommonModelConfig { provider: string; url: ({ path }: { path: string }) => string; headers: () => Record<string, string>; fetch?: FetchFunction; } const getCommonModelConfig = (modelType: string): CommonModelConfig => ({ provider: `vercel.${modelType}`, url: ({ path }) => `${baseURL}${path}`, headers: getHeaders, fetch: options.fetch, }); const createChatModel = (modelId: VercelChatModelId) => { return new OpenAICompatibleChatLanguageModel(modelId, { ...getCommonModelConfig('chat'), }); }; const provider = (modelId: VercelChatModelId) => createChatModel(modelId); provider.languageModel = createChatModel; provider.textEmbeddingModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'textEmbeddingModel' }); }; provider.imageModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'imageModel' }); }; return provider; } export const vercel = createVercel(); --- File: /ai/packages/vercel/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/vercel/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/vercel/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/vue/src/chat.vue.ts --- import { AbstractChat, ChatInit as BaseChatInit, ChatState, ChatStatus, UIMessage, } from 'ai'; import { Ref, ref } from 'vue'; class VueChatState<UI_MESSAGE extends UIMessage> implements ChatState<UI_MESSAGE> { private messagesRef: Ref<UI_MESSAGE[]>; private statusRef = ref<ChatStatus>('ready'); private errorRef = ref<Error | undefined>(undefined); constructor(messages?: UI_MESSAGE[]) { this.messagesRef = ref(messages ?? []) as Ref<UI_MESSAGE[]>; } get messages(): UI_MESSAGE[] { return this.messagesRef.value; } set messages(messages: UI_MESSAGE[]) { this.messagesRef.value = messages; } get status(): ChatStatus { return this.statusRef.value; } set status(status: ChatStatus) { this.statusRef.value = status; } get error(): Error | undefined { return this.errorRef.value; } set error(error: Error | undefined) { this.errorRef.value = error; } pushMessage = (message: UI_MESSAGE) => { this.messagesRef.value = [...this.messagesRef.value, message]; }; popMessage = () => { this.messagesRef.value = this.messagesRef.value.slice(0, -1); }; replaceMessage = (index: number, message: UI_MESSAGE) => { // message is cloned here because vue's deep reactivity shows unexpected behavior, particularly when updating tool invocation parts this.messagesRef.value[index] = { ...message }; }; snapshot = <T>(value: T): T => value; } export class Chat< UI_MESSAGE extends UIMessage, > extends AbstractChat<UI_MESSAGE> { constructor({ messages, ...init }: BaseChatInit<UI_MESSAGE>) { super({ ...init, state: new VueChatState(messages), }); } } --- File: /ai/packages/vue/src/chat.vue.ui.test.tsx --- import { createTestServer, TestResponseController, } from '@ai-sdk/provider-utils/test'; import '@testing-library/jest-dom/vitest'; import userEvent from '@testing-library/user-event'; import { screen, waitFor } from '@testing-library/vue'; import { UIMessageChunk } from 'ai'; import { setupTestComponent } from './setup-test-component'; import TestChatAppendAttachmentsComponent from './TestChatAppendAttachmentsComponent.vue'; import TestChatAttachmentsComponent from './TestChatAttachmentsComponent.vue'; import TestChatComponent from './TestChatComponent.vue'; import TestChatInitMessages from './TestChatInitMessages.vue'; import TestChatPrepareRequestBodyComponent from './TestChatPrepareRequestBodyComponent.vue'; import TestChatReloadComponent from './TestChatReloadComponent.vue'; import TestChatTextStreamComponent from './TestChatTextStreamComponent.vue'; import TestChatToolInvocationsComponent from './TestChatToolInvocationsComponent.vue'; import TestChatUrlAttachmentsComponent from './TestChatUrlAttachmentsComponent.vue'; function formatChunk(part: UIMessageChunk) { return `data: ${JSON.stringify(part)}\n\n`; } const server = createTestServer({ '/api/chat': {}, }); describe('prepareSubmitMessagesRequest', () => { setupTestComponent(TestChatPrepareRequestBodyComponent); it('should show streamed response', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), ], }; await userEvent.click(screen.getByTestId('do-append')); await waitFor(() => { const element = screen.getByTestId('on-options'); expect(element.textContent?.trim() ?? '').not.toBe(''); }); const value = JSON.parse( screen.getByTestId('on-options').textContent ?? '', ); expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi'); expect(value).toStrictEqual({ id: expect.any(String), api: '/api/chat', trigger: 'submit-message', body: { 'request-body-key': 'request-body-value' }, headers: { 'request-header-key': 'request-header-value' }, requestMetadata: { 'request-metadata-key': 'request-metadata-value' }, messages: [ { role: 'user', id: expect.any(String), parts: [{ type: 'text', text: 'hi' }], }, ], }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ 'body-key': 'body-value', }); expect(server.calls[0].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'header-key': 'header-value', }); await screen.findByTestId('message-1'); expect(screen.getByTestId('message-1')).toHaveTextContent( 'AI: Hello, world.', ); }); }); describe('data protocol stream', () => { setupTestComponent(TestChatComponent); it('should show streamed response', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), ], }; await userEvent.click(screen.getByTestId('do-append')); await screen.findByTestId('message-0'); expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi'); await screen.findByTestId('message-1'); expect(screen.getByTestId('message-1')).toHaveTextContent( 'AI: Hello, world.', ); }); it('should show error response', async () => { server.urls['/api/chat'].response = { type: 'error', status: 404, body: 'Not found', }; await userEvent.click(screen.getByTestId('do-append')); // TODO bug? the user message does not show up // await screen.findByTestId('message-0'); // expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi'); await screen.findByTestId('error'); expect(screen.getByTestId('error')).toHaveTextContent('Error: Not found'); }); describe('status', () => { it('should show status', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; await userEvent.click(screen.getByTestId('do-append')); await waitFor(() => { expect(screen.getByTestId('status')).toHaveTextContent('submitted'); }); controller.write(formatChunk({ type: 'text-start', id: '0' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), ); controller.write(formatChunk({ type: 'text-end', id: '0' })); await waitFor(() => { expect(screen.getByTestId('status')).toHaveTextContent('streaming'); }); controller.close(); await waitFor(() => { expect(screen.getByTestId('status')).toHaveTextContent('ready'); }); }); it('should update status when the tab is hidden', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream', controller, }; const originalVisibilityState = document.visibilityState; try { await userEvent.click(screen.getByTestId('do-append')); await waitFor(() => expect(screen.getByTestId('status')).toHaveTextContent('submitted'), ); controller.write(formatChunk({ type: 'text-start', id: '0' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), ); await waitFor(() => expect(screen.getByTestId('status')).toHaveTextContent('streaming'), ); Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'hidden', }); document.dispatchEvent(new Event('visibilitychange')); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: ' world.' }), ); controller.write(formatChunk({ type: 'text-end', id: '0' })); controller.close(); await waitFor(() => expect(screen.getByTestId('status')).toHaveTextContent('ready'), ); } finally { Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => originalVisibilityState, }); document.dispatchEvent(new Event('visibilitychange')); } }); it('should set status to error when there is a server error', async () => { server.urls['/api/chat'].response = { type: 'error', status: 404, body: 'Not found', }; await userEvent.click(screen.getByTestId('do-append')); await waitFor(() => { expect(screen.getByTestId('status')).toHaveTextContent('error'); }); }); }); it('should invoke onFinish when the stream finishes', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), formatChunk({ type: 'finish' }), ], }; await userEvent.click(screen.getByTestId('do-append')); await waitFor(() => { const element = screen.getByTestId('on-finish-calls'); expect(element.textContent?.trim() ?? '').not.toBe(''); }); const value = JSON.parse( screen.getByTestId('on-finish-calls').textContent ?? '', ); expect(value).toStrictEqual([ { message: { id: expect.any(String), role: 'assistant', parts: [{ text: 'Hello, world.', type: 'text', state: 'done' }], }, }, ]); }); }); describe('text stream', () => { setupTestComponent(TestChatTextStreamComponent); it('should show streamed response', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: ['Hello', ',', ' world', '.'], }; await userEvent.click(screen.getByTestId('do-append')); await screen.findByTestId('message-0'); expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi'); await screen.findByTestId('message-1'); expect(screen.getByTestId('message-1')).toHaveTextContent( 'AI: Hello, world.', ); }); it('should invoke onFinish when the stream finishes', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: ['Hello', ',', ' world', '.'], }; await userEvent.click(screen.getByTestId('do-append')); await waitFor(() => { const element = screen.getByTestId('on-finish-calls'); expect(element.textContent?.trim() ?? '').not.toBe(''); }); const value = JSON.parse( screen.getByTestId('on-finish-calls').textContent ?? '', ); expect(value).toStrictEqual([ { message: { id: expect.any(String), role: 'assistant', parts: [ { type: 'step-start' }, { text: 'Hello, world.', type: 'text', state: 'done' }, ], }, }, ]); }); }); describe('regenerate', () => { setupTestComponent(TestChatReloadComponent); it('should show streamed response', async () => { server.urls['/api/chat'].response = [ { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'first response', }), formatChunk({ type: 'text-end', id: '0' }), ], }, { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'second response', }), formatChunk({ type: 'text-end', id: '0' }), ], }, ]; await userEvent.click(screen.getByTestId('do-append')); await screen.findByTestId('message-0'); expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi'); await screen.findByTestId('message-1'); // setup done, click reload: await userEvent.click(screen.getByTestId('do-regenerate')); expect(await server.calls[1].requestBodyJson).toStrictEqual({ id: expect.any(String), messages: [ { id: 'id-0', parts: [ { text: 'hi', type: 'text', }, ], role: 'user', }, ], 'request-body-key': 'request-body-value', trigger: 'regenerate-message', }); expect(server.calls[1].requestHeaders).toStrictEqual({ 'content-type': 'application/json', 'header-key': 'header-value', }); await screen.findByTestId('message-1'); expect(screen.getByTestId('message-1')).toHaveTextContent( 'AI: second response', ); }); }); describe('tool invocations', () => { setupTestComponent(TestChatToolInvocationsComponent); it('should display partial tool call, tool call, and tool result', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = [ { type: 'controlled-stream', controller }, ]; await userEvent.click(screen.getByTestId('do-append')); controller.write( formatChunk({ type: 'tool-input-start', toolCallId: 'tool-call-0', toolName: 'test-tool', }), ); await waitFor(() => { expect(screen.getByTestId('message-1')).toHaveTextContent( '{"type":"tool-test-tool","toolCallId":"tool-call-0","state":"input-streaming"}', ); }); controller.write( formatChunk({ type: 'tool-input-delta', toolCallId: 'tool-call-0', inputTextDelta: '{"testArg":"t', }), ); await waitFor(() => { expect(screen.getByTestId('message-1')).toHaveTextContent( '{"type":"tool-test-tool","toolCallId":"tool-call-0","state":"input-streaming","input":{"testArg":"t"}}', ); }); controller.write( formatChunk({ type: 'tool-input-delta', toolCallId: 'tool-call-0', inputTextDelta: 'est-value"}}', }), ); await waitFor(() => { expect(screen.getByTestId('message-1')).toHaveTextContent( '{"type":"tool-test-tool","toolCallId":"tool-call-0","state":"input-streaming","input":{"testArg":"test-value"}}', ); }); controller.write( formatChunk({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), ); await waitFor(() => { expect( JSON.parse(screen.getByTestId('message-1').textContent ?? ''), ).toStrictEqual({ state: 'input-available', input: { testArg: 'test-value' }, toolCallId: 'tool-call-0', type: 'tool-test-tool', }); }); controller.write( formatChunk({ type: 'tool-output-available', toolCallId: 'tool-call-0', output: 'test-result', }), ); controller.close(); await waitFor(() => { expect( JSON.parse(screen.getByTestId('message-1').textContent ?? ''), ).toStrictEqual({ state: 'output-available', input: { testArg: 'test-value' }, toolCallId: 'tool-call-0', type: 'tool-test-tool', output: 'test-result', }); }); }); it('should display tool call and tool result (when there is no tool call streaming)', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = [ { type: 'controlled-stream', controller, }, ]; await userEvent.click(screen.getByTestId('do-append')); controller.write( formatChunk({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), ); await waitFor(() => { expect( JSON.parse(screen.getByTestId('message-1').textContent ?? ''), ).toStrictEqual({ state: 'input-available', input: { testArg: 'test-value' }, toolCallId: 'tool-call-0', type: 'tool-test-tool', }); }); controller.write( formatChunk({ type: 'tool-output-available', toolCallId: 'tool-call-0', output: 'test-result', }), ); controller.close(); await waitFor(() => { expect(screen.getByTestId('message-1')).toHaveTextContent('test-result'); }); }); it('should update tool call to result when addToolResult is called', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = [ { type: 'controlled-stream', controller, }, ]; await userEvent.click(screen.getByTestId('do-append')); controller.write(formatChunk({ type: 'start' })); controller.write(formatChunk({ type: 'start-step' })); controller.write( formatChunk({ type: 'tool-input-available', toolCallId: 'tool-call-0', toolName: 'test-tool', input: { testArg: 'test-value' }, }), ); await waitFor(() => { expect(screen.getByTestId('message-1')).toHaveTextContent( '{"type":"tool-test-tool","toolCallId":"tool-call-0","state":"input-available","input":{"testArg":"test-value"}}', ); }); await userEvent.click(screen.getByTestId('add-result-0')); await waitFor(() => { expect(screen.getByTestId('message-1')).toHaveTextContent( '{"type":"tool-test-tool","toolCallId":"tool-call-0","state":"output-available","input":{"testArg":"test-value"},"output":"test-result"}', ); }); controller.write(formatChunk({ type: 'text-start', id: '0' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: 'more text', }), ); controller.write(formatChunk({ type: 'text-end', id: '0' })); controller.close(); }); }); describe('file attachments with data url', () => { setupTestComponent(TestChatAttachmentsComponent); it('should handle text file attachment and submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0', }), formatChunk({ type: 'text-delta', id: '0', delta: 'Response to message with text attachment', }), formatChunk({ type: 'text-end', id: '0' }), ], }; const file = new File(['test file content'], 'test.txt', { type: 'text/plain', }); const fileInput = screen.getByTestId('file-input'); await userEvent.upload(fileInput, file); const messageInput = screen.getByTestId('message-input'); await userEvent.type(messageInput, 'Message with text attachment'); const submitButton = screen.getByTestId('submit-button'); await userEvent.click(submitButton); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { id: 'id-0', role: 'user', parts: [ { type: 'file', mediaType: 'text/plain', filename: 'test.txt', url: 'data:text/plain;base64,dGVzdCBmaWxlIGNvbnRlbnQ=', }, { type: 'text', text: 'Message with text attachment', }, ], }, { id: 'id-1', parts: [ { text: 'Response to message with text attachment', type: 'text', state: 'done', }, ], role: 'assistant', }, ]); }); }); it('should handle image file attachment and submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0', }), formatChunk({ type: 'text-delta', id: '0', delta: 'Response to message with image attachment', }), formatChunk({ type: 'text-end', id: '0' }), ], }; const file = new File(['test image content'], 'test.png', { type: 'image/png', }); const fileInput = screen.getByTestId('file-input'); await userEvent.upload(fileInput, file); const messageInput = screen.getByTestId('message-input'); await userEvent.type(messageInput, 'Message with image attachment'); const submitButton = screen.getByTestId('submit-button'); await userEvent.click(submitButton); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { role: 'user', id: 'id-0', parts: [ { type: 'file', mediaType: 'image/png', filename: 'test.png', url: 'data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50', }, { type: 'text', text: 'Message with image attachment', }, ], }, { role: 'assistant', id: 'id-1', parts: [ { type: 'text', text: 'Response to message with image attachment', state: 'done', }, ], }, ]); }); }); }); describe('file attachments with url', () => { setupTestComponent(TestChatUrlAttachmentsComponent); it('should handle image file attachment and submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0', }), formatChunk({ type: 'text-delta', id: '0', delta: 'Response to message with image attachment', }), formatChunk({ type: 'text-end', id: '0' }), ], }; const messageInput = screen.getByTestId('message-input'); await userEvent.type(messageInput, 'Message with image attachment'); const submitButton = screen.getByTestId('submit-button'); await userEvent.click(submitButton); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { role: 'user', id: 'id-0', parts: [ { type: 'file', mediaType: 'image/png', url: 'https://example.com/image.png', }, { type: 'text', text: 'Message with image attachment', }, ], }, { role: 'assistant', id: 'id-1', parts: [ { type: 'text', text: 'Response to message with image attachment', state: 'done', }, ], }, ]); }); }); }); describe('attachments with empty submit', () => { setupTestComponent(TestChatAttachmentsComponent); it('should handle image file attachment and empty submission', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0', }), formatChunk({ type: 'text-delta', id: '0', delta: 'Response to empty message with attachment', }), formatChunk({ type: 'text-end', id: '0' }), ], }; const file = new File(['test image content'], 'test.png', { type: 'image/png', }); const fileInput = screen.getByTestId('file-input'); await userEvent.upload(fileInput, file); const submitButton = screen.getByTestId('submit-button'); await userEvent.click(submitButton); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { id: 'id-0', role: 'user', parts: [ { type: 'file', mediaType: 'image/png', filename: 'test.png', url: 'data:image/png;base64,dGVzdCBpbWFnZSBjb250ZW50', }, { type: 'text', text: '', }, ], }, { id: 'id-1', role: 'assistant', parts: [ { type: 'text', text: 'Response to empty message with attachment', state: 'done', }, ], }, ]); }); }); }); describe('should append message with attachments', () => { setupTestComponent(TestChatAppendAttachmentsComponent); it('should handle image file attachment with append', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0', }), formatChunk({ type: 'text-delta', id: '0', delta: 'Response to message with image attachment', }), formatChunk({ type: 'text-end', id: '0' }), ], }; const appendButton = screen.getByTestId('do-append'); await userEvent.click(appendButton); await waitFor(() => { expect( JSON.parse(screen.getByTestId('messages').textContent ?? ''), ).toStrictEqual([ { id: 'id-0', parts: [ { mediaType: 'image/png', type: 'file', url: 'https://example.com/image.png', }, { text: 'Message with image attachment', type: 'text', }, ], role: 'user', }, { id: 'id-1', parts: [ { text: 'Response to message with image attachment', type: 'text', state: 'done', }, ], role: 'assistant', }, ]); }); }); }); describe('init messages', () => { setupTestComponent(TestChatInitMessages); it('should show streamed response', async () => { server.urls['/api/chat'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), ], }; await userEvent.click(screen.getByTestId('do-append')); await screen.findByTestId('message-2'); expect(screen.getByTestId('message-2')).toHaveTextContent('User: Hi.'); await screen.findByTestId('message-3'); expect(screen.getByTestId('message-3')).toHaveTextContent( 'AI: Hello, world.', ); }); }); --- File: /ai/packages/vue/src/index.ts --- export * from './use-completion'; export { Chat } from './chat.vue'; --- File: /ai/packages/vue/src/setup-test-component.ts --- import { cleanup, render } from '@testing-library/vue'; export const setupTestComponent = ( TestComponent: any, { init, }: { init?: (TestComponent: any) => any; } = {}, ) => { beforeEach(() => { render(init?.(TestComponent) ?? TestComponent); }); afterEach(() => { vi.restoreAllMocks(); cleanup(); }); return TestComponent; }; --- File: /ai/packages/vue/src/shims-vue.d.ts --- // required for vue testing library declare module '*.vue' { import Vue from 'vue'; export default Vue; } --- File: /ai/packages/vue/src/use-completion.ts --- import type { CompletionRequestOptions, UseCompletionOptions } from 'ai'; import { callCompletionApi } from 'ai'; import swrv from 'swrv'; import type { Ref } from 'vue'; import { ref, unref } from 'vue'; export type { UseCompletionOptions }; export type UseCompletionHelpers = { /** The current completion result */ completion: Ref<string>; /** The error object of the API request */ error: Ref<undefined | Error>; /** * Send a new prompt to the API endpoint and update the completion state. */ complete: ( prompt: string, options?: CompletionRequestOptions, ) => Promise<string | null | undefined>; /** * Abort the current API request but keep the generated tokens. */ stop: () => void; /** * Update the `completion` state locally. */ setCompletion: (completion: string) => void; /** The current value of the input */ input: Ref<string>; /** * Form submission handler to automatically reset input and append a user message * @example * ```jsx * <form @submit="handleSubmit"> * <input @change="handleInputChange" v-model="input" /> * </form> * ``` */ handleSubmit: (event?: { preventDefault?: () => void }) => void; /** Whether the API request is in progress */ isLoading: Ref<boolean | undefined>; }; let uniqueId = 0; // @ts-expect-error - some issues with the default export of useSWRV const useSWRV = (swrv.default as (typeof import('swrv'))['default']) || swrv; const store: Record<string, any> = {}; export function useCompletion({ api = '/api/completion', id, initialCompletion = '', initialInput = '', credentials, headers, body, streamProtocol, onFinish, onError, fetch, }: UseCompletionOptions = {}): UseCompletionHelpers { // Generate an unique id for the completion if not provided. const completionId = id || `completion-${uniqueId++}`; const key = `${api}|${completionId}`; const { data, mutate: originalMutate } = useSWRV<string>( key, () => store[key] || initialCompletion, ); const { data: isLoading, mutate: mutateLoading } = useSWRV<boolean>( `${completionId}-loading`, null, ); isLoading.value ??= false; // Force the `data` to be `initialCompletion` if it's `undefined`. data.value ||= initialCompletion; const mutate = (data: string) => { store[key] = data; return originalMutate(); }; // Because of the `initialData` option, the `data` will never be `undefined`. const completion = data as Ref<string>; const error = ref<undefined | Error>(undefined); let abortController: AbortController | null = null; async function triggerRequest( prompt: string, options?: CompletionRequestOptions, ) { return callCompletionApi({ api, prompt, credentials, headers: { ...headers, ...options?.headers, }, body: { ...unref(body), ...options?.body, }, streamProtocol, setCompletion: mutate, setLoading: loading => mutateLoading(() => loading), setError: err => { error.value = err; }, setAbortController: controller => { abortController = controller; }, onFinish, onError, fetch, }); } const complete: UseCompletionHelpers['complete'] = async ( prompt, options, ) => { return triggerRequest(prompt, options); }; const stop = () => { if (abortController) { abortController.abort(); abortController = null; } }; const setCompletion = (completion: string) => { mutate(completion); }; const input = ref(initialInput); const handleSubmit = (event?: { preventDefault?: () => void }) => { event?.preventDefault?.(); const inputValue = input.value; return inputValue ? complete(inputValue) : undefined; }; return { completion, complete, error, stop, setCompletion, input, handleSubmit, isLoading, }; } --- File: /ai/packages/vue/src/use-completion.ui.test.ts --- import { createTestServer, TestResponseController, } from '@ai-sdk/provider-utils/test'; import '@testing-library/jest-dom/vitest'; import userEvent from '@testing-library/user-event'; import { findByText, screen } from '@testing-library/vue'; import { UIMessageChunk } from 'ai'; import TestCompletionComponent from './TestCompletionComponent.vue'; import TestCompletionTextStreamComponent from './TestCompletionTextStreamComponent.vue'; import { setupTestComponent } from './setup-test-component'; function formatChunk(part: UIMessageChunk) { return `data: ${JSON.stringify(part)}\n\n`; } const server = createTestServer({ '/api/completion': {}, }); describe('stream data stream', () => { setupTestComponent(TestCompletionComponent); it('should show streamed response', async () => { server.urls['/api/completion'].response = { type: 'stream-chunks', chunks: [ formatChunk({ type: 'text-start', id: '0' }), formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), formatChunk({ type: 'text-delta', id: '0', delta: ',' }), formatChunk({ type: 'text-delta', id: '0', delta: ' world' }), formatChunk({ type: 'text-delta', id: '0', delta: '.' }), formatChunk({ type: 'text-end', id: '0' }), ], }; await userEvent.type(screen.getByTestId('input'), 'hi{enter}'); await screen.findByTestId('completion'); expect(screen.getByTestId('completion')).toHaveTextContent('Hello, world.'); }); describe('loading state', () => { it('should show loading state', async () => { const controller = new TestResponseController(); server.urls['/api/completion'].response = { type: 'controlled-stream', controller, }; await userEvent.type(screen.getByTestId('input'), 'hi{enter}'); await screen.findByTestId('loading'); expect(screen.getByTestId('loading')).toHaveTextContent('true'); controller.write(formatChunk({ type: 'text-start', id: '0' })); controller.write( formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }), ); controller.write(formatChunk({ type: 'text-end', id: '0' })); controller.close(); await findByText(await screen.findByTestId('loading'), 'false'); expect(screen.getByTestId('loading')).toHaveTextContent('false'); }); it('should reset loading state on error', async () => { server.urls['/api/completion'].response = { type: 'error', status: 404, body: 'Not found', }; await userEvent.type(screen.getByTestId('input'), 'hi{enter}'); await screen.findByTestId('loading'); expect(screen.getByTestId('loading')).toHaveTextContent('false'); }); }); }); describe('stream data stream', () => { setupTestComponent(TestCompletionTextStreamComponent); it('should show streamed response', async () => { server.urls['/api/completion'].response = { type: 'stream-chunks', chunks: ['Hello', ',', ' world', '.'], }; await userEvent.type(screen.getByTestId('input'), 'hi{enter}'); await screen.findByTestId('completion'); expect(screen.getByTestId('completion')).toHaveTextContent('Hello, world.'); }); }); --- File: /ai/packages/vue/.eslintrc.js --- module.exports = { root: true, extends: ['vercel-ai'], }; --- File: /ai/packages/vue/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], outDir: 'dist', banner: {}, format: ['cjs', 'esm'], external: ['vue'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/vue/vitest.config.js --- import vue from '@vitejs/plugin-vue'; import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], test: { environment: 'jsdom', globals: true, include: ['src/**/*.ui.test.ts', 'src/**/*.ui.test.tsx'], }, }); --- File: /ai/packages/xai/src/convert-to-xai-chat-messages.test.ts --- import { convertToXaiChatMessages } from './convert-to-xai-chat-messages'; describe('convertToXaiChatMessages', () => { it('should convert simple text messages', () => { const { messages, warnings } = convertToXaiChatMessages([ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]); expect(warnings).toEqual([]); expect(messages).toEqual([{ role: 'user', content: 'Hello' }]); }); it('should convert system messages', () => { const { messages, warnings } = convertToXaiChatMessages([ { role: 'system', content: 'You are a helpful assistant.' }, ]); expect(warnings).toEqual([]); expect(messages).toEqual([ { role: 'system', content: 'You are a helpful assistant.' }, ]); }); it('should convert assistant messages', () => { const { messages, warnings } = convertToXaiChatMessages([ { role: 'assistant', content: [{ type: 'text', text: 'Hello there!' }] }, ]); expect(warnings).toEqual([]); expect(messages).toEqual([ { role: 'assistant', content: 'Hello there!', tool_calls: undefined }, ]); }); it('should convert messages with image parts', () => { const { messages, warnings } = convertToXaiChatMessages([ { role: 'user', content: [ { type: 'text', text: 'What is in this image?' }, { type: 'file', mediaType: 'image/png', data: Buffer.from([0, 1, 2, 3]), }, ], }, ]); expect(warnings).toEqual([]); expect(messages).toEqual([ { role: 'user', content: [ { type: 'text', text: 'What is in this image?' }, { type: 'image_url', image_url: { url: 'data:image/png;base64,AAECAw==' }, }, ], }, ]); }); it('should convert image URLs', () => { const { messages, warnings } = convertToXaiChatMessages([ { role: 'user', content: [ { type: 'file', mediaType: 'image/jpeg', data: new URL('https://example.com/image.jpg'), }, ], }, ]); expect(warnings).toEqual([]); expect(messages).toEqual([ { role: 'user', content: [ { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' }, }, ], }, ]); }); it('should throw error for unsupported file types', () => { expect(() => { convertToXaiChatMessages([ { role: 'user', content: [ { type: 'file', mediaType: 'application/pdf', data: Buffer.from([0, 1, 2, 3]), }, ], }, ]); }).toThrow('file part media type application/pdf'); }); it('should convert tool calls and tool responses', () => { const { messages, warnings } = convertToXaiChatMessages([ { role: 'assistant', content: [ { type: 'tool-call', toolCallId: 'call_123', toolName: 'weather', input: { location: 'Paris' }, }, ], }, { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'call_123', toolName: 'weather', output: { type: 'json', value: { temperature: 20 } }, }, ], }, ]); expect(warnings).toEqual([]); expect(messages).toEqual([ { role: 'assistant', content: '', tool_calls: [ { id: 'call_123', type: 'function', function: { name: 'weather', arguments: '{"location":"Paris"}', }, }, ], }, { role: 'tool', tool_call_id: 'call_123', content: '{"temperature":20}', }, ]); }); it('should handle multiple tool calls in one message', () => { const { messages, warnings } = convertToXaiChatMessages([ { role: 'assistant', content: [ { type: 'tool-call', toolCallId: 'call_123', toolName: 'weather', input: { location: 'Paris' }, }, { type: 'tool-call', toolCallId: 'call_456', toolName: 'time', input: { timezone: 'UTC' }, }, ], }, ]); expect(warnings).toEqual([]); expect(messages).toEqual([ { role: 'assistant', content: '', tool_calls: [ { id: 'call_123', type: 'function', function: { name: 'weather', arguments: '{"location":"Paris"}', }, }, { id: 'call_456', type: 'function', function: { name: 'time', arguments: '{"timezone":"UTC"}', }, }, ], }, ]); }); it('should handle mixed content with text and tool calls', () => { const { messages, warnings } = convertToXaiChatMessages([ { role: 'assistant', content: [ { type: 'text', text: 'Let me check the weather for you.' }, { type: 'tool-call', toolCallId: 'call_123', toolName: 'weather', input: { location: 'Paris' }, }, ], }, ]); expect(warnings).toEqual([]); expect(messages).toEqual([ { role: 'assistant', content: 'Let me check the weather for you.', tool_calls: [ { id: 'call_123', type: 'function', function: { name: 'weather', arguments: '{"location":"Paris"}', }, }, ], }, ]); }); }); --- File: /ai/packages/xai/src/convert-to-xai-chat-messages.ts --- import { LanguageModelV2CallWarning, LanguageModelV2Prompt, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { convertToBase64 } from '@ai-sdk/provider-utils'; import { XaiChatPrompt } from './xai-chat-prompt'; export function convertToXaiChatMessages(prompt: LanguageModelV2Prompt): { messages: XaiChatPrompt; warnings: Array<LanguageModelV2CallWarning>; } { const messages: XaiChatPrompt = []; const warnings: Array<LanguageModelV2CallWarning> = []; for (const { role, content } of prompt) { switch (role) { case 'system': { messages.push({ role: 'system', content }); break; } case 'user': { if (content.length === 1 && content[0].type === 'text') { messages.push({ role: 'user', content: content[0].text }); break; } messages.push({ role: 'user', content: content.map(part => { switch (part.type) { case 'text': { return { type: 'text', text: part.text }; } case 'file': { if (part.mediaType.startsWith('image/')) { const mediaType = part.mediaType === 'image/*' ? 'image/jpeg' : part.mediaType; return { type: 'image_url', image_url: { url: part.data instanceof URL ? part.data.toString() : `data:${mediaType};base64,${convertToBase64(part.data)}`, }, }; } else { throw new UnsupportedFunctionalityError({ functionality: `file part media type ${part.mediaType}`, }); } } } }), }); break; } case 'assistant': { let text = ''; const toolCalls: Array<{ id: string; type: 'function'; function: { name: string; arguments: string }; }> = []; for (const part of content) { switch (part.type) { case 'text': { text += part.text; break; } case 'tool-call': { toolCalls.push({ id: part.toolCallId, type: 'function', function: { name: part.toolName, arguments: JSON.stringify(part.input), }, }); break; } } } messages.push({ role: 'assistant', content: text, tool_calls: toolCalls.length > 0 ? toolCalls : undefined, }); break; } case 'tool': { for (const toolResponse of content) { const output = toolResponse.output; let contentValue: string; switch (output.type) { case 'text': case 'error-text': contentValue = output.value; break; case 'content': case 'json': case 'error-json': contentValue = JSON.stringify(output.value); break; } messages.push({ role: 'tool', tool_call_id: toolResponse.toolCallId, content: contentValue, }); } break; } default: { const _exhaustiveCheck: never = role; throw new Error(`Unsupported role: ${_exhaustiveCheck}`); } } } return { messages, warnings }; } --- File: /ai/packages/xai/src/get-response-metadata.ts --- export function getResponseMetadata({ id, model, created, }: { id?: string | undefined | null; created?: number | undefined | null; model?: string | undefined | null; }) { return { id: id ?? undefined, modelId: model ?? undefined, timestamp: created != null ? new Date(created * 1000) : undefined, }; } --- File: /ai/packages/xai/src/index.ts --- export type { XaiProviderOptions } from './xai-chat-options'; export type { XaiErrorData } from './xai-error'; export { createXai, xai } from './xai-provider'; export type { XaiProvider, XaiProviderSettings } from './xai-provider'; --- File: /ai/packages/xai/src/map-xai-finish-reason.ts --- import { LanguageModelV2FinishReason } from '@ai-sdk/provider'; export function mapXaiFinishReason( finishReason: string | null | undefined, ): LanguageModelV2FinishReason { switch (finishReason) { case 'stop': return 'stop'; case 'length': return 'length'; case 'tool_calls': case 'function_call': return 'tool-calls'; case 'content_filter': return 'content-filter'; default: return 'unknown'; } } --- File: /ai/packages/xai/src/xai-chat-language-model.test.ts --- import { LanguageModelV2Prompt } from '@ai-sdk/provider'; import { convertReadableStreamToArray, createTestServer, } from '@ai-sdk/provider-utils/test'; import { XaiChatLanguageModel } from './xai-chat-language-model'; const TEST_PROMPT: LanguageModelV2Prompt = [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, ]; const testConfig = { provider: 'xai.chat', baseURL: 'https://api.x.ai/v1', headers: () => ({ authorization: 'Bearer test-api-key' }), generateId: () => 'test-id', }; const model = new XaiChatLanguageModel('grok-beta', testConfig); const server = createTestServer({ 'https://api.x.ai/v1/chat/completions': {}, }); describe('XaiChatLanguageModel', () => { it('should be instantiated correctly', () => { expect(model.modelId).toBe('grok-beta'); expect(model.provider).toBe('xai.chat'); expect(model.specificationVersion).toBe('v2'); }); it('should have supported URLs', () => { expect(model.supportedUrls).toEqual({ 'image/*': [/^https?:\/\/.*$/], }); }); describe('doGenerate', () => { function prepareJsonResponse({ content = '', usage = { prompt_tokens: 4, total_tokens: 34, completion_tokens: 30, }, id = 'chatcmpl-test-id', created = 1699472111, model = 'grok-beta', headers, }: { content?: string; usage?: { prompt_tokens: number; total_tokens: number; completion_tokens: number; }; id?: string; created?: number; model?: string; headers?: Record<string, string>; }) { server.urls['https://api.x.ai/v1/chat/completions'].response = { type: 'json-value', headers, body: { id, object: 'chat.completion', created, model, choices: [ { index: 0, message: { role: 'assistant', content, tool_calls: null, }, finish_reason: 'stop', }, ], usage, }, }; } it('should extract text content', async () => { prepareJsonResponse({ content: 'Hello, World!' }); const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "text": "Hello, World!", "type": "text", }, ] `); }); it('should avoid duplication when there is a trailing assistant message', async () => { prepareJsonResponse({ content: 'prefix and more content' }); const { content } = await model.doGenerate({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, { role: 'assistant', content: [{ type: 'text', text: 'prefix ' }], }, ], }); expect(content).toMatchInlineSnapshot(` [ { "text": "prefix and more content", "type": "text", }, ] `); }); it('should extract tool call content', async () => { server.urls['https://api.x.ai/v1/chat/completions'].response = { type: 'json-value', body: { id: 'chatcmpl-test-tool-call', object: 'chat.completion', created: 1699472111, model: 'grok-beta', choices: [ { index: 0, message: { role: 'assistant', content: null, tool_calls: [ { id: 'call_test123', type: 'function', function: { name: 'weatherTool', arguments: '{"location": "paris"}', }, }, ], }, finish_reason: 'tool_calls', }, ], usage: { prompt_tokens: 124, total_tokens: 146, completion_tokens: 22, }, }, }; const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "input": "{"location": "paris"}", "toolCallId": "call_test123", "toolName": "weatherTool", "type": "tool-call", }, ] `); }); it('should extract usage', async () => { prepareJsonResponse({ usage: { prompt_tokens: 20, total_tokens: 25, completion_tokens: 5 }, }); const { usage } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(usage).toMatchInlineSnapshot(` { "inputTokens": 20, "outputTokens": 5, "reasoningTokens": undefined, "totalTokens": 25, } `); }); it('should send additional response information', async () => { prepareJsonResponse({ id: 'test-id', created: 123, model: 'test-model', }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect({ id: response?.id, timestamp: response?.timestamp, modelId: response?.modelId, }).toStrictEqual({ id: 'test-id', timestamp: new Date(123 * 1000), modelId: 'test-model', }); }); it('should expose the raw response headers', async () => { prepareJsonResponse({ headers: { 'test-header': 'test-value' }, }); const { response } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-length': '271', 'content-type': 'application/json', // custom header 'test-header': 'test-value', }); }); it('should pass the model and the messages', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'grok-beta', messages: [{ role: 'user', content: 'Hello' }], }); }); it('should pass tools and toolChoice', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], toolChoice: { type: 'tool', toolName: 'test-tool', }, prompt: TEST_PROMPT, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'grok-beta', messages: [{ role: 'user', content: 'Hello' }], tools: [ { type: 'function', function: { name: 'test-tool', parameters: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, }, ], tool_choice: { type: 'function', function: { name: 'test-tool' }, }, }); }); it('should pass headers', async () => { prepareJsonResponse({ content: '' }); const modelWithHeaders = new XaiChatLanguageModel('grok-beta', { provider: 'xai.chat', baseURL: 'https://api.x.ai/v1', headers: () => ({ authorization: 'Bearer test-api-key', 'Custom-Provider-Header': 'provider-header-value', }), generateId: () => 'test-id', }); await modelWithHeaders.doGenerate({ prompt: TEST_PROMPT, headers: { 'Custom-Request-Header': 'request-header-value', }, }); const requestHeaders = server.calls[0].requestHeaders; expect(requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should send request body', async () => { prepareJsonResponse({ content: '' }); const { request } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(request).toMatchInlineSnapshot(` { "body": { "max_tokens": undefined, "messages": [ { "content": "Hello", "role": "user", }, ], "model": "grok-beta", "reasoning_effort": undefined, "response_format": undefined, "search_parameters": undefined, "seed": undefined, "temperature": undefined, "tool_choice": undefined, "tools": undefined, "top_p": undefined, }, } `); }); it('should pass search parameters', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { xai: { searchParameters: { mode: 'auto', returnCitations: true, fromDate: '2024-01-01', toDate: '2024-12-31', maxSearchResults: 10, }, }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'grok-beta', messages: [{ role: 'user', content: 'Hello' }], search_parameters: { mode: 'auto', return_citations: true, from_date: '2024-01-01', to_date: '2024-12-31', max_search_results: 10, }, }); }); it('should pass search parameters with sources array', async () => { prepareJsonResponse({ content: '' }); await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { xai: { searchParameters: { mode: 'on', sources: [ { type: 'web', country: 'US', excludedWebsites: ['example.com'], safeSearch: false, }, { type: 'x', xHandles: ['grok'], }, { type: 'news', country: 'GB', }, { type: 'rss', links: ['https://status.x.ai/feed.xml'], }, ], }, }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'grok-beta', messages: [{ role: 'user', content: 'Hello' }], search_parameters: { mode: 'on', sources: [ { type: 'web', country: 'US', excluded_websites: ['example.com'], safe_search: false, }, { type: 'x', x_handles: ['grok'], }, { type: 'news', country: 'GB', }, { type: 'rss', links: ['https://status.x.ai/feed.xml'], }, ], }, }); }); it('should extract content when message content is a content object', async () => { server.urls['https://api.x.ai/v1/chat/completions'].response = { type: 'json-value', body: { id: 'object-id', object: 'chat.completion', created: 1699472111, model: 'grok-beta', choices: [ { index: 0, message: { role: 'assistant', content: 'Hello from object', tool_calls: null, }, finish_reason: 'stop', }, ], usage: { prompt_tokens: 4, total_tokens: 34, completion_tokens: 30 }, }, }; const { content } = await model.doGenerate({ prompt: TEST_PROMPT, }); expect(content).toMatchInlineSnapshot(` [ { "text": "Hello from object", "type": "text", }, ] `); }); it('should extract citations as sources', async () => { server.urls['https://api.x.ai/v1/chat/completions'].response = { type: 'json-value', body: { id: 'citations-test', object: 'chat.completion', created: 1699472111, model: 'grok-beta', choices: [ { index: 0, message: { role: 'assistant', content: 'Here are the latest developments in AI.', tool_calls: null, }, finish_reason: 'stop', }, ], usage: { prompt_tokens: 4, total_tokens: 34, completion_tokens: 30 }, citations: [ 'https://example.com/article1', 'https://example.com/article2', ], }, }; const { content } = await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { xai: { searchParameters: { mode: 'auto', returnCitations: true, }, }, }, }); expect(content).toMatchInlineSnapshot(` [ { "text": "Here are the latest developments in AI.", "type": "text", }, { "id": "test-id", "sourceType": "url", "type": "source", "url": "https://example.com/article1", }, { "id": "test-id", "sourceType": "url", "type": "source", "url": "https://example.com/article2", }, ] `); }); it('should handle complex search parameter combinations', async () => { prepareJsonResponse({ content: 'Research results from multiple sources', }); await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { xai: { searchParameters: { mode: 'on', returnCitations: true, fromDate: '2024-01-01', toDate: '2024-12-31', maxSearchResults: 15, sources: [ { type: 'web', country: 'US', allowedWebsites: ['arxiv.org', 'nature.com'], safeSearch: true, }, { type: 'news', country: 'GB', excludedWebsites: ['tabloid.com'], }, { type: 'x', xHandles: ['openai', 'deepmind'], }, ], }, }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'grok-beta', messages: [{ role: 'user', content: 'Hello' }], search_parameters: { mode: 'on', return_citations: true, from_date: '2024-01-01', to_date: '2024-12-31', max_search_results: 15, sources: [ { type: 'web', country: 'US', allowed_websites: ['arxiv.org', 'nature.com'], safe_search: true, }, { type: 'news', country: 'GB', excluded_websites: ['tabloid.com'], }, { type: 'x', x_handles: ['openai', 'deepmind'], }, ], }, }); }); it('should handle empty citations array', async () => { server.urls['https://api.x.ai/v1/chat/completions'].response = { type: 'json-value', body: { id: 'no-citations-test', object: 'chat.completion', created: 1699472111, model: 'grok-beta', choices: [ { index: 0, message: { role: 'assistant', content: 'Response without citations.', tool_calls: null, }, finish_reason: 'stop', }, ], usage: { prompt_tokens: 4, total_tokens: 34, completion_tokens: 30 }, citations: [], }, }; const { content } = await model.doGenerate({ prompt: TEST_PROMPT, providerOptions: { xai: { searchParameters: { mode: 'auto', returnCitations: true, }, }, }, }); expect(content).toMatchInlineSnapshot(` [ { "text": "Response without citations.", "type": "text", }, ] `); }); }); describe('doStream', () => { function prepareStreamResponse({ content, headers, }: { content: string[]; headers?: Record<string, string>; }) { server.urls['https://api.x.ai/v1/chat/completions'].response = { type: 'stream-chunks', headers, chunks: [ `data: {"id":"35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe","object":"chat.completion.chunk",` + `"created":1750537778,"model":"grok-beta","choices":[{"index":0,` + `"delta":{"role":"assistant","content":""},"finish_reason":null}],"system_fingerprint":"fp_13a6dc65a6"}\n\n`, ...content.map(text => { return ( `data: {"id":"35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe","object":"chat.completion.chunk",` + `"created":1750537778,"model":"grok-beta","choices":[{"index":0,` + `"delta":{"role":"assistant","content":"${text}"},"finish_reason":null}],"system_fingerprint":"fp_13a6dc65a6"}\n\n` ); }), `data: {"id":"35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe","object":"chat.completion.chunk",` + `"created":1750537778,"model":"grok-beta","choices":[{"index":0,` + `"delta":{"content":""},"finish_reason":"stop"}],` + `"usage":{"prompt_tokens":4,"total_tokens":36,"completion_tokens":32},"system_fingerprint":"fp_13a6dc65a6"}\n\n`, `data: [DONE]\n\n`, ], }; } it('should stream text deltas', async () => { prepareStreamResponse({ content: ['Hello', ', ', 'world!'] }); const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe", "modelId": "grok-beta", "timestamp": 2025-06-21T20:29:38.000Z, "type": "response-metadata", }, { "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe", "type": "text-start", }, { "delta": "Hello", "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe", "type": "text-delta", }, { "delta": ", ", "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe", "type": "text-delta", }, { "delta": "world!", "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe", "type": "text-delta", }, { "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe", "type": "text-end", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 4, "outputTokens": 32, "reasoningTokens": undefined, "totalTokens": 36, }, }, ] `); }); it('should avoid duplication when there is a trailing assistant message', async () => { prepareStreamResponse({ content: ['prefix', ' and', ' more content'] }); const { stream } = await model.doStream({ prompt: [ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }, { role: 'assistant', content: [{ type: 'text', text: 'prefix ' }], }, ], includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe", "modelId": "grok-beta", "timestamp": 2025-06-21T20:29:38.000Z, "type": "response-metadata", }, { "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe", "type": "text-start", }, { "delta": "prefix", "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe", "type": "text-delta", }, { "delta": " and", "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe", "type": "text-delta", }, { "delta": " more content", "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe", "type": "text-delta", }, { "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe", "type": "text-end", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 4, "outputTokens": 32, "reasoningTokens": undefined, "totalTokens": 36, }, }, ] `); }); it('should stream tool deltas', async () => { server.urls['https://api.x.ai/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"a9648117-740c-4270-9e07-6a8457f23b7a","object":"chat.completion.chunk","created":1750535985,"model":"grok-beta",` + `"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}],"system_fingerprint":"fp_13a6dc65a6"}\n\n`, `data: {"id":"a9648117-740c-4270-9e07-6a8457f23b7a","object":"chat.completion.chunk","created":1750535985,"model":"grok-beta",` + `"choices":[{"index":0,"delta":{"content":null,"tool_calls":[{"id":"call_yfBEybNYi","type":"function","function":{"name":"test-tool","arguments":` + `"{\\"value\\":\\"Sparkle Day\\"}"` + `}}]},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":183,"total_tokens":316,"completion_tokens":133},"system_fingerprint":"fp_13a6dc65a6"}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ tools: [ { type: 'function', name: 'test-tool', inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }, ], prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "a9648117-740c-4270-9e07-6a8457f23b7a", "modelId": "grok-beta", "timestamp": 2025-06-21T19:59:45.000Z, "type": "response-metadata", }, { "id": "call_yfBEybNYi", "toolName": "test-tool", "type": "tool-input-start", }, { "delta": "{"value":"Sparkle Day"}", "id": "call_yfBEybNYi", "type": "tool-input-delta", }, { "id": "call_yfBEybNYi", "type": "tool-input-end", }, { "input": "{"value":"Sparkle Day"}", "toolCallId": "call_yfBEybNYi", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "tool-calls", "type": "finish", "usage": { "inputTokens": 183, "outputTokens": 133, "reasoningTokens": undefined, "totalTokens": 316, }, }, ] `); }); it('should expose the raw response headers', async () => { prepareStreamResponse({ content: [], headers: { 'test-header': 'test-value' }, }); const { response } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(response?.headers).toStrictEqual({ // default headers: 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', // custom header 'test-header': 'test-value', }); }); it('should pass the messages', async () => { prepareStreamResponse({ content: [''] }); await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ stream: true, model: 'grok-beta', messages: [{ role: 'user', content: 'Hello' }], stream_options: { include_usage: true, }, }); }); it('should pass headers', async () => { prepareStreamResponse({ content: [] }); const modelWithHeaders = new XaiChatLanguageModel('grok-beta', { provider: 'xai.chat', baseURL: 'https://api.x.ai/v1', headers: () => ({ authorization: 'Bearer test-api-key', 'Custom-Provider-Header': 'provider-header-value', }), generateId: () => 'test-id', }); await modelWithHeaders.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, headers: { 'Custom-Request-Header': 'request-header-value', }, }); expect(server.calls[0].requestHeaders).toStrictEqual({ authorization: 'Bearer test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', 'custom-request-header': 'request-header-value', }); }); it('should send request body', async () => { prepareStreamResponse({ content: [] }); const { request } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, }); expect(request).toMatchInlineSnapshot(` { "body": { "max_tokens": undefined, "messages": [ { "content": "Hello", "role": "user", }, ], "model": "grok-beta", "reasoning_effort": undefined, "response_format": undefined, "search_parameters": undefined, "seed": undefined, "stream": true, "stream_options": { "include_usage": true, }, "temperature": undefined, "tool_choice": undefined, "tools": undefined, "top_p": undefined, }, } `); }); it('should stream citations as sources', async () => { server.urls['https://api.x.ai/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"c8e45f92-7a3b-4d8e-9c1f-5e6a8b9d2f4c","object":"chat.completion.chunk","created":1750538200,"model":"grok-beta",` + `"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}],"system_fingerprint":"fp_13a6dc65a6"}\n\n`, `data: {"id":"c8e45f92-7a3b-4d8e-9c1f-5e6a8b9d2f4c","object":"chat.completion.chunk","created":1750538200,"model":"grok-beta",` + `"choices":[{"index":0,"delta":{"content":"Latest AI news"},"finish_reason":null}],"system_fingerprint":"fp_13a6dc65a6"}\n\n`, `data: {"id":"c8e45f92-7a3b-4d8e-9c1f-5e6a8b9d2f4c","object":"chat.completion.chunk","created":1750538200,"model":"grok-beta",` + `"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],` + `"usage":{"prompt_tokens":4,"total_tokens":34,"completion_tokens":30},` + `"citations":["https://example.com/source1","https://example.com/source2"],"system_fingerprint":"fp_13a6dc65a6"}\n\n`, `data: [DONE]\n\n`, ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, providerOptions: { xai: { searchParameters: { mode: 'auto', returnCitations: true, }, }, }, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "c8e45f92-7a3b-4d8e-9c1f-5e6a8b9d2f4c", "modelId": "grok-beta", "timestamp": 2025-06-21T20:36:40.000Z, "type": "response-metadata", }, { "id": "text-c8e45f92-7a3b-4d8e-9c1f-5e6a8b9d2f4c", "type": "text-start", }, { "delta": "Latest AI news", "id": "text-c8e45f92-7a3b-4d8e-9c1f-5e6a8b9d2f4c", "type": "text-delta", }, { "id": "test-id", "sourceType": "url", "type": "source", "url": "https://example.com/source1", }, { "id": "test-id", "sourceType": "url", "type": "source", "url": "https://example.com/source2", }, { "id": "text-c8e45f92-7a3b-4d8e-9c1f-5e6a8b9d2f4c", "type": "text-end", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 4, "outputTokens": 30, "reasoningTokens": undefined, "totalTokens": 34, }, }, ] `); }); }); describe('reasoning models', () => { const reasoningModel = new XaiChatLanguageModel('grok-3-mini', testConfig); function prepareReasoningResponse({ content = 'The result is 303.', reasoning_content = 'Let me calculate 101 multiplied by 3: 101 * 3 = 303.', usage = { prompt_tokens: 15, total_tokens: 35, completion_tokens: 20, completion_tokens_details: { reasoning_tokens: 10, }, }, }: { content?: string; reasoning_content?: string; usage?: { prompt_tokens: number; total_tokens: number; completion_tokens: number; completion_tokens_details?: { reasoning_tokens?: number; }; }; }) { server.urls['https://api.x.ai/v1/chat/completions'].response = { type: 'json-value', body: { id: 'chatcmpl-reasoning-test', object: 'chat.completion', created: 1699472111, model: 'grok-3-mini', choices: [ { index: 0, message: { role: 'assistant', content, reasoning_content, tool_calls: null, }, finish_reason: 'stop', }, ], usage, }, }; } it('should pass reasoning_effort parameter', async () => { prepareReasoningResponse({}); await reasoningModel.doGenerate({ prompt: TEST_PROMPT, providerOptions: { xai: { reasoningEffort: 'high' }, }, }); expect(await server.calls[0].requestBodyJson).toStrictEqual({ model: 'grok-3-mini', messages: [{ role: 'user', content: 'Hello' }], reasoning_effort: 'high', }); }); it('should extract reasoning content', async () => { prepareReasoningResponse({ content: 'The answer is 303.', reasoning_content: 'Let me think: 101 * 3 = 303.', }); const { content } = await reasoningModel.doGenerate({ prompt: TEST_PROMPT, providerOptions: { xai: { reasoningEffort: 'low' }, }, }); expect(content).toMatchInlineSnapshot(` [ { "text": "The answer is 303.", "type": "text", }, { "text": "Let me think: 101 * 3 = 303.", "type": "reasoning", }, ] `); }); it('should extract reasoning tokens from usage', async () => { prepareReasoningResponse({ usage: { prompt_tokens: 15, completion_tokens: 20, total_tokens: 35, completion_tokens_details: { reasoning_tokens: 10, }, }, }); const { usage } = await reasoningModel.doGenerate({ prompt: TEST_PROMPT, providerOptions: { xai: { reasoningEffort: 'high' }, }, }); expect(usage).toMatchInlineSnapshot(` { "inputTokens": 15, "outputTokens": 20, "reasoningTokens": 10, "totalTokens": 35, } `); }); it('should handle reasoning streaming', async () => { server.urls['https://api.x.ai/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b","object":"chat.completion.chunk","created":1750538120,"model":"grok-3-mini",` + `"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`, `data: {"id":"b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b","object":"chat.completion.chunk","created":1750538120,"model":"grok-3-mini",` + `"choices":[{"index":0,"delta":{"reasoning_content":"Let me calculate: "},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`, `data: {"id":"b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b","object":"chat.completion.chunk","created":1750538120,"model":"grok-3-mini",` + `"choices":[{"index":0,"delta":{"reasoning_content":"101 * 3 = 303"},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`, `data: {"id":"b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b","object":"chat.completion.chunk","created":1750538120,"model":"grok-3-mini",` + `"choices":[{"index":0,"delta":{"content":"The answer is 303."},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`, `data: {"id":"b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b","object":"chat.completion.chunk","created":1750538120,"model":"grok-3-mini",` + `"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],` + `"usage":{"prompt_tokens":15,"total_tokens":35,"completion_tokens":20,"completion_tokens_details":{"reasoning_tokens":10}},"system_fingerprint":"fp_reasoning_v1"}\n\n`, `data: [DONE]\n\n`, ], }; const { stream } = await reasoningModel.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, providerOptions: { xai: { reasoningEffort: 'low' }, }, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b", "modelId": "grok-3-mini", "timestamp": 2025-06-21T20:35:20.000Z, "type": "response-metadata", }, { "id": "reasoning-b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b", "type": "reasoning-start", }, { "delta": "Let me calculate: ", "id": "reasoning-b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b", "type": "reasoning-delta", }, { "delta": "101 * 3 = 303", "id": "reasoning-b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b", "type": "reasoning-delta", }, { "id": "text-b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b", "type": "text-start", }, { "delta": "The answer is 303.", "id": "text-b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b", "type": "text-delta", }, { "id": "reasoning-b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b", "type": "reasoning-end", }, { "id": "text-b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b", "type": "text-end", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 15, "outputTokens": 20, "reasoningTokens": 10, "totalTokens": 35, }, }, ] `); }); it('should deduplicate repetitive reasoning deltas', async () => { server.urls['https://api.x.ai/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"grok-4-test","object":"chat.completion.chunk","created":1750538120,"model":"grok-4-0709",` + `"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`, // Multiple identical "Thinking..." deltas (simulating Grok 4 issue) `data: {"id":"grok-4-test","object":"chat.completion.chunk","created":1750538120,"model":"grok-4-0709",` + `"choices":[{"index":0,"delta":{"reasoning_content":"Thinking... "},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`, `data: {"id":"grok-4-test","object":"chat.completion.chunk","created":1750538120,"model":"grok-4-0709",` + `"choices":[{"index":0,"delta":{"reasoning_content":"Thinking... "},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`, `data: {"id":"grok-4-test","object":"chat.completion.chunk","created":1750538120,"model":"grok-4-0709",` + `"choices":[{"index":0,"delta":{"reasoning_content":"Thinking... "},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`, // Different reasoning content should still come through `data: {"id":"grok-4-test","object":"chat.completion.chunk","created":1750538120,"model":"grok-4-0709",` + `"choices":[{"index":0,"delta":{"reasoning_content":"Actually calculating now..."},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`, `data: {"id":"grok-4-test","object":"chat.completion.chunk","created":1750538120,"model":"grok-4-0709",` + `"choices":[{"index":0,"delta":{"content":"The answer is 42."},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`, `data: {"id":"grok-4-test","object":"chat.completion.chunk","created":1750538120,"model":"grok-4-0709",` + `"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],` + `"usage":{"prompt_tokens":15,"total_tokens":35,"completion_tokens":20,"completion_tokens_details":{"reasoning_tokens":10}},"system_fingerprint":"fp_reasoning_v1"}\n\n`, `data: [DONE]\n\n`, ], }; const { stream } = await reasoningModel.doStream({ prompt: TEST_PROMPT, includeRawChunks: false, providerOptions: { xai: { reasoningEffort: 'low' }, }, }); expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "id": "grok-4-test", "modelId": "grok-4-0709", "timestamp": 2025-06-21T20:35:20.000Z, "type": "response-metadata", }, { "id": "reasoning-grok-4-test", "type": "reasoning-start", }, { "delta": "Thinking... ", "id": "reasoning-grok-4-test", "type": "reasoning-delta", }, { "delta": "Actually calculating now...", "id": "reasoning-grok-4-test", "type": "reasoning-delta", }, { "id": "text-grok-4-test", "type": "text-start", }, { "delta": "The answer is 42.", "id": "text-grok-4-test", "type": "text-delta", }, { "id": "reasoning-grok-4-test", "type": "reasoning-end", }, { "id": "text-grok-4-test", "type": "text-end", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 15, "outputTokens": 20, "reasoningTokens": 10, "totalTokens": 35, }, }, ] `); }); }); }); describe('doStream with raw chunks', () => { it('should stream raw chunks when includeRawChunks is true', async () => { server.urls['https://api.x.ai/v1/chat/completions'].response = { type: 'stream-chunks', chunks: [ `data: {"id":"d9f56e23-8b4c-4e7a-9d2f-6c8a9b5e3f7d","object":"chat.completion.chunk","created":1750538300,"model":"grok-beta","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null}],"system_fingerprint":"fp_13a6dc65a6"}\n\n`, `data: {"id":"e2a47b89-3f6d-4c8e-9a1b-7d5f8c9e2a4b","object":"chat.completion.chunk","created":1750538301,"model":"grok-beta","choices":[{"index":0,"delta":{"content":" world"},"finish_reason":null}],"system_fingerprint":"fp_13a6dc65a6"}\n\n`, `data: {"id":"f3b58c9a-4e7f-5d9e-ab2c-8e6f9d0e3b5c","object":"chat.completion.chunk","created":1750538302,"model":"grok-beta","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15},"citations":["https://example.com"],"system_fingerprint":"fp_13a6dc65a6"}\n\n`, 'data: [DONE]\n\n', ], }; const { stream } = await model.doStream({ prompt: TEST_PROMPT, includeRawChunks: true, }); const chunks = await convertReadableStreamToArray(stream); expect(chunks).toMatchInlineSnapshot(` [ { "type": "stream-start", "warnings": [], }, { "rawValue": { "choices": [ { "delta": { "content": "Hello", "role": "assistant", }, "finish_reason": null, "index": 0, }, ], "created": 1750538300, "id": "d9f56e23-8b4c-4e7a-9d2f-6c8a9b5e3f7d", "model": "grok-beta", "object": "chat.completion.chunk", "system_fingerprint": "fp_13a6dc65a6", }, "type": "raw", }, { "id": "d9f56e23-8b4c-4e7a-9d2f-6c8a9b5e3f7d", "modelId": "grok-beta", "timestamp": 2025-06-21T20:38:20.000Z, "type": "response-metadata", }, { "id": "text-d9f56e23-8b4c-4e7a-9d2f-6c8a9b5e3f7d", "type": "text-start", }, { "delta": "Hello", "id": "text-d9f56e23-8b4c-4e7a-9d2f-6c8a9b5e3f7d", "type": "text-delta", }, { "rawValue": { "choices": [ { "delta": { "content": " world", }, "finish_reason": null, "index": 0, }, ], "created": 1750538301, "id": "e2a47b89-3f6d-4c8e-9a1b-7d5f8c9e2a4b", "model": "grok-beta", "object": "chat.completion.chunk", "system_fingerprint": "fp_13a6dc65a6", }, "type": "raw", }, { "id": "text-e2a47b89-3f6d-4c8e-9a1b-7d5f8c9e2a4b", "type": "text-start", }, { "delta": " world", "id": "text-e2a47b89-3f6d-4c8e-9a1b-7d5f8c9e2a4b", "type": "text-delta", }, { "rawValue": { "choices": [ { "delta": {}, "finish_reason": "stop", "index": 0, }, ], "citations": [ "https://example.com", ], "created": 1750538302, "id": "f3b58c9a-4e7f-5d9e-ab2c-8e6f9d0e3b5c", "model": "grok-beta", "object": "chat.completion.chunk", "system_fingerprint": "fp_13a6dc65a6", "usage": { "completion_tokens": 5, "prompt_tokens": 10, "total_tokens": 15, }, }, "type": "raw", }, { "id": "test-id", "sourceType": "url", "type": "source", "url": "https://example.com", }, { "id": "text-d9f56e23-8b4c-4e7a-9d2f-6c8a9b5e3f7d", "type": "text-end", }, { "id": "text-e2a47b89-3f6d-4c8e-9a1b-7d5f8c9e2a4b", "type": "text-end", }, { "finishReason": "stop", "type": "finish", "usage": { "inputTokens": 10, "outputTokens": 5, "reasoningTokens": undefined, "totalTokens": 15, }, }, ] `); }); }); --- File: /ai/packages/xai/src/xai-chat-language-model.ts --- import { LanguageModelV2, LanguageModelV2CallWarning, LanguageModelV2Content, LanguageModelV2FinishReason, LanguageModelV2StreamPart, LanguageModelV2Usage, } from '@ai-sdk/provider'; import { combineHeaders, createEventSourceResponseHandler, createJsonResponseHandler, FetchFunction, parseProviderOptions, ParseResult, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { convertToXaiChatMessages } from './convert-to-xai-chat-messages'; import { getResponseMetadata } from './get-response-metadata'; import { mapXaiFinishReason } from './map-xai-finish-reason'; import { XaiChatModelId, xaiProviderOptions } from './xai-chat-options'; import { xaiFailedResponseHandler } from './xai-error'; import { prepareTools } from './xai-prepare-tools'; type XaiChatConfig = { provider: string; baseURL: string | undefined; headers: () => Record<string, string | undefined>; generateId: () => string; fetch?: FetchFunction; }; export class XaiChatLanguageModel implements LanguageModelV2 { readonly specificationVersion = 'v2'; readonly modelId: XaiChatModelId; private readonly config: XaiChatConfig; constructor(modelId: XaiChatModelId, config: XaiChatConfig) { this.modelId = modelId; this.config = config; } get provider(): string { return this.config.provider; } readonly supportedUrls: Record<string, RegExp[]> = { 'image/*': [/^https?:\/\/.*$/], }; private async getArgs({ prompt, maxOutputTokens, temperature, topP, topK, frequencyPenalty, presencePenalty, stopSequences, seed, responseFormat, providerOptions, tools, toolChoice, }: Parameters<LanguageModelV2['doGenerate']>[0]) { const warnings: LanguageModelV2CallWarning[] = []; // parse xai-specific provider options const options = (await parseProviderOptions({ provider: 'xai', providerOptions, schema: xaiProviderOptions, })) ?? {}; // check for unsupported parameters if (topK != null) { warnings.push({ type: 'unsupported-setting', setting: 'topK', }); } if (frequencyPenalty != null) { warnings.push({ type: 'unsupported-setting', setting: 'frequencyPenalty', }); } if (presencePenalty != null) { warnings.push({ type: 'unsupported-setting', setting: 'presencePenalty', }); } if (stopSequences != null) { warnings.push({ type: 'unsupported-setting', setting: 'stopSequences', }); } if ( responseFormat != null && responseFormat.type === 'json' && responseFormat.schema != null ) { warnings.push({ type: 'unsupported-setting', setting: 'responseFormat', details: 'JSON response format schema is not supported', }); } // convert ai sdk messages to xai format const { messages, warnings: messageWarnings } = convertToXaiChatMessages(prompt); warnings.push(...messageWarnings); // prepare tools for xai const { tools: xaiTools, toolChoice: xaiToolChoice, toolWarnings, } = prepareTools({ tools, toolChoice, }); warnings.push(...toolWarnings); const baseArgs = { // model id model: this.modelId, // standard generation settings max_tokens: maxOutputTokens, temperature, top_p: topP, seed, reasoning_effort: options.reasoningEffort, // response format response_format: responseFormat?.type === 'json' ? responseFormat.schema != null ? { type: 'json_schema', json_schema: { name: responseFormat.name ?? 'response', schema: responseFormat.schema, strict: true, }, } : { type: 'json_object' } : undefined, // search parameters search_parameters: options.searchParameters ? { mode: options.searchParameters.mode, return_citations: options.searchParameters.returnCitations, from_date: options.searchParameters.fromDate, to_date: options.searchParameters.toDate, max_search_results: options.searchParameters.maxSearchResults, sources: options.searchParameters.sources?.map(source => ({ type: source.type, ...(source.type === 'web' && { country: source.country, excluded_websites: source.excludedWebsites, allowed_websites: source.allowedWebsites, safe_search: source.safeSearch, }), ...(source.type === 'x' && { x_handles: source.xHandles, }), ...(source.type === 'news' && { country: source.country, excluded_websites: source.excludedWebsites, safe_search: source.safeSearch, }), ...(source.type === 'rss' && { links: source.links, }), })), } : undefined, // messages in xai format messages, // tools in xai format tools: xaiTools, tool_choice: xaiToolChoice, }; return { args: baseArgs, warnings, }; } async doGenerate( options: Parameters<LanguageModelV2['doGenerate']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>> { const { args: body, warnings } = await this.getArgs(options); const { responseHeaders, value: response, rawValue: rawResponse, } = await postJsonToApi({ url: `${this.config.baseURL ?? 'https://api.x.ai/v1'}/chat/completions`, headers: combineHeaders(this.config.headers(), options.headers), body, failedResponseHandler: xaiFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( xaiChatResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const choice = response.choices[0]; const content: Array<LanguageModelV2Content> = []; // extract text content if (choice.message.content != null && choice.message.content.length > 0) { let text = choice.message.content; // skip if this content duplicates the last assistant message const lastMessage = body.messages[body.messages.length - 1]; if (lastMessage?.role === 'assistant' && text === lastMessage.content) { text = ''; } if (text.length > 0) { content.push({ type: 'text', text }); } } // extract reasoning content if ( choice.message.reasoning_content != null && choice.message.reasoning_content.length > 0 ) { content.push({ type: 'reasoning', text: choice.message.reasoning_content, }); } // extract tool calls if (choice.message.tool_calls != null) { for (const toolCall of choice.message.tool_calls) { content.push({ type: 'tool-call', toolCallId: toolCall.id, toolName: toolCall.function.name, input: toolCall.function.arguments, }); } } // extract citations if (response.citations != null) { for (const url of response.citations) { content.push({ type: 'source', sourceType: 'url', id: this.config.generateId(), url, }); } } return { content, finishReason: mapXaiFinishReason(choice.finish_reason), usage: { inputTokens: response.usage.prompt_tokens, outputTokens: response.usage.completion_tokens, totalTokens: response.usage.total_tokens, reasoningTokens: response.usage.completion_tokens_details?.reasoning_tokens ?? undefined, }, request: { body }, response: { ...getResponseMetadata(response), headers: responseHeaders, body: rawResponse, }, warnings, }; } async doStream( options: Parameters<LanguageModelV2['doStream']>[0], ): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>> { const { args, warnings } = await this.getArgs(options); const body = { ...args, stream: true, stream_options: { include_usage: true, }, }; const { responseHeaders, value: response } = await postJsonToApi({ url: `${this.config.baseURL ?? 'https://api.x.ai/v1'}/chat/completions`, headers: combineHeaders(this.config.headers(), options.headers), body, failedResponseHandler: xaiFailedResponseHandler, successfulResponseHandler: createEventSourceResponseHandler(xaiChatChunkSchema), abortSignal: options.abortSignal, fetch: this.config.fetch, }); let finishReason: LanguageModelV2FinishReason = 'unknown'; const usage: LanguageModelV2Usage = { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, }; let isFirstChunk = true; const contentBlocks: Record<string, { type: 'text' | 'reasoning' }> = {}; const lastReasoningDeltas: Record<string, string> = {}; const self = this; return { stream: response.pipeThrough( new TransformStream< ParseResult<z.infer<typeof xaiChatChunkSchema>>, LanguageModelV2StreamPart >({ start(controller) { controller.enqueue({ type: 'stream-start', warnings }); }, transform(chunk, controller) { // Emit raw chunk if requested (before anything else) if (options.includeRawChunks) { controller.enqueue({ type: 'raw', rawValue: chunk.rawValue }); } if (!chunk.success) { controller.enqueue({ type: 'error', error: chunk.error }); return; } const value = chunk.value; // emit response metadata on first chunk if (isFirstChunk) { controller.enqueue({ type: 'response-metadata', ...getResponseMetadata(value), }); isFirstChunk = false; } // emit citations if present (they come in the last chunk according to docs) if (value.citations != null) { for (const url of value.citations) { controller.enqueue({ type: 'source', sourceType: 'url', id: self.config.generateId(), url, }); } } // update usage if present if (value.usage != null) { usage.inputTokens = value.usage.prompt_tokens; usage.outputTokens = value.usage.completion_tokens; usage.totalTokens = value.usage.total_tokens; usage.reasoningTokens = value.usage.completion_tokens_details?.reasoning_tokens ?? undefined; } const choice = value.choices[0]; // update finish reason if present if (choice?.finish_reason != null) { finishReason = mapXaiFinishReason(choice.finish_reason); } // exit if no delta to process if (choice?.delta == null) { return; } const delta = choice.delta; const choiceIndex = choice.index; // process text content if (delta.content != null && delta.content.length > 0) { const textContent = delta.content; // skip if this content duplicates the last assistant message const lastMessage = body.messages[body.messages.length - 1]; if ( lastMessage?.role === 'assistant' && textContent === lastMessage.content ) { return; } const blockId = `text-${value.id || choiceIndex}`; if (contentBlocks[blockId] == null) { contentBlocks[blockId] = { type: 'text' }; controller.enqueue({ type: 'text-start', id: blockId, }); } controller.enqueue({ type: 'text-delta', id: blockId, delta: textContent, }); } // process reasoning content if ( delta.reasoning_content != null && delta.reasoning_content.length > 0 ) { const blockId = `reasoning-${value.id || choiceIndex}`; // skip if this reasoning content duplicates the last delta if (lastReasoningDeltas[blockId] === delta.reasoning_content) { return; } lastReasoningDeltas[blockId] = delta.reasoning_content; if (contentBlocks[blockId] == null) { contentBlocks[blockId] = { type: 'reasoning' }; controller.enqueue({ type: 'reasoning-start', id: blockId, }); } controller.enqueue({ type: 'reasoning-delta', id: blockId, delta: delta.reasoning_content, }); } // process tool calls if (delta.tool_calls != null) { for (const toolCall of delta.tool_calls) { // xai tool calls come in one piece (like mistral) const toolCallId = toolCall.id; controller.enqueue({ type: 'tool-input-start', id: toolCallId, toolName: toolCall.function.name, }); controller.enqueue({ type: 'tool-input-delta', id: toolCallId, delta: toolCall.function.arguments, }); controller.enqueue({ type: 'tool-input-end', id: toolCallId, }); controller.enqueue({ type: 'tool-call', toolCallId, toolName: toolCall.function.name, input: toolCall.function.arguments, }); } } }, flush(controller) { for (const [blockId, block] of Object.entries(contentBlocks)) { controller.enqueue({ type: block.type === 'text' ? 'text-end' : 'reasoning-end', id: blockId, }); } controller.enqueue({ type: 'finish', finishReason, usage }); }, }), ), request: { body }, response: { headers: responseHeaders }, }; } } // XAI API Response Schemas const xaiUsageSchema = z.object({ prompt_tokens: z.number(), completion_tokens: z.number(), total_tokens: z.number(), completion_tokens_details: z .object({ reasoning_tokens: z.number().nullish(), }) .nullish(), }); const xaiChatResponseSchema = z.object({ id: z.string().nullish(), created: z.number().nullish(), model: z.string().nullish(), choices: z.array( z.object({ message: z.object({ role: z.literal('assistant'), content: z.string().nullish(), reasoning_content: z.string().nullish(), tool_calls: z .array( z.object({ id: z.string(), type: z.literal('function'), function: z.object({ name: z.string(), arguments: z.string(), }), }), ) .nullish(), }), index: z.number(), finish_reason: z.string().nullish(), }), ), object: z.literal('chat.completion'), usage: xaiUsageSchema, citations: z.array(z.string().url()).nullish(), }); const xaiChatChunkSchema = z.object({ id: z.string().nullish(), created: z.number().nullish(), model: z.string().nullish(), choices: z.array( z.object({ delta: z.object({ role: z.enum(['assistant']).optional(), content: z.string().nullish(), reasoning_content: z.string().nullish(), tool_calls: z .array( z.object({ id: z.string(), type: z.literal('function'), function: z.object({ name: z.string(), arguments: z.string(), }), }), ) .nullish(), }), finish_reason: z.string().nullish(), index: z.number(), }), ), usage: xaiUsageSchema.nullish(), citations: z.array(z.string().url()).nullish(), }); --- File: /ai/packages/xai/src/xai-chat-options.ts --- import { z } from 'zod/v4'; // https://console.x.ai and see "View models" export type XaiChatModelId = | 'grok-4' | 'grok-4-0709' | 'grok-4-latest' | 'grok-3' | 'grok-3-latest' | 'grok-3-fast' | 'grok-3-fast-latest' | 'grok-3-mini' | 'grok-3-mini-latest' | 'grok-3-mini-fast' | 'grok-3-mini-fast-latest' | 'grok-2-vision-1212' | 'grok-2-vision' | 'grok-2-vision-latest' | 'grok-2-image-1212' | 'grok-2-image' | 'grok-2-image-latest' | 'grok-2-1212' | 'grok-2' | 'grok-2-latest' | 'grok-vision-beta' | 'grok-beta' | (string & {}); // search source schemas const webSourceSchema = z.object({ type: z.literal('web'), country: z.string().length(2).optional(), excludedWebsites: z.array(z.string()).max(5).optional(), allowedWebsites: z.array(z.string()).max(5).optional(), safeSearch: z.boolean().optional(), }); const xSourceSchema = z.object({ type: z.literal('x'), xHandles: z.array(z.string()).optional(), }); const newsSourceSchema = z.object({ type: z.literal('news'), country: z.string().length(2).optional(), excludedWebsites: z.array(z.string()).max(5).optional(), safeSearch: z.boolean().optional(), }); const rssSourceSchema = z.object({ type: z.literal('rss'), links: z.array(z.string().url()).max(1), // currently only supports one RSS link }); const searchSourceSchema = z.discriminatedUnion('type', [ webSourceSchema, xSourceSchema, newsSourceSchema, rssSourceSchema, ]); // xai-specific provider options export const xaiProviderOptions = z.object({ /** * reasoning effort for reasoning models * only supported by grok-3-mini and grok-3-mini-fast models */ reasoningEffort: z.enum(['low', 'high']).optional(), searchParameters: z .object({ /** * search mode preference * - "off": disables search completely * - "auto": model decides whether to search (default) * - "on": always enables search */ mode: z.enum(['off', 'auto', 'on']), /** * whether to return citations in the response * defaults to true */ returnCitations: z.boolean().optional(), /** * start date for search data (ISO8601 format: YYYY-MM-DD) */ fromDate: z.string().optional(), /** * end date for search data (ISO8601 format: YYYY-MM-DD) */ toDate: z.string().optional(), /** * maximum number of search results to consider * defaults to 20 */ maxSearchResults: z.number().min(1).max(50).optional(), /** * data sources to search from * defaults to ["web", "x"] if not specified */ sources: z.array(searchSourceSchema).optional(), }) .optional(), }); export type XaiProviderOptions = z.infer<typeof xaiProviderOptions>; --- File: /ai/packages/xai/src/xai-chat-prompt.ts --- export type XaiChatPrompt = Array<XaiChatMessage>; export type XaiChatMessage = | XaiSystemMessage | XaiUserMessage | XaiAssistantMessage | XaiToolMessage; export interface XaiSystemMessage { role: 'system'; content: string; } export interface XaiUserMessage { role: 'user'; content: string | Array<XaiUserMessageContent>; } export type XaiUserMessageContent = | { type: 'text'; text: string } | { type: 'image_url'; image_url: { url: string } }; export interface XaiAssistantMessage { role: 'assistant'; content: string; tool_calls?: Array<{ id: string; type: 'function'; function: { name: string; arguments: string }; }>; } export interface XaiToolMessage { role: 'tool'; tool_call_id: string; content: string; } // xai tool choice export type XaiToolChoice = | 'auto' | 'none' | 'required' | { type: 'function'; function: { name: string } }; --- File: /ai/packages/xai/src/xai-error.ts --- import { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; // Add error schema and structure export const xaiErrorDataSchema = z.object({ error: z.object({ message: z.string(), type: z.string().nullish(), param: z.any().nullish(), code: z.union([z.string(), z.number()]).nullish(), }), }); export type XaiErrorData = z.infer<typeof xaiErrorDataSchema>; export const xaiFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: xaiErrorDataSchema, errorToMessage: data => data.error.message, }); --- File: /ai/packages/xai/src/xai-image-settings.ts --- export type XaiImageModelId = 'grok-2-image' | (string & {}); --- File: /ai/packages/xai/src/xai-prepare-tools.ts --- import { LanguageModelV2CallOptions, LanguageModelV2CallWarning, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { XaiToolChoice } from './xai-chat-prompt'; export function prepareTools({ tools, toolChoice, }: { tools: LanguageModelV2CallOptions['tools']; toolChoice?: LanguageModelV2CallOptions['toolChoice']; }): { tools: | Array<{ type: 'function'; function: { name: string; description: string | undefined; parameters: unknown; }; }> | undefined; toolChoice: XaiToolChoice | undefined; toolWarnings: LanguageModelV2CallWarning[]; } { // when the tools array is empty, change it to undefined to prevent errors tools = tools?.length ? tools : undefined; const toolWarnings: LanguageModelV2CallWarning[] = []; if (tools == null) { return { tools: undefined, toolChoice: undefined, toolWarnings }; } // convert ai sdk tools to xai format const xaiTools: Array<{ type: 'function'; function: { name: string; description: string | undefined; parameters: unknown; }; }> = []; for (const tool of tools) { if (tool.type === 'provider-defined') { toolWarnings.push({ type: 'unsupported-tool', tool }); } else { xaiTools.push({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.inputSchema, }, }); } } if (toolChoice == null) { return { tools: xaiTools, toolChoice: undefined, toolWarnings }; } const type = toolChoice.type; switch (type) { case 'auto': case 'none': return { tools: xaiTools, toolChoice: type, toolWarnings }; case 'required': // xai supports 'required' directly return { tools: xaiTools, toolChoice: 'required', toolWarnings }; case 'tool': // xai supports specific tool selection return { tools: xaiTools, toolChoice: { type: 'function', function: { name: toolChoice.toolName }, }, toolWarnings, }; default: { const _exhaustiveCheck: never = type; throw new UnsupportedFunctionalityError({ functionality: `tool choice type: ${_exhaustiveCheck}`, }); } } } --- File: /ai/packages/xai/src/xai-provider.test.ts --- import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; import { createXai } from './xai-provider'; import { loadApiKey } from '@ai-sdk/provider-utils'; import { XaiChatLanguageModel } from './xai-chat-language-model'; import { OpenAICompatibleImageModel } from '@ai-sdk/openai-compatible'; const XaiChatLanguageModelMock = XaiChatLanguageModel as unknown as Mock; const OpenAICompatibleImageModelMock = OpenAICompatibleImageModel as unknown as Mock; vi.mock('./xai-chat-language-model', () => ({ XaiChatLanguageModel: vi.fn(), })); vi.mock('@ai-sdk/openai-compatible', () => ({ OpenAICompatibleChatLanguageModel: vi.fn(), OpenAICompatibleCompletionLanguageModel: vi.fn(), OpenAICompatibleEmbeddingModel: vi.fn(), OpenAICompatibleImageModel: vi.fn(), })); vi.mock('./xai-image-model', () => ({ XaiImageModel: vi.fn(), })); vi.mock('@ai-sdk/provider-utils', () => ({ loadApiKey: vi.fn().mockReturnValue('mock-api-key'), withoutTrailingSlash: vi.fn(url => url), createJsonErrorResponseHandler: vi.fn().mockReturnValue(() => {}), generateId: vi.fn().mockReturnValue('mock-id'), })); describe('xAIProvider', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('createXAI', () => { it('should create an XAIProvider instance with default options', () => { const provider = createXai(); const model = provider('model-id'); const constructorCall = XaiChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; config.headers(); expect(loadApiKey).toHaveBeenCalledWith({ apiKey: undefined, environmentVariableName: 'XAI_API_KEY', description: 'xAI API key', }); }); it('should create an XAIProvider instance with custom options', () => { const options = { apiKey: 'custom-key', baseURL: 'https://custom.url', headers: { 'Custom-Header': 'value' }, }; const provider = createXai(options); provider('model-id'); const constructorCall = XaiChatLanguageModelMock.mock.calls[0]; const config = constructorCall[1]; config.headers(); expect(loadApiKey).toHaveBeenCalledWith({ apiKey: 'custom-key', environmentVariableName: 'XAI_API_KEY', description: 'xAI API key', }); }); it('should return a chat model when called as a function', () => { const provider = createXai(); const modelId = 'foo-model-id'; const model = provider(modelId); expect(model).toBeInstanceOf(XaiChatLanguageModel); }); }); describe('chatModel', () => { it('should construct a chat model with correct configuration', () => { const provider = createXai(); const modelId = 'xai-chat-model'; const model = provider.chat(modelId); expect(model).toBeInstanceOf(XaiChatLanguageModel); }); it('should pass the includeUsage option to the chat model, to make sure usage is reported while streaming', () => { const provider = createXai(); const modelId = 'xai-chat-model'; const model = provider.chat(modelId); expect(model).toBeInstanceOf(XaiChatLanguageModel); const constructorCall = XaiChatLanguageModelMock.mock.calls[0]; expect(constructorCall[0]).toBe(modelId); expect(constructorCall[1].provider).toBe('xai.chat'); expect(constructorCall[1].baseURL).toBe('https://api.x.ai/v1'); }); }); describe('imageModel', () => { it('should construct an image model with correct configuration', () => { const provider = createXai(); const modelId = 'grok-2-image'; const model = provider.imageModel(modelId); expect(model).toBeInstanceOf(OpenAICompatibleImageModel); const constructorCall = OpenAICompatibleImageModelMock.mock.calls[0]; expect(constructorCall[0]).toBe(modelId); const config = constructorCall[1]; expect(config.provider).toBe('xai.image'); expect(config.url({ path: '/test-path' })).toBe( 'https://api.x.ai/v1/test-path', ); }); it('should use custom baseURL for image model', () => { const customBaseURL = 'https://custom.xai.api'; const provider = createXai({ baseURL: customBaseURL }); const modelId = 'grok-2-image'; provider.imageModel(modelId); const constructorCall = OpenAICompatibleImageModelMock.mock.calls[0]; const config = constructorCall[1]; expect(config.url({ path: '/test-path' })).toBe( `${customBaseURL}/test-path`, ); }); it('should pass custom headers to image model', () => { const customHeaders = { 'Custom-Header': 'test-value' }; const provider = createXai({ headers: customHeaders }); provider.imageModel('grok-2-image'); const constructorCall = OpenAICompatibleImageModelMock.mock.calls[0]; const config = constructorCall[1]; const headers = config.headers(); expect(headers).toMatchObject({ Authorization: 'Bearer mock-api-key', 'Custom-Header': 'test-value', }); }); }); }); --- File: /ai/packages/xai/src/xai-provider.ts --- import { OpenAICompatibleImageModel, ProviderErrorStructure, } from '@ai-sdk/openai-compatible'; import { ImageModelV2, LanguageModelV2, NoSuchModelError, ProviderV2, } from '@ai-sdk/provider'; import { FetchFunction, generateId, loadApiKey, withoutTrailingSlash, } from '@ai-sdk/provider-utils'; import { XaiChatLanguageModel } from './xai-chat-language-model'; import { XaiChatModelId } from './xai-chat-options'; import { XaiErrorData, xaiErrorDataSchema } from './xai-error'; import { XaiImageModelId } from './xai-image-settings'; const xaiErrorStructure: ProviderErrorStructure<XaiErrorData> = { errorSchema: xaiErrorDataSchema, errorToMessage: data => data.error.message, }; export interface XaiProvider extends ProviderV2 { /** Creates an Xai chat model for text generation. */ (modelId: XaiChatModelId): LanguageModelV2; /** Creates an Xai language model for text generation. */ languageModel(modelId: XaiChatModelId): LanguageModelV2; /** Creates an Xai chat model for text generation. */ chat: (modelId: XaiChatModelId) => LanguageModelV2; /** Creates an Xai image model for image generation. */ image(modelId: XaiImageModelId): ImageModelV2; /** Creates an Xai image model for image generation. */ imageModel(modelId: XaiImageModelId): ImageModelV2; } export interface XaiProviderSettings { /** Base URL for the xAI API calls. */ baseURL?: string; /** API key for authenticating requests. */ apiKey?: string; /** Custom headers to include in the requests. */ headers?: Record<string, string>; /** Custom fetch implementation. You can use it as a middleware to intercept requests, or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; } export function createXai(options: XaiProviderSettings = {}): XaiProvider { const baseURL = withoutTrailingSlash( options.baseURL ?? 'https://api.x.ai/v1', ); const getHeaders = () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'XAI_API_KEY', description: 'xAI API key', })}`, ...options.headers, }); const createLanguageModel = (modelId: XaiChatModelId) => { return new XaiChatLanguageModel(modelId, { provider: 'xai.chat', baseURL, headers: getHeaders, generateId, fetch: options.fetch, }); }; const createImageModel = (modelId: XaiImageModelId) => { return new OpenAICompatibleImageModel(modelId, { provider: 'xai.image', url: ({ path }) => `${baseURL}${path}`, headers: getHeaders, fetch: options.fetch, errorStructure: xaiErrorStructure, }); }; const provider = (modelId: XaiChatModelId) => createLanguageModel(modelId); provider.languageModel = createLanguageModel; provider.chat = createLanguageModel; provider.textEmbeddingModel = (modelId: string) => { throw new NoSuchModelError({ modelId, modelType: 'textEmbeddingModel' }); }; provider.imageModel = createImageModel; provider.image = createImageModel; return provider; } export const xai = createXai(); --- File: /ai/packages/xai/tsup.config.ts --- import { defineConfig } from 'tsup'; export default defineConfig([ { entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, sourcemap: true, }, ]); --- File: /ai/packages/xai/vitest.edge.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'edge-runtime', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/packages/xai/vitest.node.config.js --- import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: 'node', globals: true, include: ['**/*.test.ts', '**/*.test.tsx'], }, }); --- File: /ai/tools/analyze-downloads/src/analyze-market.ts --- #!/usr/bin/env tsx import * as https from 'https'; /** * Fetches the raw HTML text from the given URL using https. */ function fetchPage(url: string, retries = 10, delay = 4000): Promise<string> { return new Promise((resolve, reject) => { https .get(url, res => { if (res.statusCode === 429 && retries > 0) { // Handle rate limiting with exponential backoff const retryDelay = delay * 2; console.log(`Rate limited, retrying in ${delay}ms...`); setTimeout(() => { fetchPage(url, retries - 1, retryDelay) .then(resolve) .catch(reject); }, delay); return; } if (res.statusCode !== 200) { reject( new Error(`Failed to fetch ${url}. Status code: ${res.statusCode}`), ); return; } let rawData = ''; res.on('data', chunk => (rawData += chunk)); res.on('end', () => resolve(rawData)); }) .on('error', err => reject(err)); }); } /** * Extracts weekly downloads from the npm package page * using a regex-based search on the HTML. */ function parseWeeklyDownloads(html: string): number { // Look for the weekly downloads number in the new HTML structure const weeklyDownloadsRegex = /Weekly Downloads<\/h3>.*?<p[^>]*>([0-9,]+)<\/p>/s; const match = html.match(weeklyDownloadsRegex); if (!match) { return 0; } const downloadsStr = match[1].replace(/[^\d]/g, ''); // remove commas return parseInt(downloadsStr, 10) || 0; } /** * Main execution function. */ async function main() { const packages = [ 'ai', 'openai', '@anthropic-ai/sdk', 'langchain', '@aws-sdk/client-bedrock-runtime', '@google/generative-ai', '@google-cloud/vertexai', '@xenova/transformers', '@mistralai/mistralai', 'llamaindex', '@instructor-ai/instructor', 'together-ai', ]; const results: Array<{ package: string; 'weekly downloads': number; percentage: string; }> = []; try { for (const pkg of packages) { const url = `https://www.npmjs.com/package/${pkg}`; const html = await fetchPage(url); const weeklyDownloads = parseWeeklyDownloads(html); results.push({ package: pkg, 'weekly downloads': weeklyDownloads, percentage: '0%', // Initial placeholder }); } // Calculate total downloads const totalDownloads = results.reduce( (sum, item) => sum + item['weekly downloads'], 0, ); // Update percentages results.forEach(item => { const percentage = (item['weekly downloads'] / totalDownloads) * 100; item['percentage'] = `${percentage.toFixed(1)}%`; }); // Sort results by weekly downloads in descending order results.sort((a, b) => b['weekly downloads'] - a['weekly downloads']); console.table(results); } catch (err) { console.error('Error:', err); } } main(); --- File: /ai/tools/analyze-downloads/src/analyze-providers.ts --- #!/usr/bin/env tsx import * as https from 'https'; /** * Fetches the raw HTML text from the given URL using https. */ function fetchPage(url: string, retries = 10, delay = 4000): Promise<string> { return new Promise((resolve, reject) => { https .get(url, res => { if (res.statusCode === 429 && retries > 0) { // Handle rate limiting with exponential backoff const retryDelay = delay * 2; console.log(`Rate limited, retrying in ${delay}ms...`); setTimeout(() => { fetchPage(url, retries - 1, retryDelay) .then(resolve) .catch(reject); }, delay); return; } if (res.statusCode !== 200) { reject( new Error(`Failed to fetch page. Status code: ${res.statusCode}`), ); return; } let rawData = ''; res.on('data', chunk => (rawData += chunk)); res.on('end', () => resolve(rawData)); }) .on('error', err => reject(err)); }); } /** * Extracts weekly downloads from the npm package page * using a regex-based search on the HTML. */ function parseWeeklyDownloads(html: string): number { // Look for the weekly downloads number in the new HTML structure const weeklyDownloadsRegex = /Weekly Downloads<\/h3>.*?<p[^>]*>([0-9,]+)<\/p>/s; const match = html.match(weeklyDownloadsRegex); if (!match) { return 0; } const downloadsStr = match[1].replace(/[^\d]/g, ''); // remove commas return parseInt(downloadsStr, 10) || 0; } /** * Main execution function. */ async function main() { const packages = [ '@ai-sdk/openai', '@ai-sdk/openai-compatible', '@ai-sdk/azure', '@ai-sdk/anthropic', '@ai-sdk/amazon-bedrock', '@ai-sdk/google', '@ai-sdk/google-vertex', '@ai-sdk/mistral', '@ai-sdk/xai', '@ai-sdk/togetherai', '@ai-sdk/cohere', '@ai-sdk/fireworks', '@ai-sdk/deepinfra', '@ai-sdk/deepseek', '@ai-sdk/cerebras', '@ai-sdk/groq', '@ai-sdk/replicate', '@ai-sdk/perplexity', '@ai-sdk/vercel', '@ai-sdk/assemblyai', '@ai-sdk/deepgram', '@ai-sdk/elevenlabs', '@ai-sdk/fal', '@ai-sdk/gateway', '@ai-sdk/gladia', '@ai-sdk/hume', '@ai-sdk/langchain', '@ai-sdk/llamaindex', '@ai-sdk/lmnt', '@ai-sdk/luma', '@ai-sdk/revai', '@ai-sdk/valibot', 'ollama-ai-provider', '@portkey-ai/vercel-provider', 'workers-ai-provider', '@openrouter/ai-sdk-provider', '@langdb/vercel-provider', ]; const results: Array<{ package: string; 'weekly downloads': number; percentage: string; }> = []; try { for (const pkg of packages) { const url = `https://www.npmjs.com/package/${pkg}`; const html = await fetchPage(url); const weeklyDownloads = parseWeeklyDownloads(html); results.push({ package: pkg, 'weekly downloads': weeklyDownloads, percentage: '0%', // Initial placeholder }); } // Calculate total downloads const totalDownloads = results.reduce( (sum, item) => sum + item['weekly downloads'], 0, ); // Update percentages results.forEach(item => { const percentage = (item['weekly downloads'] / totalDownloads) * 100; item['percentage'] = `${percentage.toFixed(1)}%`; }); // Sort results by weekly downloads in descending order results.sort((a, b) => b['weekly downloads'] - a['weekly downloads']); console.table(results); } catch (err) { console.error('Error:', err); } } main(); --- File: /ai/tools/analyze-downloads/src/analyze-versions.ts --- #!/usr/bin/env tsx import * as https from 'https'; /** * Fetches the raw HTML text from the given URL using https. */ function fetchPage(url: string): Promise<string> { return new Promise((resolve, reject) => { https .get(url, res => { if (res.statusCode !== 200) { reject( new Error(`Failed to fetch page. Status code: ${res.statusCode}`), ); return; } let rawData = ''; res.on('data', chunk => (rawData += chunk)); res.on('end', () => resolve(rawData)); }) .on('error', err => reject(err)); }); } /** * Extracts version + all-time downloads from the npm versions page * by applying a naive regex-based search on the HTML. * * Returns an array of { version, weeklyDownloads } objects. */ function parseVersions( html: string, ): { version: string; weeklyDownloads: number }[] { const results: { version: string; weeklyDownloads: number }[] = []; // First find the Version History section const versionHistorySection = html.split('Version History')[1]; if (!versionHistorySection) { return results; } /** * Regex matching the npm version table row structure in Version History: * - Matches version number in <a> tag * - Matches download count in <td class="downloads"> */ const versionRegex = /<a href="\/package\/ai\/v\/([^"]+)"[^>]*>[^<]+<\/a><\/td><td class="downloads">([\d,]+)/g; let match: RegExpExecArray | null; while ((match = versionRegex.exec(versionHistorySection)) !== null) { const version = match[1]; // Skip versions starting with 0.x or 1.x if (version.startsWith('0.') || version.startsWith('1.')) { continue; } const downloadsStr = match[2].replace(/[^\d]/g, ''); // remove commas, etc. const downloads = parseInt(downloadsStr, 10); if (!isNaN(downloads)) { results.push({ version, weeklyDownloads: downloads, }); } } return results; } /** * Converts a full version string like "1.2.3" (or "1.2.3-alpha") * to its "major.minor" part, e.g. "1.2" */ function toMinorVersion(fullVersion: string): string { // Split on dot to handle something like "1.2.3" // If the version has a pre-release, e.g. "1.2.3-alpha.1", we still // only extract [major, minor] from the front. const [major, minor] = fullVersion.split('.'); return [major ?? '0', minor ?? '0'].join('.'); } /** * Aggregates the download counts by major.minor key. */ function aggregateByMinor( data: { version: string; weeklyDownloads: number }[], ): Record<string, number> { const output: Record<string, number> = {}; for (const entry of data) { const minor = toMinorVersion(entry.version); output[minor] = (output[minor] || 0) + entry.weeklyDownloads; } return output; } /** * Main execution function. */ async function main() { const url = 'https://www.npmjs.com/package/ai?activeTab=versions'; try { const html = await fetchPage(url); const parsed = parseVersions(html); const aggregated = aggregateByMinor(parsed); // Calculate total downloads const totalDownloads = Object.values(aggregated).reduce( (sum, count) => sum + count, 0, ); // Convert the aggregated data into an array of objects for console.table const results = Object.entries(aggregated).map(([version, downloads]) => ({ version, 'weekly downloads': downloads, percentage: ((downloads / totalDownloads) * 100).toFixed(1) + '%', })); // Show the results in a table format console.log('Aggregated downloads by minor version:'); console.table(results); } catch (err) { console.error('Error:', err); } } main(); --- File: /ai/tools/eslint-config/index.js --- module.exports = { extends: ['next', 'turbo', 'prettier'], rules: { '@next/next/no-html-link-for-pages': 'off', }, parserOptions: { babelOptions: { presets: [require.resolve('next/babel')], }, }, }; --- File: /ai/tools/generate-llms-txt/src/generate-llms-txt.ts --- #!/usr/bin/env tsx import { readdir, readFile, writeFile } from 'fs/promises'; import { join } from 'path'; async function getAllFiles(dir: string): Promise<string[]> { const files: string[] = []; const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(dir, entry.name); if (entry.isDirectory()) { files.push(...(await getAllFiles(fullPath))); } else { files.push(fullPath); } } return files; } const exclusionPrefixes = [ 'cookbook/01-next', 'cookbook/05-node', 'cookbook/15-api-servers/index', 'cookbook/20-rsc', 'docs/01-introduction', 'docs/02-foundations', 'docs/02-getting-started', 'docs/02-guides', 'docs/03-ai-sdk-core/index', 'docs/04-ai-sdk-ui/index', 'docs/04-ai-sdk-ui/50-stream-protocol', 'docs/05-ai-sdk-rsc', 'docs/06-advanced', 'docs/07-reference/index', 'docs/07-reference/01-ai-sdk-core', 'docs/07-reference/02-ai-sdk-ui', 'docs/07-reference/03-ai-sdk-rsc', 'docs/07-reference/04-stream-helpers', 'docs/07-reference/05-ai-sdk-errors/index', 'docs/08-migration-guides', 'docs/09-troubleshooting', 'providers/01-ai-sdk-providers/index', 'providers/02-openai-compatible-providers/01-custom-providers', 'providers/02-openai-compatible-providers/40-baseten', 'providers/03-community-providers', 'providers/04-adapters', 'providers/05-observability', ]; async function main() { try { const contentDir = join(process.cwd(), '../../content'); const files = await getAllFiles(contentDir); const filteredFiles = files.filter(file => { for (const prefix of exclusionPrefixes) { if (file.includes(prefix)) { return false; } } return true; }); console.log(filteredFiles.join('\n')); console.log(); let fullContent = ''; for (const file of filteredFiles) { const content = await readFile(file, 'utf-8'); fullContent += content; fullContent += '\n\n'; } console.log(`Length (chars ): ${fullContent.length}`); console.log(`Length (approx tokens): ${fullContent.length / 4}`); // write to llms.txt await writeFile('llms.txt', fullContent); } catch (error) { console.error('Error:', error); process.exit(1); } } main(); --- File: /ai/.eslintrc.js --- module.exports = { root: true, // This tells ESLint to load the config from the package `eslint-config-vercel-ai` extends: ['vercel-ai'], settings: { next: { rootDir: ['apps/*/'], }, }, };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/steipete/Peekaboo'

If you have feedback or need assistance with the MCP directory API, please join our Discord server